first commit.
way too late.
This commit is contained in:
commit
dbbfaf99b6
15 files changed
+757
No files matched your search
+27
@@ -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
|
||||
@@ -0,0 +1,6 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.3
|
||||
- 1.4
|
||||
- tip
|
||||
@@ -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.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Doorman [](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.
|
||||
+153
@@ -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())
|
||||
}
|
||||
+215
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
//}
|
||||
|
||||
}
|
||||
@@ -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 = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head></head>
|
||||
<body style="background:{{ .color }};width:100%;height:100%">
|
||||
<h1 style="text-align:center;">the doorman value is {{ .value }}</h1>
|
||||
<p style="text-align:center;">
|
||||
this doorman depend solely on the url. If you hit different url you
|
||||
will have different doorman results.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
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))
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user