Compare commits

...

4 Commits

  1. 78
      .github/workflows/runtests.yml
  2. 4
      Makefile
  3. 101
      cmd/client_server/client.go
  4. 79
      cmd/client_server/handlers.go
  5. 37
      cmd/client_server/server.go
  6. 14
      cmd/requests_types/response_type.go
  7. 10
      internal/json/valid_response.json
  8. 5
      internal/moc_server.go

78
.github/workflows/runtests.yml

@ -0,0 +1,78 @@
# This is a basic workflow to help you get started with Actions
name: CI
# Controls when the workflow will run
on:
# Triggers the workflow on push or pull request events but only for the "main" branch
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.19
check-latest: true
- name: Create dir for binaries
run: mkdir builds
- name: Download DSP binary
run: curl -o builds/dsp https://dummy-dsp.s3.eu-central-1.amazonaws.com/dsp
- name: Making it executable
run: chmod +x builds/dsp
- name: Download tests binary
run: curl -o builds/tests https://dummy-dsp.s3.eu-central-1.amazonaws.com/tests
- name: Making it executable
run: chmod +x builds/tests
# your code must be built into builds/ssp binary
- name: Build SSP binary
run: go build -o builds/ssp cmd/main.go
- name: Making it executable
run: chmod +x builds/ssp
# run a battery of tests
- name: happy path
run: (cd builds && ./tests -test.v -test.run Test_HappyPath) && sleep 1
- name: not totally happy path, easy difficulty
run: (cd builds && ./tests -test.v -test.run Test_LessHappyEasy) && sleep 1
- name: not totally happy path, medium difficulty
run: (cd builds && ./tests -test.v -test.run Test_LessHappyMedium) && sleep 1
- name: not totally happy path, hard difficulty
run: (cd builds && ./tests -test.v -test.run Test_LessHappyHard) && sleep 1
- name: not happy at all, easy difficulty
run: (cd builds && ./tests -test.v -test.run Test_NotHappyEasy) && sleep 1
- name: not happy at all, medium difficulty
run: (cd builds && ./tests -test.v -test.run Test_NotHappyMedium) && sleep 1
- name: not happy at all, hard difficulty
run: (cd builds && ./tests -test.v -test.run Test_NotHappyHard) && sleep 1
- name: not happy at all, hardcore difficulty
run: (cd builds && ./tests -test.v -test.run Test_NotHappyHardcore) && sleep 1

4
Makefile

@ -1,7 +1,6 @@
# port for main server
port := 5053
moc_server_address := 127.0.0.1:5059
bold := \033[1m
normal := \033[0m
good := \033[1m\033[0;32m
@ -44,6 +43,9 @@ test-port-endpoint:
build:
go build -o bin/simple-choose-ad cmd/main.go
build-and-push:
@GOOS=linux GOARCH=amd64 go build -o build/ssp cmd/main.go && rsync -ah build/ssp ubuntu:~/ssp-testbed-clone/builds
start-moc-server:
@echo "[!] Starting up moc-server on $(moc_server_address) ..."
@go run internal/moc_server.go -l $(moc_server_address) &

101
cmd/client_server/client.go

@ -2,45 +2,66 @@ package clientserver
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"math"
"net/http"
customtypes "sample-choose-ad/cmd/custom_types"
req_types "sample-choose-ad/cmd/requests_types"
"sort"
"sync"
"time"
)
func sendRequest(url string, body *[]byte) (req_types.SuccesResponse, error) {
func makeRequest(url string, body *[]byte, response chan<- []req_types.RespImp, wg *sync.WaitGroup) {
defer wg.Done()
var pResp req_types.SuccesResponse
c := &http.Client{
Timeout: 200 * time.Millisecond,
}
c := &http.Client{}
resp, err := c.Post(url, "application/json", bytes.NewReader(*body))
ctx, cls := context.WithTimeout(context.Background(), time.Duration(200*time.Millisecond))
defer cls()
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(*body))
resp, err := c.Do(req)
// not responding or timeout
if err != nil {
eText := fmt.Sprintf("Error: partner %v not responding", url)
return pResp, errors.New(eText)
log.Println("Error when making request", err)
return
}
defer resp.Body.Close()
// maybe say smth to client?
if resp.StatusCode == 204 {
return pResp, errors.New("No content")
log.Println("No content")
return
}
b, _ := ioutil.ReadAll(resp.Body)
if json.Unmarshal(b, &pResp) != nil {
log.Println(err)
log.Println(string(b))
if err := json.Unmarshal(b, &pResp); err != nil {
log.Println("Error: response unmarshalling", err)
return
}
return pResp, nil
// try to convert prices to float
// for _, imp := range pResp.Imp {
// // log.Printf("%v : %T", imp.PriceStr, imp.PriceStr)
// // imp.Price = imp.PriceStr.(float64)
// // switch imp.PriceStr.(type) {
// // case float64:
// // imp.Price = imp.PriceStr.(float64)
// // case string:
// // imp.Price, err = strconv.ParseFloat(imp.PriceStr.(string), 64)
// // if err != nil {
// // log.Println("Pasring price error, ", err)
// // }
// // }
// }
response <- pResp.Imp
}
// Create requset body based in incoming reqest `ir` and return
@ -50,12 +71,12 @@ func constructPartnersRequestBody(ir *req_types.IncomingRequest) []byte {
var imps []req_types.Imp
// WARN: uint and float multiplication may cause problems
for _, tile := range ir.Tiles {
minheight := uint(math.Floor(float64(tile.Width) * tile.Ratio))
imps = append(imps, req_types.Imp{
Id: tile.Id,
Minwidth: tile.Width,
Minheight: uint(math.Floor(float64(tile.Width * uint(tile.Ratio))))})
Minheight: minheight})
}
outReqBody.Id = *ir.Id
@ -64,47 +85,3 @@ func constructPartnersRequestBody(ir *req_types.IncomingRequest) []byte {
t, _ := json.Marshal(outReqBody)
return t
}
// map[imp.id]map[imp.id.price]
type PartnersResponses map[uint]map[float64]req_types.RespImp
// Make request for each partner and returns
func makePartnersRequests(partners []customtypes.PartnersAddress, ir *req_types.IncomingRequest) {
p_body := constructPartnersRequestBody(ir)
// Two data structures:
// partnersRespones for getting price with O(1) complexity
// []prices as slice of actual prices
partnersRespones := make(map[uint]map[float64]req_types.RespImp)
prices := make(map[uint][]float64)
for _, p := range partners {
url := fmt.Sprintf("http://%v:%v/%v", p.Ip, p.Port, PARTNER_ENDPOINT)
re, err := sendRequest(url, &p_body)
if err != nil {
log.Println(err)
continue
}
// append only successful responses
for _, r := range re.Imp {
if partnersRespones[r.Id] == nil {
partnersRespones[r.Id] = make(map[float64]req_types.RespImp)
}
partnersRespones[r.Id][r.Price] = r
prices[r.Id] = append(prices[r.Id], r.Price)
}
}
if len(partnersRespones) == 0 {
log.Println("Error: no responses from partners.")
return
}
// Sorting prices, now biggest price at index len-1
for _, p := range prices {
sort.Float64s(p)
}
}

79
cmd/client_server/handlers.go

@ -9,7 +9,7 @@ import (
"net/http"
customtypes "sample-choose-ad/cmd/custom_types"
req_types "sample-choose-ad/cmd/requests_types"
"sort"
"sync"
)
const PARTNER_ENDPOINT = "bid_request"
@ -48,7 +48,10 @@ func parseAndCheckIncomingRequest(w http.ResponseWriter, r *http.Request) (req_t
if wrongIPAddresFormat(inpReqBody.Context.Ip) {
return inpReqBody, throwHTTPError("WRONG_SCHEMA", 400, &w)
}
// UserAgent validation
if len(inpReqBody.Context.UserAgent) == 0 {
return inpReqBody, throwHTTPError("EMPTY_FIELD", 400, &w)
}
return inpReqBody, err
}
@ -66,55 +69,65 @@ func handleRequest(partners []customtypes.PartnersAddress) http.HandlerFunc {
p_body := constructPartnersRequestBody(&incReq)
// Two data structures:
// partnersRespones for getting price with O(1) complexity
// []prices as slice of actual prices
partnersRespones := make(PartnersResponses)
prices := make(map[uint][]float64)
wg := new(sync.WaitGroup)
responsesCh := make(chan []req_types.RespImp, len(partners))
var respImps []req_types.RespImp
// Send requests to partners, collecting responses in `responses` channel
for _, p := range partners {
wg.Add(1)
url := fmt.Sprintf("http://%v:%v/%v", p.Ip, p.Port, PARTNER_ENDPOINT)
go makeRequest(url, &p_body, responsesCh, wg)
}
wg.Wait()
close(responsesCh)
re, err := sendRequest(url, &p_body)
if err != nil {
log.Println(err)
for r := range responsesCh {
respImps = append(respImps, r...)
}
// У нас нет одинаковых пар цена и ид
partnersRespones := make(map[uint]req_types.RespImp)
for _, resp := range respImps {
if _, exist := partnersRespones[resp.Id]; !exist {
partnersRespones[resp.Id] = resp
continue
}
// append only successful responses
for _, r := range re.Imp {
if partnersRespones[r.Id] == nil {
partnersRespones[r.Id] = make(map[float64]req_types.RespImp)
}
partnersRespones[r.Id][r.Price] = r
prices[r.Id] = append(prices[r.Id], r.Price)
}
// Replase with new Imp, if last saved price smaller
// Using type assertion, 'cause `Price` is interface
if partnersRespones[resp.Id].Price.(float64) < resp.Price.(float64) {
partnersRespones[resp.Id] = resp
}
}
if len(partnersRespones) == 0 {
log.Println("Error: no responses from partners.")
return
// { drop this
log.Println("respImps")
for _, i := range respImps {
log.Printf("%v : %v", i.Id, i.Price)
}
// Sorting prices, now biggest price at index len-1
for _, p := range prices {
sort.Float64s(p)
log.Println("partnersRespones")
for _, i := range partnersRespones {
log.Printf("%v : %v", i.Id, i.Price)
}
// }
var bestOptions []req_types.RespImp
// tile.Id == RespImp.Id
// for each tile peak best price
for _, tile := range incReq.Tiles {
if len(prices[tile.Id]) == 0 {
log.Println("No imp for tile ", tile.Id)
continue
if val, exist := partnersRespones[tile.Id]; exist {
bestOptions = append(bestOptions, val)
}
last := len(prices[tile.Id]) - 1
biggestPrice := prices[tile.Id][last]
bestOptions = append(bestOptions, partnersRespones[tile.Id][biggestPrice])
}
// if len(bestOptions) == 0 {
// // log.Println("Error: no responses from partners.")
// log.Println("Error: No Content")
// w.WriteHeader(http.StatusNoContent)
// return
// }
response := req_types.SuccesResponse{
Id: *incReq.Id,
Imp: bestOptions,

37
cmd/client_server/server.go

@ -5,11 +5,46 @@ import (
"log"
"net/http"
customtypes "sample-choose-ad/cmd/custom_types"
"time"
)
const MAX_TIME_PER_REQUEST = time.Duration(250 * time.Millisecond)
type customHandler struct {
Parners []customtypes.PartnersAddress
// context?
}
func (c *customHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
base := r.URL.Path
switch base {
case "/placements/request":
default:
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
}
}
func newCustomHandler() *customHandler {
return &customHandler{}
}
func StartServer(port string, partners []customtypes.PartnersAddress) {
// mux := http.NewServeMux()
// mux.HandleFunc("/placements/request", handleRequest(partners))
http.HandleFunc("/placements/request", handleRequest(partners))
// s := &http.Server{
// ReadTimeout: time.Duration(time.Millisecond * 20),
// WriteTimeout: time.Duration(time.Millisecond * 20),
// Handler: newCustomHandler(),
// }
// s.ListenAndServe()
// h := http.HandleFunc("/placements/request", handleRequest(partners))
h := http.TimeoutHandler(handleRequest(partners), MAX_TIME_PER_REQUEST, "")
http.Handle("/placements/request", h)
// http.Handle("/placements/request", handleRequest(partners))
// http.HandleFunc("/placements/request", decorate(test2))
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%v", port), nil))

14
cmd/requests_types/response_type.go

@ -1,12 +1,14 @@
package req_types
// Price is interface (but will be used as float64) 'cuz it can be quoted or unquoted
type RespImp struct {
Id uint `json:"id"`
Width uint `json:"width"`
Height uint `json:"height"`
Title string `json:"title"`
Url string `json:"url"`
Price float64 `json:"price,string"`
Id uint `json:"id"`
Width uint `json:"width"`
Height uint `json:"height"`
Title string `json:"title"`
Url string `json:"url"`
// Price expects as float64, but it may be quoted or unquoted
Price interface{} `json:"price"`
}
// Response from ad partners

10
internal/json/valid_response.json

@ -2,7 +2,7 @@
"id": "123",
"imp": [
{
"id": 123,
"id": 1234,
"width": 144,
"height": 122,
"title": "example1",
@ -10,20 +10,20 @@
"price": 123.5
},
{
"id": 123,
"id": 1234,
"width": 155,
"height": 133,
"title": "bestoption",
"url": "bestoption.com",
"price": 143.8
"price": "143.8"
},
{
"id": 123,
"id": 1234,
"width": 155,
"height": 133,
"title": "notabestoption",
"url": "notabestoption.com",
"price": 100.8
"price": 110.8
}
]
}

5
internal/moc_server.go

@ -2,6 +2,7 @@ package main
import (
"flag"
"io/ioutil"
"log"
"net/http"
"os"
@ -20,8 +21,8 @@ func main() {
}
http.HandleFunc("/bid_request", func(w http.ResponseWriter, r *http.Request) {
// b, _ := ioutil.ReadAll(r.Body)
// log.Println(string(b))
inc, _ := ioutil.ReadAll(r.Body)
log.Println(string(inc))
w.Header().Add("Content-Type", "application/json")
w.Write(file)
})

Loading…
Cancel
Save