diff --git a/doorman.go b/doorman.go index b3a241c..2bd178a 100644 --- a/doorman.go +++ b/doorman.go @@ -3,7 +3,6 @@ package doorman import ( "encoding/json" "errors" - "fmt" "log" "math" "math/big" @@ -17,8 +16,6 @@ import ( "gopkg.in/mgo.v2/bson" ) -var _ = fmt.Print - var ONE *big.Rat = big.NewRat(1, 1) var rand *mathrand.Rand @@ -43,7 +40,7 @@ type Doorman struct { wg sync.WaitGroup // waitgroup for goroutine safety } -func NewDoorman(id bson.ObjectId, Probabilities []*big.Rat) *Doorman { +func New(id bson.ObjectId, Probabilities []*big.Rat) *Doorman { wab := &Doorman{} wab.Id = id wab.Probabilities = Probabilities @@ -143,6 +140,10 @@ func (w *Doorman) GetCaseFromData(data ...[]byte) uint { return w.GetCase(random) } +func (w *Doorman) GetCaseFromString(data string) uint { + return w.GetCaseFromData([]byte(data)) +} + func (w *Doorman) GetRandomCase() uint { r := rand.Float64() return w.GetCase(new(big.Rat).SetFloat64(r)) diff --git a/doorman_test.go b/doorman_test.go index 4512b6c..5061d41 100644 --- a/doorman_test.go +++ b/doorman_test.go @@ -18,22 +18,16 @@ import ( var epsilon = 0.0 var ZERO = big.NewRat(0, 1) -func assertIsAbout(t *testing.T, expected, received *big.Rat) { +func assertIsEqual(t *testing.T, expected, received *big.Rat) { if expected.Cmp(received) != 0 { t.Error("received", received, "but expected", expected) } } -func assertIsNotAbout(t *testing.T, expected, received *big.Rat) { - if expected.Cmp(received) != 0 { - t.Error("received", received, "but expected something different than", expected) - } -} - func getProbs(probs ...float64) []*big.Rat { ret := make([]*big.Rat, len(probs)) for i, p := range probs { - ret[i], _ = new(big.Rat).SetString(fmt.Sprint(p)) + ret[i] = new(big.Rat).SetFloat64(p) } return ret } @@ -72,7 +66,7 @@ func TestValidate(t *testing.T) { } func TestGetCase(t *testing.T) { - w := NewDoorman(bson.NewObjectId(), getProbs(0.25, 0.5, 0.25)) + w := New(bson.NewObjectId(), getProbs(0.25, 0.5, 0.25)) if c := w.GetCase(ZERO); c != 0 { t.Error("expected 0 but received", c) } @@ -83,7 +77,7 @@ func TestGetCase(t *testing.T) { } func TestGetCaseCoroutineSafety(t *testing.T) { - w := NewDoorman(bson.NewObjectId(), getProbs(0.25, 0.5, 0.25)) + w := New(bson.NewObjectId(), getProbs(0.25, 0.5, 0.25)) i := 0 w.wg.Add(1) go func() { @@ -98,11 +92,11 @@ func TestGetCaseCoroutineSafety(t *testing.T) { func TestGenerateRandomProbabilityFromBitSlice(t *testing.T) { w := new(Doorman) - assertIsAbout(t, big.NewRat(1, 2), w.GenerateRandomProbabilityFromInteger(1)) + assertIsEqual(t, big.NewRat(1, 2), w.GenerateRandomProbabilityFromInteger(1)) - // assertIsAbout(t, 0.5+0*0.25+1*0.125, w.GenerateRandomProbabilityFromInteger(5)) + assertIsEqual(t, new(big.Rat).SetFloat64(0.5+0*0.25+1*0.125), w.GenerateRandomProbabilityFromInteger(5)) - assertIsAbout(t, ONE, w.GenerateRandomProbabilityFromInteger(math.MaxUint64)) + assertIsEqual(t, ONE, w.GenerateRandomProbabilityFromInteger(math.MaxUint64)) } func TestHash(t *testing.T) { @@ -166,7 +160,7 @@ func randomBytes(n int) []byte { func TestUpdateHard(t *testing.T) { - wab := NewDoorman(bson.ObjectIdHex(oid), getProbs()) + wab := New(bson.ObjectIdHex(oid), getProbs()) createMessage := func() string { msg := &shared.DoormanUpdater{Timestamp: 2, Probabilities: getProbs(0.5, 0.25, 0.25), Id: oid} if ret, err := json.Marshal(msg); err != nil { @@ -209,7 +203,7 @@ func IsExtremeBinomialResult(x int, n, p float64) error { func TestGetRandomCase(t *testing.T) { p := 0.5 n := 10000 - wab := NewDoorman(bson.NewObjectId(), getProbs(p, 1-p)) + wab := New(bson.NewObjectId(), getProbs(p, 1-p)) var sum = 0 for i := 0; i < n; i++ { sum += int(wab.GetRandomCase()) @@ -220,9 +214,22 @@ func TestGetRandomCase(t *testing.T) { } } +func TestGetCaseFromString(t *testing.T) { + w := New(bson.NewObjectId(), getProbs(0.25, 0.5, 0.25)) + expts := map[string]uint{ + "j'aime la ratoutille": 2, + } + for data, expt := range expts { + if res := w.GetCaseFromString(data); res != expt { + t.Error("incorrect results for: ", data, res) + } + } + +} + func BenchmarkGetCaseFromData(b *testing.B) { var data = randomBytes(1024 * 1024) - w := NewDoorman(bson.NewObjectId(), getProbs(0.1, 0.4, 0.4, 0.05, 0.05)) + w := New(bson.NewObjectId(), getProbs(0.1, 0.4, 0.4, 0.05, 0.05)) for i := 0; i < b.N; i++ { w.GetCaseFromData(data) } diff --git a/example/integration_test.go b/example/integration_test.go deleted file mode 100644 index ed4ea2d..0000000 --- a/example/integration_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package main - -import ( - "testing" - - "github.com/didiercrunch/doorman" - "gopkg.in/mgo.v2/bson" -) - -func TestDoormanDistribution(t *testing.T) { - wab = doorman.NewDoorman(bson.NewObjectId(), []float64{0.25, 0.5, 0.10, 0.05, 0.1}) - n := 10000 - cases := make([]float64, wab.Length()) - for i := 0; i <= n; i++ { - cases[wab.GetRandomCase()] += 1 - } - expt := make([]float64, wab.Length()) - for i, p := range wab.Probabilities { - expt[i] = p * float64(n) - } - // Todo: use chi square test to assert the results are not absurde - //if err := AssertTwoDistributionAreProbabliNotDifferent(expt, cases, 0.001); err != nil { - // t.Error(err) - //} - -} diff --git a/example/main.go b/example/main.go index 17b2365..b42f1b5 100644 --- a/example/main.go +++ b/example/main.go @@ -3,17 +3,28 @@ package main import ( "html/template" "log" + "math/big" + "math/rand" "net/http" "os" + "strconv" "github.com/didiercrunch/doorman" "gopkg.in/mgo.v2/bson" ) -var wab = doorman.NewDoorman(bson.ObjectIdHex("55048bfff6fb5e213d000001"), []float64{0.25, 0.5, 0.10, 0.05, 0.1}) +func getProbs(probs ...float64) []*big.Rat { + ret := make([]*big.Rat, len(probs)) + for i, p := range probs { + ret[i] = new(big.Rat).SetFloat64(p) + } + return ret +} + +var wab = doorman.New(bson.ObjectIdHex("558701a2f6fb5e29a4000003"), getProbs(1, 0, 0)) func init() { - if err := wab.NanoMsgSubscriber("tcp://127.0.0.1:40899"); err != nil { + if err := wab.Subscriber("http://localhost:1999"); err != nil { panic(err) } } @@ -21,15 +32,11 @@ func init() { func GetDoormanValue(r *http.Request) (string, int) { switch wab.GetCaseFromData([]byte(r.URL.String())) { case 0: - return "blue", 0 + return "red", 0 case 1: - return "red", 1 + return "green", 1 case 2: - return "pink", 2 - case 3: - return "yellow", 3 - case 4: - return "magenta", 4 + return "blue", 2 } log.Panic("impossible value returned by doorman") return "", 0 @@ -43,8 +50,10 @@ var tmpl = `

the doorman value is {{ .value }}

this doorman depend solely on the url. If you hit different url you - will have different doorman results. + will have different doorman results. For example, here is a random + url: {{ .nextUrl }}

+ ` @@ -56,6 +65,7 @@ func Handler(w http.ResponseWriter, r *http.Request) { } m := make(map[string]interface{}) m["color"], m["value"] = GetDoormanValue(r) + m["nextUrl"] = "/" + strconv.Itoa(rand.Int()) if err := t.Execute(w, m); err != nil { panic(err) } diff --git a/httpsubscriber/httpsubscriber.go b/httpsubscriber/httpsubscriber.go new file mode 100644 index 0000000..eaeb15c --- /dev/null +++ b/httpsubscriber/httpsubscriber.go @@ -0,0 +1,54 @@ +package httpsubscriber + +import ( + "encoding/json" + "errors" + "log" + "net/http" + "time" + + "github.com/didiercrunch/doorman/shared" + "gopkg.in/mgo.v2/bson" +) + +type HttpSubscriber struct { + Url string + HartBeat time.Duration +} + +func (s *HttpSubscriber) GetDoormanUpdater() (*shared.DoormanUpdater, error) { + resp, err := http.Get(s.Url) + if err != nil { + return nil, err + } else if resp.StatusCode != 200 { + return nil, errors.New("bad http status when GETting update, " + resp.Status) + } + defer resp.Body.Close() + de := json.NewDecoder(resp.Body) + ret := new(shared.DoormanUpdater) + err = de.Decode(ret) + return ret, err +} + +func (s *HttpSubscriber) callUpdateHandlerFunction(f shared.UpdateHandlerFunc, data []byte) error { + wu := &shared.DoormanUpdater{} + if err := json.Unmarshal(data, wu); err != nil { + return err + } else { + return f(wu) + } +} + +func (s *HttpSubscriber) Subscribe(abtestId bson.ObjectId, update shared.UpdateHandlerFunc) error { + go func() { + c := time.Tick(s.HartBeat) + for _ = range c { + if du, err := s.GetDoormanUpdater(); err != nil { + log.Printf("error retrieving doorman %v \n%v\n", abtestId, err) + } else { + update(du) + } + } + }() + return nil +} diff --git a/httpsubscriber/httpsubscriber_test.go b/httpsubscriber/httpsubscriber_test.go new file mode 100644 index 0000000..29254c6 --- /dev/null +++ b/httpsubscriber/httpsubscriber_test.go @@ -0,0 +1,56 @@ +package httpsubscriber + +import ( + "encoding/json" + "math/big" + "net/http" + "net/http/httptest" + "testing" + + "github.com/didiercrunch/doorman/shared" +) + +func TestCallUpdateHandlerFunction(t *testing.T) { + f := func(m *shared.DoormanUpdater) error { + if m.Id != "foo" { + t.Error("bad doorman id", m.Id) + } + return nil + } + + data, err := json.Marshal(&shared.DoormanUpdater{Id: "foo"}) + if err != nil { + t.Error(err) + return + } + + if new(HttpSubscriber).callUpdateHandlerFunction(f, data); err != nil { + t.Error(err) + } +} + +func MockEndpoint(w http.ResponseWriter, r *http.Request) { + du := &shared.DoormanUpdater{ + "b64", + 897987, + []*big.Rat{big.NewRat(1, 4), big.NewRat(3, 4)}, + } + enc := json.NewEncoder(w) + if err := enc.Encode(du); err != nil { + panic(err) + } + +} + +func TestGetDoormanUpdater(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(MockEndpoint)) + defer ts.Close() + + s := HttpSubscriber{Url: ts.URL} + if d, err := s.GetDoormanUpdater(); err != nil { + t.Error(err) + } else if d.Id != "b64" { + t.Error() + } + +} diff --git a/subscribe.go b/subscribe.go index 9b031e8..3263b05 100644 --- a/subscribe.go +++ b/subscribe.go @@ -4,6 +4,7 @@ import ( "github.com/didiercrunch/doorman/nanomsgsubscriber" "github.com/didiercrunch/doorman/nsqsubscriber" "github.com/didiercrunch/doorman/shared" + "github.com/didiercrunch/doorman/subscriber" "gopkg.in/mgo.v2/bson" ) @@ -11,16 +12,21 @@ type Subscriber interface { Subscribe(doormanId bson.ObjectId, update shared.UpdateHandlerFunc) error } -func (w *Doorman) Subscribe(sub Subscriber) error { +func (w *Doorman) subscribe(sub Subscriber) error { return sub.Subscribe(w.Id, w.Update) } func (w *Doorman) NSQSubscriber(NSQLookupdURl string) error { sub := &nsqsubscriber.NSQSubscriber{NSQLookupdURl} - return w.Subscribe(sub) + return w.subscribe(sub) } func (w *Doorman) NanoMsgSubscriber(NanoMsgUrlLookupdURl string) error { sub := &nanomsgsubscriber.NanoMsgSubscriber{NanoMsgUrlLookupdURl} - return w.Subscribe(sub) + return w.subscribe(sub) +} + +func (w *Doorman) Subscriber(serverUrl string) error { + sub := &subscriber.Subscriber{serverUrl} + return w.subscribe(sub) } diff --git a/subscriber/subscriber.go b/subscriber/subscriber.go new file mode 100644 index 0000000..1524c33 --- /dev/null +++ b/subscriber/subscriber.go @@ -0,0 +1,85 @@ +package subscriber + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/didiercrunch/doorman/httpsubscriber" + "github.com/didiercrunch/doorman/nanomsgsubscriber" + "github.com/didiercrunch/doorman/shared" + "gopkg.in/mgo.v2/bson" + "net/http" + "time" +) + +type Subscriber struct { + URL string +} + +type subscriber interface { + Subscribe(abtestId bson.ObjectId, update shared.UpdateHandlerFunc) error +} + +type ServerSpecification struct { + HostName string `json:"hostname"` + Port int `json:"port"` + MessageQueue string `json:"message_queue"` + NanoMsg map[string]string `json:"nano_msg"` +} + +func (s *Subscriber) getServerSpecification() (*ServerSpecification, error) { + url := s.URL + "/api/server" + fmt.Println(">>>", url) + resp, err := http.Get(url) + if err != nil { + return nil, err + } + if resp.StatusCode != 200 { + return nil, errors.New(resp.Status) + } + defer resp.Body.Close() + ret := new(ServerSpecification) + decoder := json.NewDecoder(resp.Body) + err = decoder.Decode(ret) + fmt.Println("ok!") + return ret, err +} + +func (sub *Subscriber) GetSubsciber(serverSpec *ServerSpecification, doormanId bson.ObjectId) subscriber { + switch serverSpec.MessageQueue { + case "nanomsg": + return &nanomsgsubscriber.NanoMsgSubscriber{serverSpec.NanoMsg["url"]} + } + fmt.Println("shit!", serverSpec) + return &httpsubscriber.HttpSubscriber{sub.getDoormanStatusUrl(doormanId), time.Second * 5} +} + +func (sub *Subscriber) getDoormanStatusUrl(doormanId bson.ObjectId) string { + return sub.URL + "/api/doormen/" + doormanId.Hex() + "/status" +} + +func (sub *Subscriber) SetInitialState(doormanId bson.ObjectId, update shared.UpdateHandlerFunc) error { + httpSUbscriber := &httpsubscriber.HttpSubscriber{Url: sub.getDoormanStatusUrl(doormanId)} + if du, err := httpSUbscriber.GetDoormanUpdater(); err != nil { + return err + } else { + return update(du) + } + +} + +func startHartBeat() error { + return nil +} + +func (sub *Subscriber) Subscribe(doormanId bson.ObjectId, update shared.UpdateHandlerFunc) error { + spec, err := sub.getServerSpecification() + if err != nil { + return err + } + if err = sub.SetInitialState(doormanId, update); err != nil { + return err + } + subscriber := sub.GetSubsciber(spec, doormanId) + return subscriber.Subscribe(doormanId, update) +} diff --git a/subscriber/subscriber_test.go b/subscriber/subscriber_test.go new file mode 100644 index 0000000..002f978 --- /dev/null +++ b/subscriber/subscriber_test.go @@ -0,0 +1 @@ +package subscriber