| @ -0,0 +1,2 @@ | |||||
| dump.rdb | |||||
| golab | |||||
| @ -0,0 +1,4 @@ | |||||
| GoLab | |||||
| ===== | |||||
| A/B Testing server. | |||||
| @ -0,0 +1,68 @@ | |||||
| package main | |||||
| import ( | |||||
| "flag" | |||||
| "github.com/Sirupsen/logrus" | |||||
| "github.com/brettlangdon/golab/server" | |||||
| "github.com/garyburd/redigo/redis" | |||||
| "net" | |||||
| "strings" | |||||
| "time" | |||||
| ) | |||||
| func newPool(host string, idle int) *redis.Pool { | |||||
| return &redis.Pool{ | |||||
| MaxIdle: idle, | |||||
| IdleTimeout: 240 * time.Second, | |||||
| Dial: func() (redis.Conn, error) { | |||||
| server.Log.Debugln("Worker Connecting to Redis Host", host) | |||||
| c, err := redis.Dial("tcp", host) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return c, err | |||||
| }, | |||||
| TestOnBorrow: func(c redis.Conn, t time.Time) error { | |||||
| server.Log.Debugln("Worker PINGing Redis Host", host) | |||||
| _, err := c.Do("PING") | |||||
| return err | |||||
| }, | |||||
| } | |||||
| } | |||||
| func main() { | |||||
| var bind = flag.String("bind", ":11222", "Which [host]:port to bind to [default: ':11222']") | |||||
| var level = flag.String("log", "INFO", "Set the log level [default: 'INFO']") | |||||
| var rhost = flag.String("redis", ":6379", "Which redis [host]:port to connect to [default: ':6379']") | |||||
| var rpool = flag.Int("pool", 3, "How many redis connections to have in the pool [default: 3]") | |||||
| flag.Parse() | |||||
| switch strings.ToLower(*level) { | |||||
| case "debug": | |||||
| server.Log.Level = logrus.DebugLevel | |||||
| case "info": | |||||
| server.Log.Level = logrus.InfoLevel | |||||
| case "warn": | |||||
| server.Log.Level = logrus.WarnLevel | |||||
| case "error": | |||||
| server.Log.Level = logrus.ErrorLevel | |||||
| } | |||||
| server.Log.Infoln("Starting Golab Server") | |||||
| ln, err := net.Listen("tcp", *bind) | |||||
| if err != nil { | |||||
| return | |||||
| } | |||||
| server.Log.Debugln("Connecting to Redis Pool", *rhost, "with", *rpool, "Connections") | |||||
| var pool *redis.Pool = newPool(*rhost, *rpool) | |||||
| server.Log.Infoln("Accepting Connections on", *bind) | |||||
| for { | |||||
| conn, err := ln.Accept() | |||||
| if err != nil { | |||||
| continue | |||||
| } | |||||
| go server.HandleConnection(conn, pool) | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,267 @@ | |||||
| package server | |||||
| import ( | |||||
| "bufio" | |||||
| "bytes" | |||||
| "encoding/json" | |||||
| "errors" | |||||
| "fmt" | |||||
| "github.com/garyburd/redigo/redis" | |||||
| "math/rand" | |||||
| "net" | |||||
| "strconv" | |||||
| ) | |||||
| type ClientConnection struct { | |||||
| conn net.Conn | |||||
| reader *bufio.Reader | |||||
| pool *redis.Pool | |||||
| redisConn redis.Conn | |||||
| } | |||||
| func (client *ClientConnection) ObtainRedisConnection() { | |||||
| client.redisConn = client.pool.Get() | |||||
| } | |||||
| func (client *ClientConnection) ReleaseRedisConnection() { | |||||
| client.redisConn.Close() | |||||
| client.redisConn = nil | |||||
| } | |||||
| func (client *ClientConnection) ReadLine() (line []byte, err error) { | |||||
| line, err = client.reader.ReadBytes('\n') | |||||
| line = bytes.Trim(line, "\r\n ") | |||||
| return line, err | |||||
| } | |||||
| func (client *ClientConnection) Error() (num int, err error) { | |||||
| return client.conn.Write([]byte("ERROR\r\n")) | |||||
| } | |||||
| func (client *ClientConnection) ClientError(msg []byte) (num int, err error) { | |||||
| response := append([]byte("CLIENT_ERROR "), msg...) | |||||
| response = append(response, '\r', '\n') | |||||
| return client.conn.Write(response) | |||||
| } | |||||
| func (client *ClientConnection) ServerError(msg []byte) (num int, err error) { | |||||
| response := append([]byte("Server_ERROR "), msg...) | |||||
| response = append(response, '\r', '\n') | |||||
| return client.conn.Write(response) | |||||
| } | |||||
| func (client *ClientConnection) SendData(data []byte) (num int, err error) { | |||||
| data = append(data, '\r', '\n') | |||||
| return client.conn.Write(data) | |||||
| } | |||||
| func (client *ClientConnection) SendValue(key []byte, value []byte) (num int, err error) { | |||||
| response := append([]byte("VALUE "), key...) | |||||
| size := strconv.Itoa(len(value)) | |||||
| response = append(response, []byte(" 0 ")...) | |||||
| response = append(response, []byte(size)...) | |||||
| response = append(response, '\r', '\n') | |||||
| response = append(response, value...) | |||||
| response = append(response, '\r', '\n') | |||||
| return client.conn.Write(response) | |||||
| } | |||||
| func (client *ClientConnection) SendStat(key []byte, value []byte) (num int, err error) { | |||||
| response := append([]byte("STAT "), key...) | |||||
| response = append(response, ' ') | |||||
| response = bytes.Replace(response, []byte(":"), []byte("."), -1) | |||||
| response = append(response, value...) | |||||
| response = append(response, '\r', '\n') | |||||
| return client.conn.Write(response) | |||||
| } | |||||
| func (client *ClientConnection) SendEnd() (num int, err error) { | |||||
| return client.conn.Write([]byte("END\r\n")) | |||||
| } | |||||
| func (client *ClientConnection) SendStored() (num int, err error) { | |||||
| return client.conn.Write([]byte("STORED\r\n")) | |||||
| } | |||||
| func (client *ClientConnection) SendNotStored() (num int, err error) { | |||||
| return client.conn.Write([]byte("NOT_STORED\r\n")) | |||||
| } | |||||
| func (client *ClientConnection) SendDeleted() (num int, err error) { | |||||
| return client.conn.Write([]byte("DELETED\r\n")) | |||||
| } | |||||
| func (client *ClientConnection) SendTouched() (num int, err error) { | |||||
| return client.conn.Write([]byte("TOUCHED\r\n")) | |||||
| } | |||||
| func (client *ClientConnection) SendNotFound() (num int, err error) { | |||||
| return client.conn.Write([]byte("NOT_FOUND\r\n")) | |||||
| } | |||||
| func (client *ClientConnection) GetExperiment(id uint64, active bool) (data Experiment, err error) { | |||||
| dummy := Experiment{Id: id} | |||||
| raw, err := client.redisConn.Do("GET", dummy.Key()) | |||||
| if err != nil { | |||||
| fmt.Printf("%+v\r\n", err) | |||||
| } else if raw != nil { | |||||
| err = json.Unmarshal(raw.([]byte), &data) | |||||
| if err == nil && data.Id == 0 { | |||||
| data.Id = id | |||||
| } | |||||
| if err == nil && active { | |||||
| raw, err := client.redisConn.Do("SISMEMBER", "active_experiments", id) | |||||
| if err != nil || int(raw.(int64)) == 0 { | |||||
| return data, errors.New("Experiment is not active") | |||||
| } | |||||
| } | |||||
| } else { | |||||
| return data, errors.New("No Experiment Found") | |||||
| } | |||||
| return data, err | |||||
| } | |||||
| func (client *ClientConnection) GetUserBucket(experiment Experiment, userId []byte, assign bool) (data Variant, err error) { | |||||
| userKey := append([]byte("user:"), userId...) | |||||
| raw, err := client.redisConn.Do("HGET", userKey, experiment.Key()) | |||||
| if err != nil { | |||||
| fmt.Printf("%+v\r\n", err) | |||||
| } else if raw != nil { | |||||
| variantId, err := strconv.ParseUint(string(raw.([]byte)), 10, 64) | |||||
| if err == nil { | |||||
| for _, variant := range experiment.Variants { | |||||
| if variant.Id == variantId { | |||||
| data = variant | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| if assign && data.Id == 0 { | |||||
| idx := rand.Intn(len(experiment.Variants)) | |||||
| data = experiment.Variants[idx] | |||||
| _, err = client.redisConn.Do("HSET", userKey, experiment.Key(), data.Id) | |||||
| _, err = client.redisConn.Do("SADD", experiment.BucketUniqueKey(data.Id), userId) | |||||
| } | |||||
| _, err = client.redisConn.Do("INCR", experiment.BucketImpressionsKey(data.Id)) | |||||
| return data, err | |||||
| } | |||||
| func (client *ClientConnection) ConvertUser(experimentId uint64, userId []byte) (err error) { | |||||
| experiment, err := client.GetExperiment(experimentId, true) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| bucket, err := client.GetUserBucket(experiment, userId, false) | |||||
| if bucket.Id != 0 { | |||||
| _, err = client.redisConn.Do("SADD", experiment.ConvertUniqueKey(bucket.Id), userId) | |||||
| _, err = client.redisConn.Do("INCR", experiment.ConvertImpressionsKey(bucket.Id)) | |||||
| } | |||||
| return err | |||||
| } | |||||
| func (client *ClientConnection) GetActiveExperimentIds() (data [][]byte, err error) { | |||||
| raw, err := client.redisConn.Do("SMEMBERS", "active_experiments") | |||||
| for _, id := range raw.([]interface{}) { | |||||
| data = append(data, id.([]byte)) | |||||
| } | |||||
| return data, err | |||||
| } | |||||
| func (client *ClientConnection) GetAllExperimentIds() (data [][]byte, err error) { | |||||
| raw, err := client.redisConn.Do("KEYS", "experiment:*") | |||||
| for _, key := range raw.([]interface{}) { | |||||
| parts := bytes.Split(key.([]byte), []byte(":")) | |||||
| if len(parts) != 2 { | |||||
| continue | |||||
| } | |||||
| data = append(data, parts[1]) | |||||
| } | |||||
| return data, err | |||||
| } | |||||
| func (client *ClientConnection) GetExperimentStats(experimentId uint64) (err error) { | |||||
| experiment, err := client.GetExperiment(experimentId, false) | |||||
| for _, variant := range experiment.Variants { | |||||
| raw, _ := client.redisConn.Do("SCARD", experiment.BucketUniqueKey(variant.Id)) | |||||
| if raw == nil { | |||||
| raw = int64(0) | |||||
| } | |||||
| client.SendStat(experiment.BucketUniqueKey(variant.Id), []byte(strconv.Itoa(int(raw.(int64))))) | |||||
| raw, _ = client.redisConn.Do("GET", experiment.BucketImpressionsKey(variant.Id)) | |||||
| if raw == nil { | |||||
| raw = []uint8{0} | |||||
| } | |||||
| client.SendStat(experiment.BucketImpressionsKey(variant.Id), raw.([]uint8)) | |||||
| raw, _ = client.redisConn.Do("SCARD", experiment.ConvertUniqueKey(variant.Id)) | |||||
| if raw == nil { | |||||
| raw = int64(0) | |||||
| } | |||||
| client.SendStat(experiment.ConvertUniqueKey(variant.Id), []byte(strconv.Itoa(int(raw.(int64))))) | |||||
| raw, _ = client.redisConn.Do("GET", experiment.ConvertImpressionsKey(variant.Id)) | |||||
| if raw == nil { | |||||
| raw = []uint8{0} | |||||
| } | |||||
| client.SendStat(experiment.ConvertImpressionsKey(variant.Id), raw.([]uint8)) | |||||
| } | |||||
| return err | |||||
| } | |||||
| func (client *ClientConnection) AddNewExperiment(data []byte) (err error) { | |||||
| err = json.Unmarshal(data, new(interface{})) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| raw, err := client.redisConn.Do("INCR", "experiments") | |||||
| nextId := uint64(raw.(int64)) | |||||
| dummy := Experiment{Id: nextId} | |||||
| _, err = client.redisConn.Do("SET", dummy.Key(), data) | |||||
| return err | |||||
| } | |||||
| func (client *ClientConnection) UpdateExperiment(experimentId uint64, data []byte) (err error) { | |||||
| err = json.Unmarshal(data, new(interface{})) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| experiment, err := client.GetExperiment(experimentId, false) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| _, err = client.redisConn.Do("SET", experiment.Key(), data) | |||||
| return err | |||||
| } | |||||
| func (client *ClientConnection) DeactivateExperiment(experimentId uint64) (err error) { | |||||
| _, err = client.redisConn.Do("SREM", "active_experiments", experimentId) | |||||
| return err | |||||
| } | |||||
| func (client *ClientConnection) ActivateExperiment(experimentId uint64) (err error) { | |||||
| _, err = client.redisConn.Do("SADD", "active_experiments", experimentId) | |||||
| return err | |||||
| } | |||||
| @ -0,0 +1,53 @@ | |||||
| package server | |||||
| import ( | |||||
| "strconv" | |||||
| ) | |||||
| type Experiment struct { | |||||
| Id uint64 `json:"id"` | |||||
| Name string `json:"name"` | |||||
| Description string `json:"description"` | |||||
| Hypothesis string `json:"hypothesis"` | |||||
| Variants []Variant `json:"variants"` | |||||
| } | |||||
| func (exp *Experiment) Key() (key []byte) { | |||||
| return append([]byte("experiment:"), []byte(strconv.FormatUint(exp.Id, 10))...) | |||||
| } | |||||
| func (exp *Experiment) BucketUniqueKey(id uint64) (key []byte) { | |||||
| key = []byte("bucket-users:") | |||||
| key = append(key, []byte(strconv.FormatUint(exp.Id, 10))...) | |||||
| key = append(key, []byte(":")...) | |||||
| return append(key, []byte(strconv.FormatUint(id, 10))...) | |||||
| } | |||||
| func (exp *Experiment) BucketImpressionsKey(id uint64) (key []byte) { | |||||
| key = []byte("bucket-impressions:") | |||||
| key = append(key, []byte(strconv.FormatUint(exp.Id, 10))...) | |||||
| key = append(key, []byte(":")...) | |||||
| return append(key, []byte(strconv.FormatUint(id, 10))...) | |||||
| } | |||||
| func (exp *Experiment) ConvertUniqueKey(id uint64) (key []byte) { | |||||
| key = []byte("convert-users:") | |||||
| key = append(key, []byte(strconv.FormatUint(exp.Id, 10))...) | |||||
| key = append(key, []byte(":")...) | |||||
| return append(key, []byte(strconv.FormatUint(id, 10))...) | |||||
| } | |||||
| func (exp *Experiment) ConvertImpressionsKey(id uint64) (key []byte) { | |||||
| key = []byte("convert-impressions:") | |||||
| key = append(key, []byte(strconv.FormatUint(exp.Id, 10))...) | |||||
| key = append(key, []byte(":")...) | |||||
| return append(key, []byte(strconv.FormatUint(id, 10))...) | |||||
| } | |||||
| type Variant struct { | |||||
| Id uint64 `json:"id"` | |||||
| Name string `json:"name"` | |||||
| Value string `json:"value"` | |||||
| Control bool `json:"control"` | |||||
| Weight int64 `json:"weight"` | |||||
| } | |||||
| @ -0,0 +1,7 @@ | |||||
| package server | |||||
| import ( | |||||
| "github.com/Sirupsen/logrus" | |||||
| ) | |||||
| var Log = logrus.New() | |||||
| @ -0,0 +1,335 @@ | |||||
| package server | |||||
| import ( | |||||
| "bufio" | |||||
| "bytes" | |||||
| "encoding/json" | |||||
| "errors" | |||||
| "github.com/garyburd/redigo/redis" | |||||
| "net" | |||||
| "strconv" | |||||
| ) | |||||
| func HandleConnection(conn net.Conn, pool *redis.Pool) { | |||||
| defer conn.Close() | |||||
| client := ClientConnection{ | |||||
| conn: conn, | |||||
| reader: bufio.NewReader(conn), | |||||
| pool: pool, | |||||
| } | |||||
| Log.Debugln("New Client Connection =>", conn.RemoteAddr()) | |||||
| for { | |||||
| line, err := client.ReadLine() | |||||
| if err != nil { | |||||
| break | |||||
| } | |||||
| Log.Debugln("New Message =>", string(line)) | |||||
| err = HandleLine(line, client) | |||||
| if err != nil { | |||||
| break | |||||
| } | |||||
| } | |||||
| } | |||||
| func HandleLine(line []byte, client ClientConnection) (err error) { | |||||
| parts := bytes.Split(line, []byte(" ")) | |||||
| if len(parts[0]) > 0 { | |||||
| command := bytes.ToLower(parts[0]) | |||||
| args := parts[1:] | |||||
| switch string(command) { | |||||
| case "get", "gets": | |||||
| err = HandleGet(args, client) | |||||
| case "set", "replace": | |||||
| err = HandleSet(args, client) | |||||
| case "add": | |||||
| err = HandleAdd(args, client) | |||||
| case "delete": | |||||
| err = HandleDelete(args, client) | |||||
| case "incr": | |||||
| err = HandleIncr(args, client) | |||||
| case "touch": | |||||
| err = HandleTouch(args, client) | |||||
| case "stats": | |||||
| err = HandleStats(args, client) | |||||
| case "quit": | |||||
| err = errors.New("Quitting") | |||||
| case "decr", "flush", "append", "prepend", "cas": | |||||
| Log.Debugln("Command", string(command), "Not Implemented") | |||||
| _, err = client.ClientError([]byte("Command Not Implemented")) | |||||
| default: | |||||
| Log.Debugln("Command", string(command), "Unknown") | |||||
| _, err = client.ClientError([]byte("Unknown Command")) | |||||
| } | |||||
| } | |||||
| return err | |||||
| } | |||||
| func HandleGet(args [][]byte, client ClientConnection) (err error) { | |||||
| client.ObtainRedisConnection() | |||||
| defer client.ReleaseRedisConnection() | |||||
| if len(args) == 0 { | |||||
| _, err = client.ClientError([]byte("GET Command needs at least 1 '<experiment>:<user_id>' pair")) | |||||
| } else { | |||||
| for _, key := range args { | |||||
| parts := bytes.Split(key, []byte(":")) | |||||
| if len(parts) < 2 { | |||||
| _, err = client.ClientError([]byte("GET argument must be in the format '<experiment>:<user_id>'")) | |||||
| } else { | |||||
| if bytes.Equal(bytes.ToLower(parts[0]), []byte("experiment")) { | |||||
| ids := [][]byte{parts[1]} | |||||
| multi := false | |||||
| if bytes.Equal(parts[1], []byte("*")) { | |||||
| multi = true | |||||
| ids, err = client.GetAllExperimentIds() | |||||
| if err != nil { | |||||
| continue | |||||
| } | |||||
| } else if bytes.Equal(parts[1], []byte("active")) { | |||||
| multi = true | |||||
| ids, err = client.GetActiveExperimentIds() | |||||
| if err != nil { | |||||
| continue | |||||
| } | |||||
| } | |||||
| experiments := make([]Experiment, 0) | |||||
| for _, id := range ids { | |||||
| experimentId, err := strconv.ParseUint(string(id), 10, 64) | |||||
| if err != nil { | |||||
| continue | |||||
| } | |||||
| experiment, err := client.GetExperiment(experimentId, false) | |||||
| if err != nil { | |||||
| continue | |||||
| } | |||||
| experiments = append(experiments, experiment) | |||||
| } | |||||
| if len(experiments) == 0 { | |||||
| continue | |||||
| } | |||||
| var data []byte | |||||
| if len(experiments) == 1 && multi == false { | |||||
| data, err = json.Marshal(experiments[0]) | |||||
| } else { | |||||
| data, err = json.Marshal(experiments) | |||||
| } | |||||
| if err != nil { | |||||
| continue | |||||
| } | |||||
| _, err = client.SendValue(key, data) | |||||
| if err != nil { | |||||
| continue | |||||
| } | |||||
| } else { | |||||
| experimentId, err := strconv.ParseUint(string(parts[0]), 10, 64) | |||||
| if err != nil { | |||||
| continue | |||||
| } | |||||
| experiment, err := client.GetExperiment(experimentId, true) | |||||
| if err != nil { | |||||
| continue | |||||
| } | |||||
| bucket, err := client.GetUserBucket(experiment, parts[1], true) | |||||
| _, err = client.SendValue(key, []byte(bucket.Value)) | |||||
| if err != nil { | |||||
| break | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| _, err = client.SendEnd() | |||||
| } | |||||
| return err | |||||
| } | |||||
| func HandleIncr(args [][]byte, client ClientConnection) (err error) { | |||||
| client.ObtainRedisConnection() | |||||
| defer client.ReleaseRedisConnection() | |||||
| if len(args) < 1 { | |||||
| _, err = client.ClientError([]byte("INCR command needs at least 1 argument <key> [delta]")) | |||||
| } else { | |||||
| parts := bytes.Split(args[0], []byte(":")) | |||||
| stored := false | |||||
| if len(parts) < 2 { | |||||
| _, err = client.ClientError([]byte("INCR key must be in the format '<experiment>:<user_id>'")) | |||||
| } else { | |||||
| experimentId, err := strconv.ParseUint(string(parts[0]), 10, 64) | |||||
| if err == nil { | |||||
| err = client.ConvertUser(experimentId, parts[1]) | |||||
| stored = err == nil | |||||
| } | |||||
| } | |||||
| if stored { | |||||
| _, err = client.SendData([]byte("1")) | |||||
| } else if stored == false { | |||||
| _, err = client.SendData([]byte("0")) | |||||
| } | |||||
| } | |||||
| return err | |||||
| } | |||||
| func HandleAdd(args [][]byte, client ClientConnection) (err error) { | |||||
| client.ObtainRedisConnection() | |||||
| defer client.ReleaseRedisConnection() | |||||
| if len(args) < 4 { | |||||
| _, err = client.ClientError([]byte("ADD Command needs at least 4 arguments, <key> <flags> <exptime> <bytes> [noreply]")) | |||||
| } else { | |||||
| key, rawBytes := args[0], args[3] | |||||
| noreply := (len(args) >= 5 && bytes.Equal(bytes.ToLower(args[4]), []byte("noreply"))) | |||||
| numBytes, err := strconv.ParseUint(string(rawBytes), 10, 64) | |||||
| stored := false | |||||
| if err == nil { | |||||
| data, err := client.ReadLine() | |||||
| if err != nil || uint64(len(data)) != numBytes { | |||||
| _, err = client.ClientError([]byte("Value length does not match number of bytes send")) | |||||
| } else { | |||||
| experimentId, err := strconv.ParseUint(string(key), 10, 64) | |||||
| if err != nil || experimentId <= 0 { | |||||
| _, err = client.ClientError([]byte("ADD key must be a positive integer")) | |||||
| } | |||||
| err = client.AddNewExperiment(data) | |||||
| stored = err == nil | |||||
| } | |||||
| } | |||||
| if stored && noreply == false { | |||||
| _, err = client.SendStored() | |||||
| } else if stored == false && noreply == false { | |||||
| _, err = client.SendNotStored() | |||||
| } | |||||
| } | |||||
| return err | |||||
| } | |||||
| func HandleSet(args [][]byte, client ClientConnection) (err error) { | |||||
| client.ObtainRedisConnection() | |||||
| defer client.ReleaseRedisConnection() | |||||
| if len(args) < 4 { | |||||
| _, err = client.ClientError([]byte("SET Command needs at least 4 arguments, <key> <flags> <exptime> <bytes> [noreply]")) | |||||
| } else { | |||||
| key, rawBytes := args[0], args[3] | |||||
| noreply := (len(args) >= 5 && bytes.Equal(bytes.ToLower(args[4]), []byte("noreply"))) | |||||
| numBytes, err := strconv.ParseUint(string(rawBytes), 10, 64) | |||||
| stored := false | |||||
| if err == nil { | |||||
| data, err := client.ReadLine() | |||||
| if err != nil || uint64(len(data)) != numBytes { | |||||
| _, err = client.ClientError([]byte("Value length does not match number of bytes send")) | |||||
| } else { | |||||
| experimentId, err := strconv.ParseUint(string(key), 10, 64) | |||||
| if err != nil || experimentId <= 0 { | |||||
| _, err = client.ClientError([]byte("SET key must be a positive integer")) | |||||
| } | |||||
| err = client.UpdateExperiment(experimentId, data) | |||||
| stored = err == nil | |||||
| } | |||||
| } | |||||
| if stored && noreply == false { | |||||
| _, err = client.SendStored() | |||||
| } else if stored == false && noreply == false { | |||||
| _, err = client.SendNotStored() | |||||
| } | |||||
| } | |||||
| return err | |||||
| } | |||||
| func HandleStats(args [][]byte, client ClientConnection) (err error) { | |||||
| client.ObtainRedisConnection() | |||||
| defer client.ReleaseRedisConnection() | |||||
| if len(args) == 0 { | |||||
| args, err = client.GetActiveExperimentIds() | |||||
| } | |||||
| for _, rawId := range args { | |||||
| id, err := strconv.ParseUint(string(rawId), 10, 64) | |||||
| if err != nil { | |||||
| continue | |||||
| } | |||||
| err = client.GetExperimentStats(id) | |||||
| if err != nil { | |||||
| break | |||||
| } | |||||
| } | |||||
| _, err = client.SendEnd() | |||||
| return err | |||||
| } | |||||
| func HandleDelete(args [][]byte, client ClientConnection) (err error) { | |||||
| client.ObtainRedisConnection() | |||||
| defer client.ReleaseRedisConnection() | |||||
| if len(args) < 1 { | |||||
| _, err = client.ClientError([]byte("DELETE command takes 1 argument <key>")) | |||||
| } else { | |||||
| id, err := strconv.ParseUint(string(args[0]), 10, 64) | |||||
| if err == nil { | |||||
| err = client.DeactivateExperiment(id) | |||||
| if err == nil { | |||||
| _, err = client.SendDeleted() | |||||
| } else { | |||||
| _, err = client.SendNotFound() | |||||
| } | |||||
| } else { | |||||
| _, err = client.SendNotFound() | |||||
| } | |||||
| } | |||||
| return err | |||||
| } | |||||
| func HandleTouch(args [][]byte, client ClientConnection) (err error) { | |||||
| client.ObtainRedisConnection() | |||||
| defer client.ReleaseRedisConnection() | |||||
| if len(args) < 2 { | |||||
| _, err = client.ClientError([]byte("TOUCH command takes at least 2 arguments <key> <exptime> [noreply]")) | |||||
| } else { | |||||
| id, err := strconv.ParseUint(string(args[0]), 10, 64) | |||||
| noreply := (len(args) >= 3 && bytes.Equal(bytes.ToLower(args[2]), []byte("noreply"))) | |||||
| if err == nil { | |||||
| err = client.ActivateExperiment(id) | |||||
| if err == nil && noreply == false { | |||||
| _, err = client.SendTouched() | |||||
| } else if noreply == false { | |||||
| _, err = client.SendNotFound() | |||||
| } | |||||
| } else if noreply == false { | |||||
| _, err = client.SendNotFound() | |||||
| } | |||||
| } | |||||
| return err | |||||
| } | |||||