Skip to content

Instantly share code, notes, and snippets.

@kpacha
Last active July 17, 2018 16:15
Show Gist options
  • Save kpacha/7b7d429acda0ff0bce00af46fe06d2e5 to your computer and use it in GitHub Desktop.
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
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()
}
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