From dbbfaf99b6ff356e8b1409554c3ec0dbb4e31d84 Mon Sep 17 00:00:00 2001 From: didier amyot Date: Sat, 31 Jan 2015 10:19:31 -0800 Subject: [PATCH] first commit. way too late. --- .gitignore | 27 +++ .travis.yml | 6 + LICENSE | 21 ++ README.md | 36 ++++ doorman.go | 153 ++++++++++++++ doorman_test.go | 215 ++++++++++++++++++++ example/integration_test.go | 26 +++ example/main.go | 78 +++++++ nanomsgsubscriber/nanomsgsubscriber.go | 59 ++++++ nanomsgsubscriber/nanomsgsubscriber_test.go | 27 +++ nsqsubscriber/messages.go | 15 ++ nsqsubscriber/nsqsubscriber.go | 40 ++++ nsqsubscriber/nsqsubscriber_test.go | 19 ++ shared/interfaces.go | 9 + subscribe.go | 26 +++ 15 files changed, 757 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 doorman.go create mode 100644 doorman_test.go create mode 100644 example/integration_test.go create mode 100644 example/main.go create mode 100644 nanomsgsubscriber/nanomsgsubscriber.go create mode 100644 nanomsgsubscriber/nanomsgsubscriber_test.go create mode 100644 nsqsubscriber/messages.go create mode 100644 nsqsubscriber/nsqsubscriber.go create mode 100644 nsqsubscriber/nsqsubscriber_test.go create mode 100644 shared/interfaces.go create mode 100644 subscribe.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..68e50a8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ + + +*cover.out +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.directory diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..245a2f5 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,6 @@ +language: go + +go: + - 1.3 + - 1.4 + - tip diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2627664 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 didier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9401986 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +# Doorman [![Build Status](https://travis-ci.org/didiercrunch/doorman.svg)](https://travis-ci.org/didiercrunch/doorman) + +doorman client. For the doorman server see [doorman server](https://github.com/didiercrunch/doorman server) + + +## what it is? + +This library can be use in two ways; A/B testing and feature gating. + +### A/B testing + +The best way to explain *A/B testing* is via an example. Imagine that you have a button in you blog +and that you want to set its colour to maximaze the number of viewers that click on it. For simplicity, +let's assume the colour can be red or magenta. + +*A/B* testing allows you to randomly select a colour for the button depending on your viewers. For example, +you could deceide to show to 10% of your users the magenta button while 90% of your user will see the button +red. + +Then, you can collect data on your user choice (feature not include in this library) and make a data supported +decision to improved you blog. + + +### Feature gating + +The best way to explain *feature gating* is to give an example. Imaging that you are using *company A* to +manage the discussion section of your blog and that you want to switch to *company B*. You would like to +make the move but you are scared that the switch will cause failure in unexpected part of your blog. + +What you can do is to change graduly from *company A* to *company B*. At first, you set 1% of your +viewer to see *company A*, if no issue occure, you move to 5% then to 10% and up to 100%. + +## example + +There is an example at [example/main.go](https://github.com/didiercrunch/doorman/blob/master/example/main.go) that +change randomly the colour of the background by the url path. diff --git a/doorman.go b/doorman.go new file mode 100644 index 0000000..b44ffc9 --- /dev/null +++ b/doorman.go @@ -0,0 +1,153 @@ +package doorman + +import ( + "encoding/json" + "errors" + "log" + "math" + mathrand "math/rand" + "net/http" + "sync" + "time" + + "github.com/dchest/siphash" + "github.com/didiercrunch/doorman/shared" + "gopkg.in/mgo.v2/bson" +) + +// the smallest value that makes two floats the same. Consequently x == y +// if |x - y| < epsilon. It is a known issue that one can find three numbers +// a,b,c such that a == b, b == c and a != c. Hence, it is the programmer role +// to be very careful about equalities and inequalities. +const epsilon float64 = 0.000000001 + +var rand *mathrand.Rand + +func initRandomSeed() { + kindOfRandomSeed := mathrand.NewSource(time.Now().Unix()) + rand = mathrand.New(kindOfRandomSeed) +} + +func init() { + initRandomSeed() +} + +func IsZero(f float64) bool { + if math.Abs(f) > epsilon { + return false + } + return true +} + +func IsEqual(f1, f2 float64) bool { + return IsZero(f1 - f2) +} + +type Doorman struct { + Id bson.ObjectId // the id of the doorman + LastChangeTimestamp int64 // an always increasing int that represent the last time the doorman has beed updated + Probabilities []float64 // The probability of each cases. The sum of probabilities needs to be one + wg sync.WaitGroup // waitgroup for goroutine safety +} + +func NewDoorman(id bson.ObjectId, Probabilities []float64) *Doorman { + wab := &Doorman{} + wab.Id = id + wab.Probabilities = Probabilities + return wab +} + +func (w *Doorman) Length() int { + return len(w.Probabilities) +} + +func (w *Doorman) UpdateHard(baseURL string) error { + r, err := http.Get(baseURL + "/" + w.Id.Hex()) + if err != nil { + return err + } + message := new(shared.DoormanUpdater) + d := json.NewDecoder(r.Body) + if err := d.Decode(message); err != nil { + return err + } else { + return w.Update(message) + } +} + +func (w *Doorman) Update(wu *shared.DoormanUpdater) error { + if wu.Timestamp <= w.LastChangeTimestamp { + return nil + } + if wu.Id != w.Id.Hex() { + return errors.New("bad doorman id") + } + w.wg.Add(1) + defer w.wg.Done() + w.LastChangeTimestamp = wu.Timestamp + + if !IsEqual(w.sum(wu.Probabilities), 1) { + return errors.New("the sum of probabilities cannot be different than 1") + } + w.Probabilities = wu.Probabilities + log.Printf("Updated doorman %v with new probabilities %v with timestamp %v", wu.Id, wu.Probabilities, wu.Timestamp) + return nil +} + +func (w *Doorman) sum(prob []float64) float64 { + var ret float64 = 0 + for _, p := range prob { + ret += p + } + return ret +} + +func (w *Doorman) Validate() error { + if len(w.Probabilities) == 0 { + return errors.New("not initiated") + } + if !IsZero(w.sum(w.Probabilities) - 1.0) { + return errors.New("The sum of probabilities is not one") + } + return nil +} + +func (w *Doorman) GetCase(choosenRandomPosition float64) uint { + w.wg.Wait() + var prob float64 = 0 + for i, p := range w.Probabilities { + prob += p + if choosenRandomPosition-epsilon < prob { + return uint(i) + } + } + panic("cannot have a probability above 1") +} + +func (w *Doorman) GenerateRandomProbabilityFromInteger(data uint64) float64 { + var ret float64 = 0 + for i := 0; data > 0; i++ { + if data&1 == 1 { + ret += 1.0 / math.Pow(2, float64(i+1)) + } + data >>= 1 + } + return ret +} + +func (w *Doorman) Hash(data ...[]byte) uint64 { + h := siphash.New(make([]byte, 16)) + for _, datum := range data { + h.Write(datum) + } + return h.Sum64() +} + +func (w *Doorman) GetCaseFromData(data ...[]byte) uint { + random := w.GenerateRandomProbabilityFromInteger(w.Hash(data...)) + return w.GetCase(random) +} + +func (w *Doorman) GetRandomCase() uint { + return w.GetCase(rand.Float64()) +} diff --git a/doorman_test.go b/doorman_test.go new file mode 100644 index 0000000..e101bda --- /dev/null +++ b/doorman_test.go @@ -0,0 +1,215 @@ +package doorman + +import ( + "encoding/json" + "fmt" + "math" + "net/http" + "net/http/httptest" + "reflect" + "testing" + + "github.com/didiercrunch/doorman/shared" + "gopkg.in/mgo.v2/bson" +) + +func assertIsAbout(t *testing.T, expected, received float64) { + if math.Abs(expected-received) > epsilon { + t.Error("received", received, "but expected", expected) + } +} + +func assertIsNotAbout(t *testing.T, expected, received float64) { + if math.Abs(expected-received) < epsilon { + t.Error("received", received, "but expected something different than", expected) + } +} + +var oid = "507f1f77bcf86cd799439011" + +func TestIsZero(t *testing.T) { + if !IsZero(epsilon / 2) { + t.Error() + } + + if !IsZero(-epsilon / 2) { + t.Error() + } + if IsZero(epsilon * 1.5) { + t.Error() + } + if IsZero(-epsilon * 1.5) { + t.Error() + } +} + +func TestIsEqual(t *testing.T) { + if !IsEqual(1.0, 1.0+epsilon/2) { + t.Error() + } + if IsEqual(1.0, 1.0+epsilon*1.5) { + t.Error() + } +} + +func TestValidate(t *testing.T) { + w := &Doorman{} + if w.Validate().Error() != "not initiated" { + t.Error() + } + + w = &Doorman{Probabilities: []float64{0.5, 0.75}} + if w.Validate().Error() != "The sum of probabilities is not one" { + t.Error() + } + + w = &Doorman{Probabilities: []float64{0.25, 0.75}} + if w.Validate() != nil { + t.Error() + } + + w = &Doorman{Probabilities: []float64{0.2500000000000002, 0.75}} + if w.Validate() != nil { + t.Error() + } +} + +func TestGetCase(t *testing.T) { + w := NewDoorman(bson.NewObjectId(), []float64{0.25, 0.5, 0.25}) + if c := w.GetCase(0); c != 0 { + t.Error("expected 0 but received", c) + } + + if c := w.GetCase(1); c != 2 { + t.Error("expected 2 but received", c) + } +} + +func TestGetCaseCoroutineSafety(t *testing.T) { + w := NewDoorman(bson.NewObjectId(), []float64{0.25, 0.5, 0.25}) + i := 0 + w.wg.Add(1) + go func() { + i++ + w.wg.Done() + }() + w.GetCase(0) + if i != 1 { + t.Error("error with goroutine safety") + } +} + +func TestGenerateRandomProbabilityFromBitSlice(t *testing.T) { + w := new(Doorman) + assertIsAbout(t, 0.5, w.GenerateRandomProbabilityFromInteger(1)) + + assertIsAbout(t, 0.5+0*0.25+1*0.125, w.GenerateRandomProbabilityFromInteger(5)) + + assertIsAbout(t, 1, w.GenerateRandomProbabilityFromInteger(math.MaxUint64)) +} + +func TestHash(t *testing.T) { + m := []byte("doormen are great") + w := new(Doorman) + var expt uint64 = 0x3973fc1b3e324215 + result := w.Hash(m) + + if result != expt { + t.Error(fmt.Sprintf("bad hashing, expected: %x, result: %x", expt, result)) + } +} + +func TestUpdateTimestamp(t *testing.T) { + + w := &Doorman{LastChangeTimestamp: 10, Id: bson.ObjectIdHex(oid)} + m := &shared.DoormanUpdater{Timestamp: 9} + w.Update(m) + if w.LastChangeTimestamp != 10 { + t.Error() + } + + m = &shared.DoormanUpdater{Timestamp: 11, Probabilities: []float64{}, Id: oid} + w.Update(m) + if w.LastChangeTimestamp != 11 { + t.Error() + } +} + +func TestUpdateProbabilities(t *testing.T) { + w := &Doorman{LastChangeTimestamp: 0, Id: bson.ObjectIdHex(oid)} + m := &shared.DoormanUpdater{Timestamp: 0, Probabilities: []float64{0.5, 0.5}, Id: oid} + w.Update(m) + if len(w.Probabilities) != 0 { + t.Error() + } + + m = &shared.DoormanUpdater{Timestamp: 2, Probabilities: []float64{0.5, 0.5}, Id: oid} + if err := w.Update(m); err != nil { + t.Error(err) + } else if !reflect.DeepEqual(w.Probabilities, []float64{0.5, 0.5}) { + t.Error("bad prob") + } + + m = &shared.DoormanUpdater{Timestamp: 3, Probabilities: []float64{0.5, 0.25}, Id: oid} + if err := w.Update(m); err == nil { + t.Error("should received an error") + } else if !reflect.DeepEqual(w.Probabilities, []float64{0.5, 0.5}) { + t.Error("bad prob") + } + +} + +func randomBytes(n int) []byte { + b := make([]byte, n) + for i := range b { + b[i] = byte(i % 256) + } + return b +} + +func TestUpdateHard(t *testing.T) { + + wab := NewDoorman(bson.ObjectIdHex(oid), []float64{}) + createMessage := func() string { + msg := &shared.DoormanUpdater{Timestamp: 2, Probabilities: []float64{0.5, 0.25, 0.25}, Id: oid} + if ret, err := json.Marshal(msg); err != nil { + panic(err) + } else { + return string(ret) + } + } + handler := func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/"+wab.Id.Hex() { + t.Error("bad url", r.URL) + } + fmt.Fprint(w, createMessage()) + } + + ts := httptest.NewServer(http.HandlerFunc(handler)) + defer ts.Close() + if err := wab.UpdateHard(ts.URL); err != nil { + t.Error(err) + } + if wab.Length() != 3 { + t.Error() + } +} + +func TestGetRandomCase(t *testing.T) { + wab := NewDoorman(bson.NewObjectId(), []float64{0.5, 0.5}) + var ret uint = 0 + for i := 0; i < 10000; i++ { + ret += wab.GetRandomCase() + } + if ret < 4900 || ret > 5100 { + t.Error("very improbable result of ", ret) + } +} + +func BenchmarkGetCaseFromData(b *testing.B) { + var data = randomBytes(1024 * 1024) + w := NewDoorman(bson.NewObjectId(), []float64{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 new file mode 100644 index 0000000..ed4ea2d --- /dev/null +++ b/example/integration_test.go @@ -0,0 +1,26 @@ +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 new file mode 100644 index 0000000..e8f4c24 --- /dev/null +++ b/example/main.go @@ -0,0 +1,78 @@ +package main + +import ( + "html/template" + "log" + "net/http" + "os" + + "github.com/didiercrunch/doorman" + "gopkg.in/mgo.v2/bson" +) + +var wab = doorman.NewDoorman(bson.ObjectIdHex("54b33d5ef6fb5e3b77000001"), []float64{0.25, 0.5, 0.10, 0.05, 0.1}) + +func init() { + if err := wab.NanoMsgSubscriber("tcp://127.0.0.1:40899"); err != nil { + panic(err) + } +} + +func GetDoormanValue(r *http.Request) (string, int) { + switch wab.GetCaseFromData([]byte(r.URL.String())) { + case 0: + return "blue", 0 + case 1: + return "red", 1 + case 2: + return "pink", 2 + case 3: + return "yellow", 3 + case 4: + return "magenta", 4 + } + log.Panic("impossible value returned by doorman") + return "", 0 +} + +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. +

+ + +` + +func Handler(w http.ResponseWriter, r *http.Request) { + t, err := template.New("foo").Parse(tmpl) + if err != nil { + panic(err) + } + m := make(map[string]interface{}) + m["color"], m["value"] = GetDoormanValue(r) + if err := t.Execute(w, m); err != nil { + panic(err) + } +} + +func getPort() string { + if len(os.Args) > 1 { + return os.Args[1] + } else { + return "8080" + } +} + +func main() { + http.HandleFunc("/", Handler) + port := getPort() + log.Println("serving on port", port) + log.Fatal(http.ListenAndServe(":"+port, nil)) + +} diff --git a/nanomsgsubscriber/nanomsgsubscriber.go b/nanomsgsubscriber/nanomsgsubscriber.go new file mode 100644 index 0000000..f1abbac --- /dev/null +++ b/nanomsgsubscriber/nanomsgsubscriber.go @@ -0,0 +1,59 @@ +package nanomsgsubscriber + +import ( + "encoding/json" + "errors" + "log" + + "github.com/didiercrunch/doorman/shared" + "github.com/gdamore/mangos" + "github.com/gdamore/mangos/protocol/sub" + "github.com/gdamore/mangos/transport/ipc" + "github.com/gdamore/mangos/transport/tcp" + "gopkg.in/mgo.v2/bson" +) + +type NanoMsgSubscriber struct { + Url string +} + +func (s *NanoMsgSubscriber) 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 *NanoMsgSubscriber) Subscribe(abtestId bson.ObjectId, update shared.UpdateHandlerFunc) error { + var sock mangos.Socket + var err error + var msg []byte + + if sock, err = sub.NewSocket(); err != nil { + return errors.New("can't get new sub socket: " + err.Error()) + } + sock.AddTransport(ipc.NewTransport()) + sock.AddTransport(tcp.NewTransport()) + if err = sock.Dial(s.Url); err != nil { + return errors.New("can't dial on sub socket: " + err.Error()) + } + // Empty byte array effectively subscribes to everything + if err = sock.SetOption(mangos.OptionSubscribe, []byte("")); err != nil { + return errors.New("cannot subscribe: " + err.Error()) + } + go func(s *NanoMsgSubscriber, sock mangos.Socket) { + for { + if msg, err = sock.Recv(); err != nil { + err := errors.New("Cannot recv: " + err.Error()) + log.Println(err) + } else if err := s.callUpdateHandlerFunction(update, msg); err != nil { + log.Println("cannot update abtest with received data: ", err) + } + } + }(s, sock) + + return nil + +} diff --git a/nanomsgsubscriber/nanomsgsubscriber_test.go b/nanomsgsubscriber/nanomsgsubscriber_test.go new file mode 100644 index 0000000..f010f35 --- /dev/null +++ b/nanomsgsubscriber/nanomsgsubscriber_test.go @@ -0,0 +1,27 @@ +package nanomsgsubscriber + +import ( + "encoding/json" + "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(NanoMsgSubscriber).callUpdateHandlerFunction(f, data); err != nil { + t.Error(err) + } +} diff --git a/nsqsubscriber/messages.go b/nsqsubscriber/messages.go new file mode 100644 index 0000000..a064f6c --- /dev/null +++ b/nsqsubscriber/messages.go @@ -0,0 +1,15 @@ +package nsqsubscriber + +import "github.com/bitly/go-nsq" + +type NSQMessage struct { + *nsq.Message +} + +func (m *NSQMessage) Body() []byte { + return m.Message.Body +} + +func (m *NSQMessage) Timestamp() int64 { + return m.Message.Timestamp +} diff --git a/nsqsubscriber/nsqsubscriber.go b/nsqsubscriber/nsqsubscriber.go new file mode 100644 index 0000000..e3d5485 --- /dev/null +++ b/nsqsubscriber/nsqsubscriber.go @@ -0,0 +1,40 @@ +package nsqsubscriber + +import ( + "encoding/json" + + "code.google.com/p/go-uuid/uuid" + "github.com/bitly/go-nsq" + "github.com/didiercrunch/doorman/shared" + "gopkg.in/mgo.v2/bson" +) + +var UUID = uuid.New() + +type NSQSubscriber struct { + NSQLookupURL string +} + +func toNSQHandlerFunc(update shared.UpdateHandlerFunc) nsq.HandlerFunc { + return func(message *nsq.Message) error { + wu := &shared.DoormanUpdater{} + if err := json.Unmarshal(message.Body, wu); err != nil { + return err + } + wu.Timestamp = message.Timestamp + return update(wu) + } +} + +func (sub *NSQSubscriber) Subscribe(doormanId bson.ObjectId, update shared.UpdateHandlerFunc) error { + config := nsq.NewConfig() + q, err := nsq.NewConsumer(doormanId.Hex(), UUID, config) + if err != nil { + return err + } + q.AddHandler(nsq.HandlerFunc(toNSQHandlerFunc(update))) + if err := q.ConnectToNSQLookupd(sub.NSQLookupURL); err != nil { + return err + } + return nil +} diff --git a/nsqsubscriber/nsqsubscriber_test.go b/nsqsubscriber/nsqsubscriber_test.go new file mode 100644 index 0000000..41a0b87 --- /dev/null +++ b/nsqsubscriber/nsqsubscriber_test.go @@ -0,0 +1,19 @@ +package nsqsubscriber + +import ( + "testing" + + "github.com/bitly/go-nsq" +) + +func TestNSQMessage(t *testing.T) { + m := &nsq.Message{Body: []byte("hello world"), Timestamp: 10} + nsqM := &NSQMessage{m} + if string(nsqM.Body()) != "hello world" { + t.Error() + } + + if nsqM.Timestamp() != 10 { + t.Error() + } +} diff --git a/shared/interfaces.go b/shared/interfaces.go new file mode 100644 index 0000000..8fe2a23 --- /dev/null +++ b/shared/interfaces.go @@ -0,0 +1,9 @@ +package shared + +type DoormanUpdater struct { + Id string `json:"id"` + Timestamp int64 `json:"timestamp"` + Probabilities []float64 `json:"probabilities"` +} + +type UpdateHandlerFunc func(m *DoormanUpdater) error diff --git a/subscribe.go b/subscribe.go new file mode 100644 index 0000000..9b031e8 --- /dev/null +++ b/subscribe.go @@ -0,0 +1,26 @@ +package doorman + +import ( + "github.com/didiercrunch/doorman/nanomsgsubscriber" + "github.com/didiercrunch/doorman/nsqsubscriber" + "github.com/didiercrunch/doorman/shared" + "gopkg.in/mgo.v2/bson" +) + +type Subscriber interface { + Subscribe(doormanId bson.ObjectId, update shared.UpdateHandlerFunc) 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) +} + +func (w *Doorman) NanoMsgSubscriber(NanoMsgUrlLookupdURl string) error { + sub := &nanomsgsubscriber.NanoMsgSubscriber{NanoMsgUrlLookupdURl} + return w.Subscribe(sub) +}