feature
2015-03-16
0:caad72abc05a tip Browse Files
First pass at an implementation. Implement the calculation to determine whether a string is included in the partition or not, using crc32 to coerce the string to an evenly distributed uint32, and a modulo to turn it into a percentage. Implement a store to keep track of available flags and the level of the partition for each flag. Implement an API to retrieve, create, and modify these available flags.
flag.go server/endpoints.go server/store.go
1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/flag.go Mon Mar 16 22:45:58 2015 -0400 1.3 @@ -0,0 +1,52 @@ 1.4 +package feature 1.5 + 1.6 +import ( 1.7 + "hash/crc32" 1.8 + "log" 1.9 +) 1.10 + 1.11 +type Flag struct { 1.12 + ID string 1.13 + Limit int 1.14 +} 1.15 + 1.16 +func (f Flag) calcOffset() int { 1.17 + return calcValue(f.ID) 1.18 +} 1.19 + 1.20 +func (f Flag) Permit(candidate string) bool { 1.21 + if f.Limit >= 100 { 1.22 + return true 1.23 + } 1.24 + if f.Limit <= 0 { 1.25 + return false 1.26 + } 1.27 + return (calcValue(candidate)+f.calcOffset())%100 <= f.Limit 1.28 +} 1.29 + 1.30 +type FlagSet struct { 1.31 + flags map[string]bool 1.32 +} 1.33 + 1.34 +func (set FlagSet) Check(flag string) bool { 1.35 + pass, ok := set.flags[flag] 1.36 + if !ok { 1.37 + log.Println("Checked for flag that wasn't in response:", flag) 1.38 + pass = false 1.39 + } 1.40 + return pass 1.41 +} 1.42 + 1.43 +func calcValue(in string) int { 1.44 + return int(crc32.ChecksumIEEE([]byte(in)) % 100) 1.45 +} 1.46 + 1.47 +/* 1.48 + 1.49 +Client has one function: 1.50 + 1.51 +1. func (c Client) Load(in string) FlagSet -- loads the flagset with "in" as the parameter to gate on 1.52 + 1.53 +// TODO Have the client cache FlagSets in memory by the parameter being gated on; we can reuse FlagSets for a minute or two 1.54 + 1.55 +*/
2.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 2.2 +++ b/server/endpoints.go Mon Mar 16 22:45:58 2015 -0400 2.3 @@ -0,0 +1,187 @@ 2.4 +package server 2.5 + 2.6 +import ( 2.7 + "encoding/json" 2.8 + "fmt" 2.9 + "log" 2.10 + "net/http" 2.11 + "strings" 2.12 + 2.13 + "bitbucket.org/ww/goautoneg" 2.14 + "code.secondbit.org/feature.hg" 2.15 +) 2.16 + 2.17 +const ( 2.18 + ErrMissing = "missing" 2.19 + ErrNotFound = "not_found" 2.20 + ErrConflict = "conflict" 2.21 + ErrActOfGod = "act_of_god" 2.22 +) 2.23 + 2.24 +var ( 2.25 + encoders = []string{"application/json"} 2.26 +) 2.27 + 2.28 +type flagCheck struct { 2.29 + ID string `json:"id,omitempty"` 2.30 + Passes bool `json:"pass,omitempty"` 2.31 +} 2.32 + 2.33 +type response struct { 2.34 + status int 2.35 + contentType string 2.36 + Checks []flagCheck `json:"checks,omitempty"` 2.37 + Flags []feature.Flag `json:"flags,omitempty"` 2.38 + Errors []respError `json:"errors,omitempty"` 2.39 +} 2.40 + 2.41 +func (r response) addErrors(e ...respError) { 2.42 + if r.Errors == nil { 2.43 + r.Errors = []respError{} 2.44 + } 2.45 + r.Errors = append(r.Errors, e...) 2.46 +} 2.47 + 2.48 +func (r response) write(w http.ResponseWriter) { 2.49 + w.Header().Set("Content-Type", r.contentType) 2.50 + if r.status == 0 { 2.51 + r.status = 200 2.52 + } 2.53 + w.WriteHeader(r.status) 2.54 + var err error 2.55 + switch r.contentType { 2.56 + case "application/json": 2.57 + enc := json.NewEncoder(w) 2.58 + err = enc.Encode(r) 2.59 + default: 2.60 + _, err = w.Write([]byte(strings.Join(encoders, "\n") + "\n")) 2.61 + } 2.62 + if err != nil { 2.63 + log.Printf("Error writing response: %+v\n", err) 2.64 + } 2.65 +} 2.66 + 2.67 +func responseFromRequest(r *http.Request) response { 2.68 + resp := responseFromRequest(r) 2.69 + if r.Header.Get("Accept") != "" { 2.70 + resp.contentType = goautoneg.Negotiate(r.Header.Get("Accept"), encoders) 2.71 + if resp.contentType == "" { 2.72 + resp.status = http.StatusNotAcceptable 2.73 + } 2.74 + } else { 2.75 + resp.contentType = "application/json" 2.76 + } 2.77 + return resp 2.78 +} 2.79 + 2.80 +type respError struct { 2.81 + Code string `json:"code"` 2.82 + Param string `json:"param,omitempty"` 2.83 + Field string `json:"field,omitempty"` 2.84 +} 2.85 + 2.86 +func FlagSetHandler(w http.ResponseWriter, r *http.Request, store flagStore) { 2.87 + resp := responseFromRequest(r) 2.88 + flags, err := store.list() 2.89 + if err != nil { 2.90 + switch err.code { 2.91 + default: 2.92 + log.Printf("Error listing flags: %+v\n", err) 2.93 + resp.status = http.StatusInternalServerError 2.94 + resp.addErrors(respError{Code: ErrActOfGod}) 2.95 + } 2.96 + resp.write(w) 2.97 + return 2.98 + } 2.99 + shard := r.URL.Query().Get("shard") 2.100 + if shard == "" { 2.101 + resp.Flags = flags 2.102 + resp.write(w) 2.103 + return 2.104 + } 2.105 + checks := make([]flagCheck, len(flags)) 2.106 + for pos, flag := range flags { 2.107 + checks[pos] = flagCheck{ID: flag.ID, Passes: flag.Permit(shard)} 2.108 + } 2.109 + resp.Checks = checks 2.110 + resp.write(w) 2.111 +} 2.112 + 2.113 +func CreateFlagHandler(w http.ResponseWriter, r *http.Request, store flagStore) { 2.114 + resp := responseFromRequest(r) 2.115 + var f feature.Flag 2.116 + // BUG(paddY): Need to unmarshal the request body 2.117 + if f.ID == "" { 2.118 + resp.status = http.StatusBadRequest 2.119 + resp.addErrors(respError{Code: ErrMissing, Field: "/id"}) 2.120 + resp.write(w) 2.121 + return 2.122 + } 2.123 + err := store.create(f) 2.124 + if err != nil { 2.125 + switch err.code { 2.126 + case errAlreadyExists: 2.127 + resp.status = http.StatusBadRequest 2.128 + resp.addErrors(respError{Code: ErrConflict, Field: "/id"}) 2.129 + default: 2.130 + log.Printf("Error creating flag: %+v\n", err) 2.131 + resp.status = http.StatusInternalServerError 2.132 + resp.addErrors(respError{Code: ErrActOfGod}) 2.133 + } 2.134 + resp.write(w) 2.135 + return 2.136 + } 2.137 + resp.Flags = []feature.Flag{f} 2.138 + resp.write(w) 2.139 +} 2.140 + 2.141 +func UpdateFlagHandler(w http.ResponseWriter, r *http.Request, store flagStore) { 2.142 + resp := responseFromRequest(r) 2.143 + var flags []feature.Flag 2.144 + // BUG(paddy): Need to unmarshal the request body 2.145 + for pos, f := range flags { 2.146 + if f.ID == "" { 2.147 + resp.addErrors(respError{Code: ErrMissing, Field: fmt.Sprintf("/id/%d", pos)}) 2.148 + resp.status = http.StatusBadRequest 2.149 + } 2.150 + } 2.151 + errs := store.update(flags) 2.152 + if errs != nil { 2.153 + for _, err := range errs { 2.154 + switch err.code { 2.155 + case errNotFound: 2.156 + resp.status = http.StatusNotFound 2.157 + resp.addErrors(respError{Code: ErrNotFound, Field: fmt.Sprintf("/id/%d", err.pos)}) 2.158 + default: 2.159 + log.Printf("Error updating flag: %+v\n", err) 2.160 + resp.status = http.StatusInternalServerError 2.161 + resp.addErrors(respError{Code: ErrActOfGod}) 2.162 + } 2.163 + } 2.164 + resp.write(w) 2.165 + return 2.166 + } 2.167 + resp.Flags = flags 2.168 + resp.write(w) 2.169 +} 2.170 + 2.171 +func DeleteFlagHandler(w http.ResponseWriter, r *http.Request, store flagStore) { 2.172 + resp := responseFromRequest(r) 2.173 + var id string 2.174 + // BUG(paddY): Need to pick a routing library and get the ID 2.175 + err := store.destroy(id) 2.176 + if err != nil { 2.177 + switch err.code { 2.178 + case ErrNotFound: 2.179 + resp.status = http.StatusNotFound 2.180 + resp.addErrors(respError{Code: ErrNotFound, Param: "id"}) 2.181 + default: 2.182 + resp.status = http.StatusInternalServerError 2.183 + resp.addErrors(respError{Code: ErrActOfGod}) 2.184 + } 2.185 + resp.write(w) 2.186 + return 2.187 + } 2.188 + resp.Flags = []feature.Flag{{ID: id}} 2.189 + resp.write(w) 2.190 +}
3.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 3.2 +++ b/server/store.go Mon Mar 16 22:45:58 2015 -0400 3.3 @@ -0,0 +1,88 @@ 3.4 +package server 3.5 + 3.6 +import ( 3.7 + "fmt" 3.8 + "sync" 3.9 + 3.10 + "code.secondbit.org/feature.hg" 3.11 +) 3.12 + 3.13 +const ( 3.14 + errNotFound = "flag not found" 3.15 + errAlreadyExists = "flag already exists" 3.16 +) 3.17 + 3.18 +type flagError struct { 3.19 + pos int 3.20 + code string 3.21 +} 3.22 + 3.23 +func (f *flagError) Error() string { 3.24 + return fmt.Sprintf("[%d]: %s", f.pos, f.code) 3.25 +} 3.26 + 3.27 +type flagStore interface { 3.28 + create(f feature.Flag) *flagError 3.29 + update(flags []feature.Flag) []flagError 3.30 + destroy(flag string) *flagError 3.31 + list() ([]feature.Flag, *flagError) 3.32 +} 3.33 + 3.34 +type memstore struct { 3.35 + flags map[string]feature.Flag 3.36 + flock sync.RWMutex 3.37 +} 3.38 + 3.39 +func (m *memstore) create(f feature.Flag) *flagError { 3.40 + m.flock.Lock() 3.41 + defer m.flock.Unlock() 3.42 + 3.43 + if _, ok := m.flags[f.ID]; ok { 3.44 + return &flagError{pos: 0, code: errNotFound} 3.45 + } 3.46 + m.flags[f.ID] = f 3.47 + return nil 3.48 +} 3.49 + 3.50 +func (m *memstore) update(flags []feature.Flag) []flagError { 3.51 + m.flock.Lock() 3.52 + defer m.flock.Unlock() 3.53 + 3.54 + errs := []flagError{} 3.55 + for pos, f := range flags { 3.56 + if _, ok := m.flags[f.ID]; !ok { 3.57 + errs = append(errs, flagError{pos: pos, code: errNotFound}) 3.58 + } 3.59 + } 3.60 + if len(errs) > 0 { 3.61 + return errs 3.62 + } 3.63 + for _, flag := range flags { 3.64 + m.flags[flag.ID] = flag 3.65 + } 3.66 + return nil 3.67 +} 3.68 + 3.69 +func (m *memstore) destroy(flag string) *flagError { 3.70 + m.flock.Lock() 3.71 + defer m.flock.Unlock() 3.72 + 3.73 + if _, ok := m.flags[flag]; !ok { 3.74 + return &flagError{pos: 0, code: errNotFound} 3.75 + } 3.76 + delete(m.flags, flag) 3.77 + return nil 3.78 +} 3.79 + 3.80 +func (m *memstore) list() ([]feature.Flag, *flagError) { 3.81 + m.flock.RLock() 3.82 + defer m.flock.RUnlock() 3.83 + 3.84 + flags := make([]feature.Flag, len(m.flags)) 3.85 + pos := 0 3.86 + for _, flag := range m.flags { 3.87 + flags[pos] = flag 3.88 + pos = pos + 1 3.89 + } 3.90 + return flags, nil 3.91 +}