Last active
July 17, 2018 16:15
-
-
Save kpacha/7b7d429acda0ff0bce00af46fe06d2e5 to your computer and use it in GitHub Desktop.
Serve all the requests with a given distribution of response times and status codes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"flag" | |
"math/rand" | |
"net/http" | |
"sync" | |
"time" | |
"github.com/gin-gonic/gin" | |
) | |
func main() { | |
addr := flag.String("addr", ":8080", "address to bind the service") | |
flag.Parse() | |
engine := New(service{ | |
ranges: []Range{ | |
{ | |
weight: .8, | |
min: 10 * time.Millisecond, | |
max: 20 * time.Millisecond, | |
}, | |
{ | |
weight: .2, | |
min: 700 * time.Millisecond, | |
max: 800 * time.Millisecond, | |
}, | |
}, | |
mu: new(sync.RWMutex), | |
}) | |
engine.Run(*addr) | |
} | |
func New(s service) *gin.Engine { | |
engine := gin.New() | |
engine.Use(gin.Recovery()) | |
engine.GET("/service", func(c *gin.Context) { | |
c.JSON(s.Consume(), gin.H{"message": "OK"}) | |
}) | |
engine.PUT("/service", func(c *gin.Context) { | |
request := []SerializableRange{} | |
if err := c.BindJSON(&request); err != nil { | |
abort(c, err) | |
return | |
} | |
definition, err := parseRange(request) | |
if err != nil { | |
abort(c, err) | |
return | |
} | |
s.Update(definition) | |
c.JSON(http.StatusOK, gin.H{"message": "Updated"}) | |
}) | |
return engine | |
} | |
func abort(c *gin.Context, err error) { | |
c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()}) | |
c.Abort() | |
} | |
type SerializableRange struct { | |
Weight float64 `json:"weight"` | |
Min string `json:"min"` | |
Max string `json:"max"` | |
StatusCode int `json:"status_code"` | |
} | |
type Range struct { | |
weight float64 | |
min time.Duration | |
max time.Duration | |
sc int | |
} | |
func parseRange(in []SerializableRange) ([]Range, error) { | |
out := make([]Range, len(in)) | |
for k, v := range in { | |
min, err := time.ParseDuration(v.Min) | |
if err != nil { | |
return out, err | |
} | |
max, err := time.ParseDuration(v.Max) | |
if err != nil { | |
return out, err | |
} | |
sc := v.StatusCode | |
if sc == 0 { | |
sc = http.StatusOK | |
} | |
out[k] = Range{ | |
weight: v.Weight, | |
min: min, | |
max: max, | |
sc: sc, | |
} | |
} | |
return out, nil | |
} | |
type service struct { | |
ranges []Range | |
mu *sync.RWMutex | |
} | |
func (s *service) Consume() int { | |
s.mu.RLock() | |
rs := s.ranges | |
s.mu.RUnlock() | |
current := rand.Float64() | |
accumulator := 0.0 | |
for _, r := range rs { | |
if accumulator+r.weight >= current { | |
time.Sleep(time.Duration(rand.Int63n(int64(r.max-r.min))) + r.min) | |
return r.sc | |
} | |
accumulator += r.weight | |
} | |
return 0 | |
} | |
func (s *service) Update(ranges []Range) { | |
s.mu.RLock() | |
s.ranges = ranges | |
s.mu.RUnlock() | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"bytes" | |
"encoding/json" | |
"log" | |
"net/http" | |
"net/http/httptest" | |
"sync" | |
"testing" | |
"time" | |
) | |
func Test_parseRange(t *testing.T) { | |
buf := bytes.NewBufferString(`[{"weight":0.1,"min":"1ms","max":"3ms","status_code":418},{"weight":0.7,"min":"3ms","max":"100ms"},{"weight":0.2,"min":"150ms","max":"1s"}]`) | |
in := []SerializableRange{} | |
if err := json.Unmarshal(buf.Bytes(), &in); err != nil { | |
t.Error(err) | |
return | |
} | |
log.Printf("%+v", in) | |
out, err := parseRange(in) | |
if err != nil { | |
t.Error(err) | |
return | |
} | |
log.Printf("%+v", out) | |
} | |
func TestNew(t *testing.T) { | |
engine := New(service{ | |
ranges: []Range{ | |
{ | |
weight: .8, | |
min: 10 * time.Millisecond, | |
max: 20 * time.Millisecond, | |
sc: http.StatusOK, | |
}, | |
{ | |
weight: .2, | |
min: 700 * time.Millisecond, | |
max: 800 * time.Millisecond, | |
sc: http.StatusOK, | |
}, | |
}, | |
mu: new(sync.RWMutex), | |
}) | |
buf := bytes.NewBufferString(`[{"weight":0.1,"min":"1ms","max":"3ms"},{"weight":0.7,"min":"3ms","max":"100ms"},{"weight":0.2,"min":"150ms","max":"1s"}]`) | |
req, _ := http.NewRequest("PUT", "/service", buf) | |
w := httptest.NewRecorder() | |
engine.ServeHTTP(w, req) | |
if w.Result().StatusCode != http.StatusOK { | |
t.Errorf("unexpected status code: %d", w.Result().StatusCode) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment