feature

Paddy 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 +}