Compare commits

..

1 Commits

Author SHA1 Message Date
Jordan Orelli 51c22cfb94 working on a g-counter example 4 years ago

@ -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
}
}

@ -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)
}
}
}
})
}

@ -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()
}

@ -8,9 +8,6 @@ import (
"github.com/jordanorelli/tea" "github.com/jordanorelli/tea"
) )
// testStartServer is a test that checks that the server can start. If this
// test passes, we retain a reference to the server for future tests to
// utilize.
type testStartServer struct { type testStartServer struct {
Server *httptest.Server `tea:"save"` Server *httptest.Server `tea:"save"`
} }
@ -26,16 +23,14 @@ func (test *testStartServer) After(t *testing.T) {
test.Server.Close() test.Server.Close()
} }
// testHits sends a request to a hitcount server created in a previous test, type testRequest struct {
// checking that the number of hits returned matches what we expect.
type testHits struct {
Server *httptest.Server `tea:"load"` Server *httptest.Server `tea:"load"`
path string path string
hits int expect int
} }
func (test *testHits) Run(t *testing.T) { func (test *testRequest) Run(t *testing.T) {
client := test.Server.Client() client := test.Server.Client()
res, err := client.Get(test.Server.URL + test.path) res, err := client.Get(test.Server.URL + test.path)
@ -49,39 +44,50 @@ func (test *testHits) Run(t *testing.T) {
t.Fatalf("response at %s was not json: %v", test.path, err) t.Fatalf("response at %s was not json: %v", test.path, err)
} }
if body.Hits != test.hits { if body.Hits != test.expect {
t.Errorf("expected a count of %d hits but saw %d instead", test.hits, body.Hits) t.Errorf("expected a count of %d but saw %d", test.expect, body.Hits)
} }
} }
func TestServer(t *testing.T) { func TestServer(t *testing.T) {
// start with a root node that creates our test server type list []testRequest
root := tea.New(&testStartServer{})
// add a child node: this test is run if the root test passes. If the root
// test is failed, this test and all of its descendents are logged as
// skipped.
one := root.Child(&testHits{path: "/alice", hits: 1})
// the effects of the first test create the initial state for the second test. runSeries := func(node *tea.Tree, tests list) *tea.Tree {
two := one.Child(&testHits{path: "/alice", hits: 2}) for i := 0; i < len(tests); i++ {
node = node.Child(&tests[i])
// since we have never visited /bob, we know that bob should only have one hit. }
two.Child(&testHits{path: "/bob", hits: 1}) return node
}
// but we could also run the exact same test off of the root, like so: runSiblings := func(node *tea.Tree, tests list) {
root.Child(&testHits{path: "/bob", hits: 1}) for i := 0; i < len(tests); i++ {
node.Child(&tests[i])
}
}
// since tests are values in tea, we can re-use the exact same test from root := tea.New(&testStartServer{})
// different initial states by saving the test as a variable.
bob := &testHits{path: "/bob", hits: 1}
// these two executions of the same test value are operating on different runSeries(root, list{
// program states. Since they are not in the same sequence, they have no {path: "/users/alice", expect: 1},
// effect on one another, even though they're utilizing the same test {path: "/users/alice", expect: 2},
// value. {path: "/users/alice", expect: 3},
two.Child(bob) {path: "/users/alice", expect: 4},
root.Child(bob) })
runSeries(root, list{
{path: "/users/bob", expect: 1},
{path: "/users/alice", expect: 1},
{path: "/users/alice", expect: 2},
{path: "/users/alice", expect: 3},
{path: "/users/bob", expect: 2},
})
runSiblings(root, list{
{path: "/users/alice", expect: 1},
{path: "/users/alice", expect: 1},
{path: "/users/alice", expect: 1},
{path: "/users/alice", expect: 1},
})
tea.Run(t, root) tea.Run(t, root)
} }

Loading…
Cancel
Save