diff --git a/examples/g-counter/g_counter.go b/examples/g-counter/g_counter.go new file mode 100644 index 0000000..51d7d3b --- /dev/null +++ b/examples/g-counter/g_counter.go @@ -0,0 +1,47 @@ +package main + +import ( + "encoding/json" + "sort" +) + +type gcounter struct { + id int + counts map[int]int +} + +func (c gcounter) incr() { c.counts[c.id]++ } + +func (c gcounter) total() int { + var n int + for _, count := range c.counts { + n += count + } + return n +} + +func (c gcounter) merge(other gcounter) { + for id, count := range other.counts { + if c.[id] < count { + c.[id] = count + } + } +} + +type pair [2]int + +func (c gcounter) MarshalJSON() ([]byte, error) { + pairs := make([]pair, 0, len(c.counts)) + for id, count := range c.counts { + pairs = append(pairs, pair{id, count}) + } + sort.Slice(pairs, func(i, j int) bool { return pairs[i][0] < pairs[j][0] }) + return json.Marshal(pairs) +} + +func (c *gcounter) UnmarshalJSON(b []byte) error { + pairs := make([]pair, 0, len(c.counts)) + if err := json.Unmarshal(b, &pairs); err != nil { + return err + } +} diff --git a/examples/g-counter/g_counter_test.go b/examples/g-counter/g_counter_test.go new file mode 100644 index 0000000..bdb752a --- /dev/null +++ b/examples/g-counter/g_counter_test.go @@ -0,0 +1,71 @@ +package main + +import ( + "encoding/json" + "testing" +) + +func TestGCounterJSON(t *testing.T) { + t.Run("marshal", func(t *testing.T) { + type test struct { + counter gcounter + expect string + } + + tests := []test{ + { + gcounter{1, map[int]int{1: 5, 3: 8}}, + `[[1,5],[3,8]]`, + }, + { + gcounter{8, map[int]int{3: 10, 7: 16, 12: 9, 2: 37}}, + `[[2,37],[3,10],[7,16],[12,9]]`, + }, + } + + for _, test := range tests { + b, err := json.Marshal(test.counter) + if err != nil { + t.Errorf("marshal failed: %s", err) + continue + } + s := string(b) + if s != test.expect { + t.Errorf("expected json: %s received: %s", test.expect, s) + } + } + }) + + t.Run("unmarshal", func(t *testing.T) { + type test struct { + in string + expect gcounter + } + + tests := []test{ + { + `[[1,5],[3,8]]`, + gcounter{1, map[int]int{1: 5, 3: 8}}, + }, + { + `[[2,37],[3,10],[7,16],[12,9]]`, + gcounter{8, map[int]int{3: 10, 7: 16, 12: 9, 2: 37}}, + }, + } + + for _, test := range tests { + var c gcounter + if err := json.Unmarshal(test.in, &c); err != nil { + t.Errorf("unmarshal failed: %s", err) + continue + } + + for id, count := range c.counts { + n := test.expect.counts[id] + if n != count { + t.Errorf("mismatched counts for id %d: expected %d, saw %d", id, count, n) + } + } + } + }) +} diff --git a/examples/g-counter/main.go b/examples/g-counter/main.go new file mode 100644 index 0000000..06ab7d0 --- /dev/null +++ b/examples/g-counter/main.go @@ -0,0 +1 @@ +package main diff --git a/examples/g-counter/server.go b/examples/g-counter/server.go new file mode 100644 index 0000000..37130b8 --- /dev/null +++ b/examples/g-counter/server.go @@ -0,0 +1,78 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "sync" +) + +type response struct { + OK bool `json:"ok"` + Hits int `json:"hits"` +} + +// server implements a clustered http hit-counter server. Each path is given a +// g-counter, and every node in the cluster acts as a read-write replica. +type server struct { + sync.Mutex + + // my own id + id int + + // a mapping of peer servers id -> addr + peers map[int]string + + // distributed counts + counters map[string]gcounter +} + +func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/join": + s.join(w, r) + case "/sync": + s.sync(w, r) + default: + s.countHit(w, r) + } +} + +func (s *server) countHit(w http.ResponseWriter, r *http.Request) { + hits := s.hit(r.URL.Path) + + fmt.Printf("% 8d %s\n", hits, r.URL.Path) + + w.Header().Add("Content-Type", "application/json") + json.NewEncoder(w).Encode(response{OK: true, Hits: hits}) +} + +func (s *server) join(w http.ResponseWriter, r *http.Request) { + var body struct { + Addr string `json:"addr"` + } +} + +func (s *server) sync(w http.ResponseWriter, r *http.Request) { +} + +func (s *server) hit(path string) int { + s.Lock() + defer s.Unlock() + + if s.counters == nil { + s.counters = map[string]gcounter{ + gcounter{s.id, map[int]int{id: 1}}, + } + return 1 + } + + c, ok := s.counters[path] + if !ok { + s.counters[path] = gcounter{s.id, map[int]int{id: 1}} + return 1 + } + + c.incr() + return c.total() +} diff --git a/examples/hit-counter/server_test.go b/examples/hit-counter/server_test.go index 5204482..c8ac477 100644 --- a/examples/hit-counter/server_test.go +++ b/examples/hit-counter/server_test.go @@ -59,7 +59,7 @@ func TestServer(t *testing.T) { return node } - runParallel := func(node *tea.Tree, tests list) { + runSiblings := func(node *tea.Tree, tests list) { for i := 0; i < len(tests); i++ { node.Child(&tests[i]) } @@ -82,7 +82,7 @@ func TestServer(t *testing.T) { {path: "/users/bob", expect: 2}, }) - runParallel(root, list{ + runSiblings(root, list{ {path: "/users/alice", expect: 1}, {path: "/users/alice", expect: 1}, {path: "/users/alice", expect: 1},