├── doc ├── Makefile ├── doozer.png ├── webview.png ├── unix-doozer.jpg ├── files.md ├── data-model.md ├── uri.md ├── hacking.md ├── proto-examples.md ├── firedrill.md ├── doozerd.1.ronn └── proto.md ├── bin ├── rungodoc ├── light ├── test-cluster └── doozer_init ├── all.sh ├── peer ├── version.go ├── misc_test.go ├── liveness.go ├── liveness_test.go ├── bench_test.go ├── peer.go └── peer_test.go ├── web ├── web_test.go ├── stats.html ├── stats.html.go ├── file2gostring ├── main.html.go ├── main.html ├── main.css.go ├── main.css ├── web.go ├── main.js.go └── main.js ├── test ├── test_test.go └── test.go ├── quiet ├── quiet_test.go └── quiet.go ├── .gitignore ├── consensus ├── Makefile ├── m.go ├── m.proto ├── consensus.go ├── acceptor.go ├── learner.go ├── coordinator.go ├── run.go ├── m_test.go ├── acceptor_test.go ├── m.pb.go ├── consensus_test.go ├── run_test.go ├── manager.go ├── learner_test.go ├── coordinator_test.go └── manager_test.go ├── gc ├── clean.go ├── pulse.go ├── pulse_test.go └── clean_test.go ├── server ├── Makefile ├── conn_test.go ├── msg.proto ├── server.go ├── conn.go ├── server_test.go ├── txn.go └── msg.pb.go ├── dist.sh ├── make.sh ├── .travis.yml ├── store ├── event_test.go ├── bench_test.go ├── event.go ├── getter.go ├── glob.go ├── glob_test.go ├── node_test.go ├── getter_test.go ├── node.go └── store.go ├── LICENSE ├── member ├── member.go └── member_test.go ├── doozerd.go ├── boot.go └── README.md /doc/Makefile: -------------------------------------------------------------------------------- 1 | doc: 2 | ronn *.ronn 3 | -------------------------------------------------------------------------------- /bin/rungodoc: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | godoc -path . -http :6060 3 | -------------------------------------------------------------------------------- /all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | sh make.sh 4 | go test -v ./... 5 | -------------------------------------------------------------------------------- /peer/version.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | const Version = `0.9.0-alpha` 4 | -------------------------------------------------------------------------------- /doc/doozer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/doozerd/master/doc/doozer.png -------------------------------------------------------------------------------- /doc/webview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/doozerd/master/doc/webview.png -------------------------------------------------------------------------------- /doc/unix-doozer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hean01/doozerd/master/doc/unix-doozer.jpg -------------------------------------------------------------------------------- /web/web_test.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import "testing" 4 | 5 | func TestFoo(t *testing.T) { 6 | } 7 | -------------------------------------------------------------------------------- /test/test_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import "testing" 4 | 5 | func TestFoo(t *testing.T) { 6 | } 7 | -------------------------------------------------------------------------------- /quiet/quiet_test.go: -------------------------------------------------------------------------------- 1 | package quiet 2 | 3 | import "testing" 4 | 5 | func TestNothing(t *testing.T) { 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *-test.sh 2 | *.DS_Store 3 | *.[568] 4 | *.log 5 | *.out 6 | *.prof 7 | .roundup.* 8 | doc/*.html 9 | doc/*.[0-9] 10 | -------------------------------------------------------------------------------- /quiet/quiet.go: -------------------------------------------------------------------------------- 1 | package quiet 2 | 3 | import ( 4 | "log" 5 | "io/ioutil" 6 | ) 7 | 8 | func init() { 9 | log.SetOutput(ioutil.Discard) 10 | } 11 | -------------------------------------------------------------------------------- /consensus/Makefile: -------------------------------------------------------------------------------- 1 | m.pb.go: m.proto 2 | mkdir -p _pb 3 | protoc --go_out=_pb $< 4 | cat _pb/$@\ 5 | |sed s/Msg/msg/g\ 6 | |sed s/Newmsg/newMsg/g\ 7 | |gofmt >$@ 8 | rm -rf _pb 9 | -------------------------------------------------------------------------------- /bin/light: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | exec 2>&1 6 | 7 | n=$1 8 | test $# -eq 0 && n=1 9 | 10 | p=`expr 8040 + $n` 11 | w=`expr 8080 + $n` 12 | 13 | test $n -ne 1 && args="-b doozer:?ca=127.0.0.1:8041" 14 | 15 | exec doozerd -l 127.0.0.1:$p -w :$w $args $* 16 | -------------------------------------------------------------------------------- /gc/clean.go: -------------------------------------------------------------------------------- 1 | package gc 2 | 3 | import ( 4 | "github.com/ha/doozerd/store" 5 | "time" 6 | ) 7 | 8 | func Clean(st *store.Store, keep int64, ticker <-chan time.Time) { 9 | for _ = range ticker { 10 | last := (<-st.Seqns) - keep 11 | st.Clean(last) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /server/Makefile: -------------------------------------------------------------------------------- 1 | msg.pb.go: msg.proto 2 | mkdir -p _pb 3 | protoc --go_out=_pb $< 4 | cat _pb/$@\ 5 | |sed s/Request/request/g\ 6 | |sed s/Response/response/g\ 7 | |sed s/Newrequest/newRequest/g\ 8 | |sed s/Newresponse/newResponse/g\ 9 | >$@ 10 | rm -rf _pb 11 | -------------------------------------------------------------------------------- /web/stats.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 |
6 | Alloc 7 | TotalAlloc 8 |
11 | {Alloc} 12 | {TotalAlloc} 13 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /dist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | eval `gomake -f Make.inc go-env` 4 | ./clean.sh 5 | ./all.sh 6 | base=`./cmd/doozerd/doozerd -v|tr ' ' -` 7 | file=$base-$GOOS-$GOARCH.tar 8 | trap "rm -rf $base $file" 0 9 | mkdir $base 10 | cp cmd/doozerd/doozerd $base 11 | cp ../README.md $base/README.md 12 | tar cf $file $base 13 | gzip -9 $file 14 | -------------------------------------------------------------------------------- /make.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | PKG=web 5 | 6 | GOFILES=" 7 | main.css.go 8 | main.html.go 9 | stats.html.go 10 | main.js.go 11 | " 12 | 13 | for f in $GOFILES 14 | do 15 | b="web/$(basename $f .go)" 16 | ./web/file2gostring $PKG $b < $b > web/$f.part 17 | mv web/$f.part web/$f 18 | done 19 | 20 | go get -d ./... 21 | go install 22 | -------------------------------------------------------------------------------- /web/stats.html.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | // This file was generated from web/stats.html. 4 | 5 | var stats_html string = "\n \n \n \n \n \n \n
\n Alloc\n TotalAlloc\n
\n {Alloc}\n {TotalAlloc}\n
\n \n\n" 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | install: 3 | - go get github.com/kr/pretty 4 | - go get github.com/bmizerany/assert 5 | - go get code.google.com/p/go.net/websocket 6 | - go get code.google.com/p/goprotobuf/proto 7 | - go get github.com/ha/doozer 8 | script: 9 | - pushd $TRAVIS_BUILD_DIR 10 | - ./all.sh 11 | - popd 12 | notifications: 13 | email: false 14 | -------------------------------------------------------------------------------- /doc/files.md: -------------------------------------------------------------------------------- 1 | # Doozer's Files 2 | 3 | These follow a simple rule: doozer reserves the right to read and write 4 | paths in `/ctl`, and the details of those paths will be documented; 5 | it will never read or write other paths unless explicitly asked to. 6 | 7 | /ctl/cal CAL slots 8 | /ctl/err mutation errors are written here 9 | /ctl/node node metadata 10 | -------------------------------------------------------------------------------- /consensus/m.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | var ( 4 | nop = msg_NOP.Enum() 5 | invite = msg_INVITE.Enum() 6 | rsvp = msg_RSVP.Enum() 7 | nominate = msg_NOMINATE.Enum() 8 | vote = msg_VOTE.Enum() 9 | tick = msg_TICK.Enum() 10 | propose = msg_PROPOSE.Enum() 11 | learn = msg_LEARN.Enum() 12 | ) 13 | 14 | const nmsg = 8 15 | 16 | var ( 17 | msgTick = &msg{Cmd: tick} 18 | ) 19 | -------------------------------------------------------------------------------- /consensus/m.proto: -------------------------------------------------------------------------------- 1 | package consensus; 2 | 3 | message Msg { 4 | enum Cmd { 5 | NOP = 0; 6 | INVITE = 1; 7 | RSVP = 2; 8 | NOMINATE = 3; 9 | VOTE = 4; 10 | TICK = 5; 11 | PROPOSE = 6; 12 | LEARN = 7; 13 | } 14 | 15 | optional Cmd cmd = 1; 16 | optional int64 seqn = 2; 17 | 18 | optional int64 crnd = 3; 19 | optional int64 vrnd = 4; 20 | optional bytes value = 5; 21 | } 22 | -------------------------------------------------------------------------------- /web/file2gostring: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | munge() { 6 | printf %s "$1" | tr . _ | tr -d -c '[:alnum:]_' 7 | } 8 | 9 | quote() { 10 | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | sed 's/$/\\n/' | tr -d '\n' 11 | } 12 | 13 | pkg_path=$1 ; shift 14 | file=$1 ; shift 15 | 16 | pkg=`basename $pkg_path` 17 | 18 | printf 'package %s\n' "$pkg" 19 | printf '\n' 20 | printf '// This file was generated from %s.\n' "$file" 21 | printf '\n' 22 | printf 'var ' 23 | munge "`basename $file`" 24 | printf ' string = "' 25 | quote 26 | printf '"\n' 27 | -------------------------------------------------------------------------------- /gc/pulse.go: -------------------------------------------------------------------------------- 1 | package gc 2 | 3 | import ( 4 | "github.com/ha/doozerd/consensus" 5 | "github.com/ha/doozerd/store" 6 | "log" 7 | "strconv" 8 | "time" 9 | ) 10 | 11 | func Pulse(node string, seqns <-chan int64, p consensus.Proposer, sleep int64) { 12 | path := "/ctl/node/" + node + "/applied" 13 | for { 14 | seqn, ok := <-seqns 15 | if !ok { 16 | break 17 | } 18 | 19 | e := consensus.Set(p, path, []byte(strconv.FormatInt(seqn, 10)), store.Clobber) 20 | if e.Err != nil { 21 | log.Println(e.Err) 22 | } 23 | 24 | time.Sleep(time.Duration(sleep)) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /consensus/consensus.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | import ( 4 | "github.com/ha/doozerd/store" 5 | ) 6 | 7 | type Proposer interface { 8 | Propose(v []byte) store.Event 9 | } 10 | 11 | func Set(p Proposer, path string, body []byte, rev int64) (e store.Event) { 12 | e.Mut, e.Err = store.EncodeSet(path, string(body), rev) 13 | if e.Err != nil { 14 | return 15 | } 16 | 17 | return p.Propose([]byte(e.Mut)) 18 | } 19 | 20 | func Del(p Proposer, path string, rev int64) (e store.Event) { 21 | e.Mut, e.Err = store.EncodeDel(path, rev) 22 | if e.Err != nil { 23 | return 24 | } 25 | 26 | return p.Propose([]byte(e.Mut)) 27 | } 28 | -------------------------------------------------------------------------------- /gc/pulse_test.go: -------------------------------------------------------------------------------- 1 | package gc 2 | 3 | import ( 4 | "github.com/bmizerany/assert" 5 | "github.com/ha/doozerd/store" 6 | "testing" 7 | ) 8 | 9 | // Testing 10 | 11 | type FakeProposer chan string 12 | 13 | func (fs FakeProposer) Propose(v []byte) (e store.Event) { 14 | fs <- string(v) 15 | e.Rev = 123 16 | return 17 | } 18 | 19 | func TestGcPulse(t *testing.T) { 20 | seqns := make(chan int64) 21 | defer close(seqns) 22 | fs := make(FakeProposer) 23 | 24 | go Pulse("test", seqns, fs, 1) 25 | 26 | seqns <- 0 27 | assert.Equal(t, "-1:/ctl/node/test/applied=0", <-fs) 28 | 29 | seqns <- 1 30 | assert.Equal(t, "-1:/ctl/node/test/applied=1", <-fs) 31 | } 32 | -------------------------------------------------------------------------------- /test/test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/ha/doozerd/store" 5 | "io" 6 | "sync/atomic" 7 | ) 8 | 9 | type FakeProposer struct { 10 | *store.Store 11 | seqn int64 12 | } 13 | 14 | func (fp *FakeProposer) Propose(v []byte) store.Event { 15 | n := atomic.AddInt64(&fp.seqn, 1) 16 | 17 | ch, err := fp.Wait(store.Any, n) 18 | if err != nil { 19 | panic(err) 20 | } 21 | fp.Ops <- store.Op{n, string(v)} 22 | return <-ch 23 | } 24 | 25 | // An io.Writer that will return os.EOF on the `n`th byte written 26 | type ErrWriter struct { 27 | N int 28 | } 29 | 30 | func (e *ErrWriter) Write(p []byte) (n int, err error) { 31 | l := len(p) 32 | e.N -= l 33 | if e.N <= 0 { 34 | return 0, io.EOF 35 | } 36 | return l, nil 37 | } 38 | -------------------------------------------------------------------------------- /gc/clean_test.go: -------------------------------------------------------------------------------- 1 | package gc 2 | 3 | import ( 4 | "github.com/bmizerany/assert" 5 | "github.com/ha/doozerd/store" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestGcClean(t *testing.T) { 11 | st := store.New() 12 | defer close(st.Ops) 13 | 14 | ticker := make(chan time.Time) 15 | defer close(ticker) 16 | 17 | go Clean(st, 3, ticker) 18 | 19 | st.Ops <- store.Op{1, store.Nop} 20 | st.Ops <- store.Op{2, store.Nop} 21 | st.Ops <- store.Op{3, store.Nop} 22 | st.Ops <- store.Op{4, store.Nop} 23 | 24 | _, err := st.Wait(store.Any, 1) 25 | assert.Equal(t, nil, err) 26 | ticker <- time.Unix(0, 1) 27 | ticker <- time.Unix(0, 1) // Extra tick to ensure the last st.Clean has completed 28 | _, err = st.Wait(store.Any, 1) 29 | assert.Equal(t, store.ErrTooLate, err) 30 | } 31 | -------------------------------------------------------------------------------- /store/event_test.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "github.com/bmizerany/assert" 5 | "testing" 6 | ) 7 | 8 | func TestEventIsSet(t *testing.T) { 9 | p, v := "/x", "a" 10 | m := MustEncodeSet(p, v, Clobber) 11 | ev := Event{1, p, v, 1, m, nil, nil} 12 | assert.Equal(t, true, ev.IsSet()) 13 | assert.Equal(t, false, ev.IsDel()) 14 | assert.Equal(t, false, ev.IsNop()) 15 | } 16 | 17 | func TestEventIsDel(t *testing.T) { 18 | p := "/x" 19 | m := MustEncodeDel(p, Clobber) 20 | ev := Event{1, p, "", Missing, m, nil, nil} 21 | assert.Equal(t, true, ev.IsDel()) 22 | assert.Equal(t, false, ev.IsSet()) 23 | assert.Equal(t, false, ev.IsNop()) 24 | } 25 | 26 | func TestEventIsDummy(t *testing.T) { 27 | ev := Event{Seqn: 1, Rev: nop} 28 | assert.Equal(t, true, ev.IsNop()) 29 | assert.Equal(t, false, ev.IsSet()) 30 | assert.Equal(t, false, ev.IsDel()) 31 | } 32 | -------------------------------------------------------------------------------- /consensus/acceptor.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | type acceptor struct { 4 | rnd, vrnd int64 5 | vval string 6 | } 7 | 8 | func (ac *acceptor) update(m *msg) *msg { 9 | switch *m.Cmd { 10 | case msg_INVITE: 11 | if m.Crnd == nil { 12 | break 13 | } 14 | 15 | i := *m.Crnd 16 | 17 | if i > ac.rnd { 18 | ac.rnd = i 19 | 20 | return &msg{ 21 | Cmd: rsvp, 22 | Crnd: &i, 23 | Vrnd: &ac.vrnd, 24 | Value: []byte(ac.vval), 25 | } 26 | } 27 | case msg_NOMINATE: 28 | if m.Crnd == nil { 29 | break 30 | } 31 | 32 | i, v := *m.Crnd, m.Value 33 | 34 | // SUPER IMPT MAD PAXOS 35 | if i >= ac.rnd && i != ac.vrnd { 36 | ac.rnd = i 37 | ac.vrnd = i 38 | ac.vval = string(v) 39 | 40 | broadcast := &msg{ 41 | Cmd: vote, 42 | Vrnd: &i, 43 | Value: []byte(ac.vval), 44 | } 45 | return broadcast 46 | } 47 | } 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /web/main.html.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | // This file was generated from web/main.html. 4 | 5 | var main_html string = "\n \n {{ .Name }} {{ .Path }} doozer viewer\n \n \n\n \n
\n loading\n \n \n [Try now]\n \n ...and, we're back!\n
\n\n
\n
{{ .Path }}
\n
\n
\n
\n
\n
\n\n \n \n \n \n\n" 6 | -------------------------------------------------------------------------------- /web/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ .Name }} {{ .Path }} doozer viewer 4 | 5 | 6 | 7 | 8 |
9 | loading 10 | 11 | 12 | [Try now] 13 | 14 | ...and, we're back! 15 |
16 | 17 |
18 |
{{ .Path }}
19 |
20 |
21 |
22 |
23 |
24 | 25 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /bin/test-cluster: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Members 6 | n=$1 7 | [ -z "$n" ] || shift 8 | [ -z "$n" ] && n=3 9 | 10 | # CALs 11 | c=$1 12 | [ -z "$c" ] || shift 13 | [ -z "$c" ] && c=0 14 | 15 | waitall() { 16 | while test $running -ge 1 17 | do 18 | wait 19 | running=$(expr $running - 1) 20 | done 21 | } 22 | 23 | quit() { 24 | killall doozerd 25 | waitall 26 | } 27 | 28 | trap quit INT 29 | 30 | running=0 31 | i=1 32 | while test $i -le $n 33 | do 34 | bin/light $i $* 2>&1 | sed "s/^/$i: /" & 35 | running=$(expr $running + 1) 36 | 37 | sleep 1 38 | i=$(expr $i + 1) 39 | done 40 | 41 | sk= 42 | test "$DOOZER_ROSECRET" && sk=$DOOZER_ROSECRET 43 | test "$DOOZER_RWSECRET" && sk=$DOOZER_RWSECRET 44 | 45 | i=2 46 | while test $i -le $c 47 | do 48 | true | doozer -a "doozer:?ca=127.0.0.1:8041&sk=$sk" set /ctl/cal/$i 0 49 | i=$(expr $i + 1) 50 | done 51 | 52 | waitall 53 | -------------------------------------------------------------------------------- /peer/misc_test.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | import ( 4 | "github.com/ha/doozer" 5 | _ "github.com/ha/doozerd/quiet" 6 | "net" 7 | ) 8 | 9 | func mustListen() net.Listener { 10 | l, err := net.Listen("tcp", "127.0.0.1:0") 11 | if err != nil { 12 | panic(err) 13 | } 14 | return l 15 | } 16 | 17 | func mustListenUDP(addr string) *net.UDPConn { 18 | uaddr, err := net.ResolveUDPAddr("udp", addr) 19 | if err != nil { 20 | panic(err) 21 | } 22 | c, err := net.ListenUDP("udp", uaddr) 23 | if err != nil { 24 | panic(err) 25 | } 26 | return c 27 | } 28 | 29 | func dial(addr string) *doozer.Conn { 30 | c, err := doozer.Dial(addr) 31 | if err != nil { 32 | panic(err) 33 | } 34 | return c 35 | } 36 | 37 | func waitFor(cl *doozer.Conn, path string) { 38 | var rev int64 39 | for { 40 | ev, err := cl.Wait(path, rev) 41 | if err != nil { 42 | panic(err) 43 | } 44 | if ev.IsSet() && len(ev.Body) > 0 { 45 | break 46 | } 47 | rev = ev.Rev + 1 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /store/bench_test.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func BenchmarkHist10(b *testing.B) { 8 | benchmarkNHist(10, b) 9 | } 10 | 11 | func BenchmarkHist1e3(b *testing.B) { 12 | benchmarkNHist(1e3, b) 13 | } 14 | 15 | func BenchmarkHist1e4(b *testing.B) { 16 | benchmarkNHist(1e4, b) 17 | } 18 | 19 | func BenchmarkHist1e5(b *testing.B) { 20 | benchmarkNHist(1e5, b) 21 | } 22 | 23 | func BenchmarkHist1e6(b *testing.B) { 24 | benchmarkNHist(1e6, b) 25 | } 26 | 27 | func benchmarkNHist(n int, b *testing.B) { 28 | b.StopTimer() 29 | st := New() 30 | mut := [...]string{ 31 | MustEncodeSet("/test/path/one/foo", "12345", Clobber), 32 | MustEncodeSet("/test/path/two/foo", "23456", Clobber), 33 | MustEncodeSet("/test/path/three/foo", "34567", Clobber), 34 | } 35 | for i := 0; i < n; i++ { 36 | st.Ops <- Op{int64(i), mut[i%len(mut)]} 37 | } 38 | b.StartTimer() 39 | for i := 0; i < b.N; i++ { 40 | st.Ops <- Op{int64(n + i), mut[i%len(mut)]} 41 | st.Clean(int64(i)) 42 | } 43 | b.StopTimer() 44 | close(st.Ops) 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010 Blake Mizerany, Keith Rarick 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /store/event.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | type Event struct { 4 | Seqn int64 5 | Path string 6 | Body string 7 | 8 | // the revision for `Path` as of this event. 0 for a delete event. 9 | // undefined if the event does not represent a path operation. 10 | Rev int64 11 | 12 | // the mutation that caused this event 13 | Mut string 14 | 15 | Err error 16 | 17 | // retrieves values as defined at `Seqn` 18 | Getter 19 | } 20 | 21 | func (e Event) Desc() string { 22 | switch { 23 | case e.IsSet(): 24 | return "set" 25 | case e.IsDel(): 26 | return "del" 27 | case e.IsNop(): 28 | return "nop" 29 | } 30 | panic("unreachable") 31 | } 32 | 33 | // Returns true iff the operation represented by `e` set a path. 34 | // 35 | // Mutually exclusive with `IsDel` and `IsNop`. 36 | func (e Event) IsSet() bool { 37 | return e.Rev > Missing 38 | } 39 | 40 | // Returns true iff the operation represented by `e` deleted a path. 41 | // 42 | // Mutually exclusive with `IsSet` and `IsNop`. 43 | func (e Event) IsDel() bool { 44 | return e.Rev == Missing 45 | } 46 | 47 | // Returns true iff `e` does not represent a path operation. 48 | // 49 | // Mutually exclusive with `IsSet` and `IsDel`. 50 | func (e Event) IsNop() bool { 51 | return e.Rev < Missing 52 | } 53 | -------------------------------------------------------------------------------- /server/conn_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/bmizerany/assert" 5 | "github.com/kr/pretty" 6 | "testing" 7 | ) 8 | 9 | type grantTest struct { 10 | c *conn 11 | sk string 12 | r bool 13 | w bool 14 | ok bool 15 | } 16 | 17 | var grantTests = []grantTest{ 18 | // same 19 | {&conn{rosk: "p", rwsk: "p"}, "x", false, false, false}, 20 | {&conn{rosk: "p", rwsk: "p"}, "p", true, true, true}, 21 | 22 | // different 23 | {&conn{rosk: "a", rwsk: "b"}, "x", false, false, false}, 24 | {&conn{rosk: "a", rwsk: "b"}, "a", true, false, true}, 25 | {&conn{rosk: "a", rwsk: "b"}, "b", true, true, true}, 26 | 27 | // test blank passwords explicitly; the rules are 28 | // the same as above, but this is a common case 29 | {&conn{rosk: "", rwsk: ""}, "", true, true, true}, 30 | {&conn{rosk: "", rwsk: "b"}, "", true, false, true}, 31 | {&conn{rosk: "", rwsk: "b"}, "b", true, true, true}, 32 | } 33 | 34 | func TestConnGrant(t *testing.T) { 35 | for _, tst := range grantTests { 36 | ok := tst.c.grant(tst.sk) 37 | assert.Equalf(t, tst.ok, ok, "%# v", pretty.Formatter(tst)) 38 | assert.Equalf(t, tst.r, tst.c.raccess, "%# v", pretty.Formatter(tst)) 39 | assert.Equalf(t, tst.w, tst.c.waccess, "%# v", pretty.Formatter(tst)) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /peer/liveness.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | import ( 4 | "log" 5 | "net" 6 | ) 7 | 8 | type liverec struct { 9 | addr *net.UDPAddr 10 | seen int64 11 | } 12 | 13 | type liveness struct { 14 | timeout int64 15 | prev int64 16 | ival int64 17 | times []liverec 18 | self *net.UDPAddr 19 | shun chan<- string 20 | } 21 | 22 | func (lv *liveness) mark(a net.Addr, t int64) { 23 | uaddr, ok := a.(*net.UDPAddr) 24 | if !ok { 25 | return 26 | } 27 | var i int 28 | for i = 0; i < len(lv.times); i++ { 29 | if eq(lv.times[i].addr, uaddr) { 30 | lv.times[i].seen = t 31 | break 32 | } 33 | } 34 | if i == len(lv.times) { 35 | lv.times = append(lv.times, liverec{uaddr, t}) 36 | } 37 | } 38 | 39 | func (lv *liveness) check(t int64) { 40 | if t > lv.prev+lv.ival { 41 | n := t - lv.timeout 42 | times := make([]liverec, len(lv.times)) 43 | var i int 44 | for _, r := range lv.times { 45 | if n < r.seen || eq(r.addr, lv.self) { 46 | times[i] = r 47 | i++ 48 | } else { 49 | log.Printf("shunning addr=%s", r.addr) 50 | lv.shun <- r.addr.String() 51 | } 52 | } 53 | lv.times = times[:i] 54 | lv.prev = t 55 | } 56 | } 57 | 58 | func eq(a, b *net.UDPAddr) bool { 59 | return a.Port == b.Port && a.IP.Equal(b.IP) 60 | } 61 | -------------------------------------------------------------------------------- /member/member.go: -------------------------------------------------------------------------------- 1 | package member 2 | 3 | import ( 4 | "github.com/ha/doozerd/consensus" 5 | "github.com/ha/doozerd/store" 6 | "log" 7 | ) 8 | 9 | var ( 10 | calGlob = store.MustCompileGlob("/ctl/cal/*") 11 | ) 12 | 13 | func Clean(c chan string, st *store.Store, p consensus.Proposer) { 14 | for addr := range c { 15 | _, g := st.Snap() 16 | name := getName(addr, g) 17 | if name != "" { 18 | go func() { 19 | clearSlot(p, g, name) 20 | removeInfo(p, g, name) 21 | }() 22 | } 23 | } 24 | } 25 | 26 | func getName(addr string, g store.Getter) string { 27 | for _, name := range store.Getdir(g, "/ctl/node") { 28 | if store.GetString(g, "/ctl/node/"+name+"/addr") == addr { 29 | return name 30 | } 31 | } 32 | return "" 33 | } 34 | 35 | func clearSlot(p consensus.Proposer, g store.Getter, name string) { 36 | store.Walk(g, calGlob, func(path, body string, rev int64) bool { 37 | if body == name { 38 | consensus.Set(p, path, nil, rev) 39 | } 40 | return false 41 | }) 42 | } 43 | 44 | func removeInfo(p consensus.Proposer, g store.Getter, name string) { 45 | glob, err := store.CompileGlob("/ctl/node/" + name + "/**") 46 | if err != nil { 47 | log.Println(err) 48 | return 49 | } 50 | store.Walk(g, glob, func(path, _ string, rev int64) bool { 51 | consensus.Del(p, path, rev) 52 | return false 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /server/msg.proto: -------------------------------------------------------------------------------- 1 | package server; 2 | 3 | // see doc/proto.md 4 | message Request { 5 | optional int32 tag = 1; 6 | 7 | enum Verb { 8 | GET = 1; 9 | SET = 2; 10 | DEL = 3; 11 | REV = 5; 12 | WAIT = 6; 13 | NOP = 7; 14 | WALK = 9; 15 | GETDIR = 14; 16 | STAT = 16; 17 | SELF = 20; 18 | ACCESS = 99; 19 | } 20 | optional Verb verb = 2; 21 | 22 | optional string path = 4; 23 | optional bytes value = 5; 24 | optional int32 other_tag = 6; 25 | 26 | optional int32 offset = 7; 27 | 28 | optional int64 rev = 9; 29 | } 30 | 31 | // see doc/proto.md 32 | message Response { 33 | optional int32 tag = 1; 34 | optional int32 flags = 2; 35 | 36 | optional int64 rev = 3; 37 | optional string path = 5; 38 | optional bytes value = 6; 39 | optional int32 len = 8; 40 | 41 | enum Err { 42 | // don't use value 0 43 | OTHER = 127; 44 | TAG_IN_USE = 1; 45 | UNKNOWN_VERB = 2; 46 | READONLY = 3; 47 | TOO_LATE = 4; 48 | REV_MISMATCH = 5; 49 | BAD_PATH = 6; 50 | MISSING_ARG = 7; 51 | RANGE = 8; 52 | NOTDIR = 20; 53 | ISDIR = 21; 54 | NOENT = 22; 55 | } 56 | optional Err err_code = 100; 57 | optional string err_detail = 101; 58 | } 59 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/ha/doozerd/consensus" 5 | "github.com/ha/doozerd/store" 6 | "log" 7 | "net" 8 | "syscall" 9 | ) 10 | 11 | // ListenAndServe listens on l, accepts network connections, and 12 | // handles requests according to the doozer protocol. 13 | func ListenAndServe(l net.Listener, canWrite chan bool, st *store.Store, p consensus.Proposer, rwsk, rosk string, self string) { 14 | var w bool 15 | for { 16 | c, err := l.Accept() 17 | if err != nil { 18 | if err == syscall.EINVAL { 19 | break 20 | } 21 | if e, ok := err.(*net.OpError); ok && !e.Temporary() { 22 | break 23 | } 24 | log.Println(err) 25 | continue 26 | } 27 | 28 | // has this server become writable? 29 | select { 30 | case w = <-canWrite: 31 | canWrite = nil 32 | default: 33 | } 34 | 35 | go serve(c, st, p, w, rwsk, rosk, self) 36 | } 37 | } 38 | 39 | func serve(nc net.Conn, st *store.Store, p consensus.Proposer, w bool, rwsk, rosk string, self string) { 40 | c := &conn{ 41 | c: nc, 42 | addr: nc.RemoteAddr().String(), 43 | st: st, 44 | p: p, 45 | canWrite: w, 46 | rwsk: rwsk, 47 | rosk: rosk, 48 | self: self, 49 | } 50 | 51 | c.grant("") // start as if the client supplied a blank password 52 | c.serve() 53 | nc.Close() 54 | } 55 | -------------------------------------------------------------------------------- /consensus/learner.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | type learner struct { 4 | round int64 5 | quorum int64 6 | size int 7 | votes map[string]int64 // maps values to number of votes 8 | voted []bool // maps nodes to vote status 9 | 10 | v string 11 | done bool 12 | } 13 | 14 | func (ln *learner) init(n int, quorum int64) { 15 | ln.round = 1 16 | ln.votes = make(map[string]int64) 17 | ln.voted = make([]bool, n) 18 | ln.quorum = quorum 19 | ln.size = n 20 | } 21 | 22 | func (ln *learner) update(p *packet, from int) (m *msg, v []byte, ok bool) { 23 | if ln.done { 24 | return 25 | } 26 | 27 | in := p.msg 28 | switch *in.Cmd { 29 | case msg_LEARN: 30 | ln.done, ln.v = true, string(in.Value) 31 | return nil, in.Value, true 32 | case msg_VOTE: 33 | if in.Vrnd == nil { 34 | break 35 | } 36 | 37 | mRound, v := *in.Vrnd, in.Value 38 | 39 | switch { 40 | case mRound < ln.round: 41 | break 42 | case mRound > ln.round: 43 | ln.round = mRound 44 | ln.votes = make(map[string]int64) 45 | ln.voted = make([]bool, ln.size) 46 | fallthrough 47 | case mRound == ln.round: 48 | k := string(v) 49 | 50 | if ln.voted[from] { 51 | break 52 | } 53 | ln.votes[k]++ 54 | ln.voted[from] = true 55 | 56 | if ln.votes[k] >= ln.quorum { 57 | // winner! 58 | ln.done, ln.v = true, string(v) 59 | return &msg{Cmd: learn, Value: v}, v, true 60 | } 61 | } 62 | } 63 | return 64 | } 65 | -------------------------------------------------------------------------------- /doc/data-model.md: -------------------------------------------------------------------------------- 1 | # Data Model 2 | 3 | ## Files 4 | 5 | Files in Doozer are organized in a tree, much like in [Unix](). Each directory 6 | contains a list of names that correspond to other directories or files. Each file 7 | contains a sequence of bytes that can be read or written. The root of the store 8 | is the directory `/`. The sequence of names leading from the root to any 9 | given file, including the filename, constitutes a path that uniquely identifies 10 | the file. Paths are notated by joining all the names with `/` between them. 11 | 12 | For example, `/foo/bar/baz` refers to the file `baz`, inside the directory 13 | `bar`, inside the directory `foo`, inside the root directory. 14 | 15 | ### Naming files 16 | 17 | Names are UTF-8 character strings that contain only ASCII letters, numbers, `.`, 18 | or `-`. 19 | 20 | NOTE: This may seem ironic but we are keeping the door open to lift some of the 21 | restrictions. 22 | 23 | ## Read/Write 24 | 25 | Users are limited to whole-file read and writes. Users can only read a whole 26 | file or write over a whole file. 27 | 28 | ## Revisions 29 | 30 | Changes to the store (i.e. creating, updating, or deleting a file) are applied, 31 | one at a time, in sequence. Every change creates a new version of the store 32 | with one difference from the previous version. Every version of the store gets 33 | assigned an integer, its `rev`, one greater than the previous rev. Previous 34 | revisions are kept for reference until [some time later](). 35 | -------------------------------------------------------------------------------- /doc/uri.md: -------------------------------------------------------------------------------- 1 | # Doozer URIs 2 | 3 | This document describes the `doozer:` URI scheme. 4 | 5 | A doozer link identifies a doozer cluster and provides 6 | hints on how to contact the cluster. It contains a 7 | sequence of parameters, the order of which is not 8 | significant, formatted in the same way as the HTTP URL 9 | query string. 10 | 11 | There are three parameters: 12 | 13 | * *un* ("unique name"): a Base32-encoded 160-bit value 14 | unique to the doozer cluster that created it. 15 | 16 | Example: 17 | 18 | un=BCIRJYENEZYYYA5K65TY3C2SSZLGKW2K 19 | 20 | [TODO specify address discovery mechanisms, such as 21 | looking up addresses in another doozer cluster] 22 | 23 | * *cn* ("cluster name"): an ASCII value representing the name of the cluster 24 | given to `doozerd` with the `-c` flag. 25 | 26 | Example: 27 | 28 | cn=example 29 | 30 | * *ca* ("cluster address"): a host name or ip address, 31 | with an optional port suffix. The default port 8046 will be used if no port 32 | is specified. 33 | 34 | This parameter can appear more than once, to provide 35 | more than one address through which to access the 36 | cluster. 37 | 38 | Examples: 39 | 40 | ca=10.0.0.1:5003 41 | ca=d.example.net 42 | 43 | * *sk* ("secret key"): an arbitrary string of characters clients must send to 44 | the server (via the `ACCESS` verb) before reading or writing. 45 | 46 | Example: 47 | 48 | sk=eXampl3 49 | 50 | Full Example: 51 | 52 | doozer:?un=BCIRJYENEZYYYA5K65TY3C2SSZLGKW2K&ca=10.0.1.1&ca=10.0.1.2&ca=10.0.1.3 53 | -------------------------------------------------------------------------------- /member/member_test.go: -------------------------------------------------------------------------------- 1 | package member 2 | 3 | import ( 4 | "github.com/bmizerany/assert" 5 | "github.com/ha/doozerd/store" 6 | "github.com/ha/doozerd/test" 7 | "sort" 8 | "testing" 9 | ) 10 | 11 | func TestMemberSimple(t *testing.T) { 12 | st := store.New() 13 | defer close(st.Ops) 14 | fp := &test.FakeProposer{Store: st} 15 | c := make(chan string) 16 | go Clean(c, fp.Store, fp) 17 | 18 | fp.Propose([]byte(store.MustEncodeSet("/ctl/node/a/x", "a", store.Missing))) 19 | fp.Propose([]byte(store.MustEncodeSet("/ctl/node/a/y", "b", store.Missing))) 20 | fp.Propose([]byte(store.MustEncodeSet("/ctl/node/a/addr", "1.2.3.4", store.Missing))) 21 | fp.Propose([]byte(store.MustEncodeSet("/ctl/cal/0", "a", store.Missing))) 22 | 23 | calCh, err := fp.Wait(store.MustCompileGlob("/ctl/cal/0"), 1+<-fp.Seqns) 24 | if err != nil { 25 | panic(err) 26 | } 27 | nodeCh, err := fp.Wait(store.MustCompileGlob("/ctl/node/a/?"), 1+<-fp.Seqns) 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | // indicate that this peer is inactive 33 | go func() { c <- "1.2.3.4" }() 34 | 35 | ev := <-calCh 36 | assert.T(t, ev.IsSet()) 37 | assert.Equal(t, "", ev.Body) 38 | 39 | cs := []int{} 40 | 41 | ev = <-nodeCh 42 | assert.T(t, ev.IsDel()) 43 | cs = append(cs, int(ev.Path[len(ev.Path)-1])) 44 | nodeCh, err = fp.Wait(store.MustCompileGlob("/ctl/node/a/?"), ev.Seqn+1) 45 | if err != nil { 46 | panic(err) 47 | } 48 | 49 | ev = <-nodeCh 50 | assert.T(t, ev.IsDel()) 51 | cs = append(cs, int(ev.Path[len(ev.Path)-1])) 52 | 53 | sort.Ints(cs) 54 | assert.Equal(t, []int{'x', 'y'}, cs) 55 | } 56 | -------------------------------------------------------------------------------- /doc/hacking.md: -------------------------------------------------------------------------------- 1 | # Hacking on Doozer 2 | 3 | If you want to hack on doozer, we suggest discussing your plans on the 4 | [mailing list][mail] to avoid duplicating effort. 5 | But if not, that's cool too. Just have fun. 6 | 7 | Here are some instructions for building doozer from source: 8 | 9 | ## Installing Go 10 | 11 | I recommend not using apt, homebrew, or any other packaging system to install 12 | Go. It's better to install straight from the official Go packages. 13 | Easy-to-follow instructions are at . 14 | 15 | ## Installing Dependencies 16 | 17 | If you want to change .proto files, you need to nstall the `protoc` 18 | command (from ): 19 | 20 | $ sudo apt-get install protobuf-compiler 21 | (or) 22 | $ brew install protobuf 23 | 24 | If you want to run doozer's tests, install 25 | . 26 | 27 | ## Building Doozer 28 | 29 | (make sure you have set $GOPATH) 30 | $ mkdir -p $GOPATH/src/github.com/ha/ 31 | $ git clone https://github.com/ha/doozerd.git 32 | $ cd doozerd 33 | $ ./all.sh 34 | 35 | This will build the rest of the dependencies and 36 | all doozer packages and commands, 37 | and copy the commands into `$GOPATH/bin`. You can test individual doozer 38 | components by running `go test` in that sub-package directory. 39 | 40 | ## Try It Out 41 | 42 | $ doozerd >/dev/null 2>&1 & 43 | $ open http://localhost:8000/ 44 | 45 | This will start up one doozer process and show a web view of its contents. 46 | 47 | [mail]: https://groups.google.com/group/doozer 48 | -------------------------------------------------------------------------------- /web/main.css.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | // This file was generated from web/main.css. 4 | 5 | var main_css string = "body {\n color: #333;\n font-family: monospace;\n}\n\n#info {\n background: #ccc;\n padding: .2em .4em;\n margin: 0 0 1em;\n -webkit-border-radius: .4em;\n border-radius: .4em;\n}\n\n.error #info {\n background: #d88;\n}\n\n.msg {\n display: none;\n background: #ee8;\n -webkit-border-radius: .4em;\n border-radius: .4em;\n padding: 0 .3em;\n}\n\n.waiting #waiting.msg, .wereback #wereback.msg {\n display: inline;\n}\n\na {\n color: #35e;\n cursor: pointer;\n font-weight: bold;\n text-decoration: underline;\n}\n\n#tree {\n opacity: .5;\n}\n\n.open #tree {\n opacity: 1;\n}\n\ndl {\n margin: 0 0 0 .5em;\n padding: 0;\n}\n\ndt {\n font-weight: bold;\n margin: 0;\n padding: 0;\n}\n\ndd {\n margin: 0 0 .5em;\n padding: 0 0 0 1em;\n}\n\ntable {\n border-spacing: 0;\n}\n\ntr {\n -webkit-transition-property: background;\n -webkit-transition-duration: 350ms;\n -webkit-transition-timing-function: ease-in-out;\n -moz-transition-property: background;\n -moz-transition-duration: 350ms;\n -moz-transition-timing-function: ease-in-out;\n transition-property: background;\n transition-duration: 350ms;\n transition-timing-function: ease-in-out;\n}\n\ntr.new {\n background: #f7f787;\n}\n\nth {\n font-weight: normal;\n margin: 0;\n padding: 0 .5em;\n text-align: left;\n}\n\ntd.eq:after {\n content: \"=\";\n}\n\ntd {\n margin: 0;\n padding: 0 .5em;\n}\n\ntd.rev {\n color: #aaa;\n text-align: right;\n}\n\ntd.body {\n}\n" 6 | -------------------------------------------------------------------------------- /doc/proto-examples.md: -------------------------------------------------------------------------------- 1 | 2 | ## Examples 3 | 4 | (In these examples, we'll use an informal notation 5 | similar to JSON to indicate the contents of structures 6 | sent over the wire.) 7 | 8 | ### Get 9 | 10 | Let's say the client wants to retrieve file `/a`. So it 11 | sends the following: 12 | 13 | { 14 | tag: 0, 15 | verb: GET, 16 | path: "/a", 17 | } 18 | 19 | The server replies 20 | 21 | { 22 | tag: 0, 23 | flags: 3, // 3 == valid|done 24 | rev: 5, 25 | value: "hello", 26 | } 27 | 28 | ### Set and Get 29 | 30 | Set usually takes much longer than get, so here we'll 31 | see replies come out of order: 32 | 33 | { 34 | tag: 0, 35 | verb: SET, 36 | path: "/a", 37 | rev: -1, 38 | value: "goodbye", 39 | } 40 | 41 | { 42 | tag: 1, 43 | verb: GET, 44 | path: "/a", 45 | } 46 | 47 | The server replies immediately: 48 | 49 | { 50 | tag: 1, 51 | flags: 3, // 3 == valid|done 52 | rev: 5, 53 | value: "hello", 54 | } 55 | 56 | Some time later, the set operation finishes: 57 | 58 | { 59 | tag: 0, 60 | flags: 3, // 3 == valid|done 61 | rev: 6, 62 | } 63 | 64 | Now, the client can issue the same get request once 65 | more: 66 | 67 | { 68 | tag: 1, 69 | verb: GET, 70 | path: "/a", 71 | } 72 | 73 | This time, the server replies: 74 | 75 | { 76 | tag: 1, 77 | flags: 3, // 3 == valid|done 78 | rev: 6, 79 | value: "goodbye", 80 | } 81 | -------------------------------------------------------------------------------- /consensus/coordinator.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | type coordinator struct { 4 | size int 5 | quor int 6 | 7 | begun bool 8 | target string 9 | crnd int64 10 | cval string 11 | rsvp []bool 12 | nrsvp int 13 | vr int64 14 | vv string 15 | 16 | sched bool 17 | } 18 | 19 | func (co *coordinator) update(p *packet, from int) (m *msg, wantTick bool) { 20 | in := &p.msg 21 | switch *in.Cmd { 22 | case msg_PROPOSE: 23 | if co.begun { 24 | break 25 | } 26 | 27 | co.begun = true 28 | co.target = string(in.Value) 29 | co.vr = 0 30 | co.vv = "" 31 | co.rsvp = make([]bool, co.size) 32 | co.cval = "" 33 | return &msg{Cmd: invite, Crnd: &co.crnd}, true 34 | case msg_RSVP: 35 | if !co.begun { 36 | break 37 | } 38 | 39 | if in.Crnd == nil || in.Vrnd == nil { 40 | break 41 | } 42 | 43 | i, vrnd, vval := *in.Crnd, *in.Vrnd, in.Value 44 | 45 | if co.cval != "" { 46 | break 47 | } 48 | 49 | if i != co.crnd { 50 | break 51 | } 52 | 53 | if vrnd > co.vr { 54 | co.vr = vrnd 55 | co.vv = string(vval) 56 | } 57 | 58 | if !co.rsvp[from] { 59 | co.rsvp[from] = true 60 | co.nrsvp++ 61 | } 62 | if co.nrsvp >= co.quor { 63 | var v string 64 | 65 | if co.vr > 0 { 66 | v = co.vv 67 | } else { 68 | v = co.target 69 | } 70 | co.cval = v 71 | 72 | return &msg{Cmd: nominate, Crnd: &co.crnd, Value: []byte(v)}, false 73 | } 74 | case msg_TICK: 75 | co.crnd += int64(co.size) 76 | co.vr = 0 77 | co.vv = "" 78 | co.rsvp = make([]bool, co.size) 79 | co.nrsvp = 0 80 | co.cval = "" 81 | co.sched = false 82 | return &msg{Cmd: invite, Crnd: &co.crnd}, true 83 | } 84 | 85 | return 86 | } 87 | -------------------------------------------------------------------------------- /server/conn.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "code.google.com/p/goprotobuf/proto" 5 | "encoding/binary" 6 | "github.com/ha/doozerd/consensus" 7 | "github.com/ha/doozerd/store" 8 | "io" 9 | "log" 10 | "sync" 11 | ) 12 | 13 | type conn struct { 14 | c io.ReadWriter 15 | wl sync.Mutex // write lock 16 | addr string 17 | p consensus.Proposer 18 | st *store.Store 19 | canWrite bool 20 | rwsk string 21 | rosk string 22 | waccess bool 23 | raccess bool 24 | self string 25 | } 26 | 27 | func (c *conn) serve() { 28 | for { 29 | var t txn 30 | t.c = c 31 | err := c.read(&t.req) 32 | if err != nil { 33 | if err != io.EOF { 34 | log.Println(err) 35 | } 36 | return 37 | } 38 | t.run() 39 | } 40 | } 41 | 42 | func (c *conn) read(r *request) error { 43 | var size int32 44 | err := binary.Read(c.c, binary.BigEndian, &size) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | buf := make([]byte, size) 50 | _, err = io.ReadFull(c.c, buf) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | return proto.Unmarshal(buf, r) 56 | } 57 | 58 | func (c *conn) write(r *response) error { 59 | buf, err := proto.Marshal(r) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | c.wl.Lock() 65 | defer c.wl.Unlock() 66 | 67 | err = binary.Write(c.c, binary.BigEndian, int32(len(buf))) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | _, err = c.c.Write(buf) 73 | return err 74 | } 75 | 76 | // Grant compares sk against c.rwsk and c.rosk and 77 | // updates c.waccess and c.raccess as necessary. 78 | // It returns true if sk matched either password. 79 | func (c *conn) grant(sk string) bool { 80 | switch sk { 81 | case c.rwsk: 82 | c.waccess = true 83 | c.raccess = true 84 | return true 85 | case c.rosk: 86 | c.raccess = true 87 | return true 88 | } 89 | return false 90 | } 91 | -------------------------------------------------------------------------------- /doc/firedrill.md: -------------------------------------------------------------------------------- 1 | # Fire Drills 2 | 3 | Experience doozer's fault tolerance firsthand. 4 | 5 | This document intends to give examples of failure 6 | scenarios and how you might deal with them. Our hope 7 | is that it will evolve into a useful playbook for 8 | handling actual failures. 9 | 10 | ## Node Failure 11 | 12 | The plan: boot three consensors, kill one, then replace it with 13 | a fresh fourth. Clients will see no interruption in service. 14 | 15 | 1. Start three active doozerds: 16 | 17 | $ doozerd -timeout 5 -l 127.0.0.1:8046 -w 127.0.0.1:8000 2>/dev/null & 18 | $ doozerd -timeout 5 -l 127.0.0.1:8047 -w 127.0.0.1:8001 -a 127.0.0.1:8046 2>/dev/null & 19 | $ doozerd -timeout 5 -l 127.0.0.1:8048 -w 127.0.0.1:8002 -a 127.0.0.1:8046 2>/dev/null & 20 | $ echo -n | doozer add /ctl/cal/1 # activate the second one 21 | $ echo -n | doozer add /ctl/cal/2 # activate the third one 22 | 23 | 2. Start a client: 24 | 25 | $ doozer watch '/**' 26 | 27 | 3. Kill one of the doozerds: 28 | 29 | $ kill -STOP `ps auxww|grep doozerd|grep :8047|awk '{print $2}'` 30 | 31 | Notice that activity continues in the cluster. The client 32 | continues printing updates, but with NOPs in place of changes 33 | that would have been originated by the dead node. After about 34 | five seconds (the value you gave for -timeout) the dead node 35 | will be removed by the other two and the cluster will shrink 36 | from three to two nodes. 37 | 38 | (Note: the command above kills the doozerd running on port 8047. 39 | You could kill any of the three doozerds, but the client doesn't 40 | yet try to reconnect if it loses its connection, so it's easier 41 | to demonstrate doozerd's behavior by killing one the client isn't 42 | connected to.) 43 | 44 | 4. Start a fourth 45 | 46 | $ doozerd -timeout 5 -l 127.0.0.1:8049 -w 127.0.0.1:8004 -a 127.0.0.1:8046 2>/dev/null & 47 | 48 | 5. Cleanup 49 | 50 | $ killall -9 doozerd doozer 51 | -------------------------------------------------------------------------------- /web/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #333; 3 | font-family: monospace; 4 | } 5 | 6 | #info { 7 | background: #ccc; 8 | padding: .2em .4em; 9 | margin: 0 0 1em; 10 | -webkit-border-radius: .4em; 11 | border-radius: .4em; 12 | } 13 | 14 | .error #info { 15 | background: #d88; 16 | } 17 | 18 | .msg { 19 | display: none; 20 | background: #ee8; 21 | -webkit-border-radius: .4em; 22 | border-radius: .4em; 23 | padding: 0 .3em; 24 | } 25 | 26 | .waiting #waiting.msg, .wereback #wereback.msg { 27 | display: inline; 28 | } 29 | 30 | a { 31 | color: #35e; 32 | cursor: pointer; 33 | font-weight: bold; 34 | text-decoration: underline; 35 | } 36 | 37 | #tree { 38 | opacity: .5; 39 | } 40 | 41 | .open #tree { 42 | opacity: 1; 43 | } 44 | 45 | dl { 46 | margin: 0 0 0 .5em; 47 | padding: 0; 48 | } 49 | 50 | dt { 51 | font-weight: bold; 52 | margin: 0; 53 | padding: 0; 54 | } 55 | 56 | dd { 57 | margin: 0 0 .5em; 58 | padding: 0 0 0 1em; 59 | } 60 | 61 | table { 62 | border-spacing: 0; 63 | } 64 | 65 | tr { 66 | -webkit-transition-property: background; 67 | -webkit-transition-duration: 350ms; 68 | -webkit-transition-timing-function: ease-in-out; 69 | -moz-transition-property: background; 70 | -moz-transition-duration: 350ms; 71 | -moz-transition-timing-function: ease-in-out; 72 | transition-property: background; 73 | transition-duration: 350ms; 74 | transition-timing-function: ease-in-out; 75 | } 76 | 77 | tr.new { 78 | background: #f7f787; 79 | } 80 | 81 | th { 82 | font-weight: normal; 83 | margin: 0; 84 | padding: 0 .5em; 85 | text-align: left; 86 | } 87 | 88 | td.eq:after { 89 | content: "="; 90 | } 91 | 92 | td { 93 | margin: 0; 94 | padding: 0 .5em; 95 | } 96 | 97 | td.rev { 98 | color: #aaa; 99 | text-align: right; 100 | } 101 | 102 | td.body { 103 | } 104 | -------------------------------------------------------------------------------- /consensus/run.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | import ( 4 | "code.google.com/p/goprotobuf/proto" 5 | "container/heap" 6 | "github.com/ha/doozerd/store" 7 | "log" 8 | "math/rand" 9 | "net" 10 | "time" 11 | ) 12 | 13 | const initialWaitBound = 1e6 // ns == 1ms 14 | 15 | type run struct { 16 | seqn int64 17 | self string 18 | cals []string 19 | addr []*net.UDPAddr 20 | 21 | c coordinator 22 | a acceptor 23 | l learner 24 | 25 | out chan<- Packet 26 | ops chan<- store.Op 27 | bound int64 28 | ntick int 29 | prop bool 30 | } 31 | 32 | func (r *run) quorum() int { 33 | return len(r.cals)/2 + 1 34 | } 35 | 36 | func (r *run) update(p *packet, from int, ticks heap.Interface) { 37 | if p.msg.Cmd != nil && *p.msg.Cmd == msg_TICK { 38 | log.Printf("tick wasteful=%v", r.l.done) 39 | } 40 | 41 | m, tick := r.c.update(p, from) 42 | r.broadcast(m) 43 | if tick { 44 | r.ntick++ 45 | r.bound *= 2 46 | t := rand.Int63n(r.bound + 1) // +1 because it panics if bound is 0. 47 | log.Printf("sched tick=%d seqn=%d t=%d", r.ntick, r.seqn, t) 48 | schedTrigger(ticks, r.seqn, time.Now().UnixNano(), t) 49 | } 50 | 51 | m = r.a.update(&p.msg) 52 | r.broadcast(m) 53 | 54 | m, v, ok := r.l.update(p, from) 55 | r.broadcast(m) 56 | if ok { 57 | log.Printf("learn seqn=%d", r.seqn) 58 | r.ops <- store.Op{r.seqn, string(v)} 59 | } 60 | } 61 | 62 | func (r *run) broadcast(m *msg) { 63 | if m != nil { 64 | m.Seqn = &r.seqn 65 | b, _ := proto.Marshal(m) 66 | for _, addr := range r.addr { 67 | r.out <- Packet{addr, b} 68 | } 69 | } 70 | } 71 | 72 | func (r *run) indexOf(self string) int64 { 73 | for i, id := range r.cals { 74 | if id == self { 75 | return int64(i) 76 | } 77 | } 78 | return -1 79 | } 80 | 81 | func (r *run) indexOfAddr(a *net.UDPAddr) int { 82 | if a == nil { 83 | return -1 84 | } 85 | for i, b := range r.addr { 86 | if a.Port == b.Port && a.IP.Equal(b.IP) { 87 | return i 88 | } 89 | } 90 | return -1 91 | } 92 | 93 | func (r *run) isLeader(self string) bool { 94 | for i, id := range r.cals { 95 | if id == self { 96 | return r.seqn%int64(len(r.cals)) == int64(i) 97 | } 98 | } 99 | return false 100 | } 101 | -------------------------------------------------------------------------------- /consensus/m_test.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | import ( 4 | "code.google.com/p/goprotobuf/proto" 5 | "fmt" 6 | "net" 7 | ) 8 | 9 | func (x *msg_Cmd) Format(f fmt.State, c int) { 10 | if c == 'v' && f.Flag('#') && x != nil { 11 | fmt.Fprintf(f, "msg_%s", msg_Cmd_name[int32(*x)]) 12 | return 13 | } 14 | 15 | s := "%" 16 | for i := 0; i < 128; i++ { 17 | if f.Flag(i) { 18 | s += string(i) 19 | } 20 | } 21 | if w, ok := f.Width(); ok { 22 | s += fmt.Sprintf("%d", w) 23 | } 24 | if p, ok := f.Precision(); ok { 25 | s += fmt.Sprintf(".%d", p) 26 | } 27 | s += string(c) 28 | fmt.Fprintf(f, s, (*int32)(x)) 29 | } 30 | 31 | // For testing convenience 32 | func newVote(i int64, vval string) *msg { 33 | return &msg{Cmd: vote, Vrnd: &i, Value: []byte(vval)} 34 | } 35 | 36 | // For testing convenience 37 | func newVoteFrom(from int, i int64, vval string) (*packet, int) { 38 | m := newVote(i, vval) 39 | m.Seqn = proto.Int64(1) 40 | return &packet{&net.UDPAddr{Port: from}, *m}, from 41 | } 42 | 43 | // For testing convenience 44 | func newNominate(crnd int64, v string) *msg { 45 | return &msg{Cmd: nominate, Crnd: &crnd, Value: []byte(v)} 46 | } 47 | 48 | // For testing convenience 49 | func newNominateSeqn1(crnd int64, v string) *msg { 50 | m := newNominate(crnd, v) 51 | m.Seqn = proto.Int64(1) 52 | return m 53 | } 54 | 55 | // For testing convenience 56 | func newRsvp(i, vrnd int64, vval string) *msg { 57 | return &msg{ 58 | Cmd: rsvp, 59 | Crnd: &i, 60 | Vrnd: &vrnd, 61 | Value: []byte(vval), 62 | } 63 | } 64 | 65 | // For testing convenience 66 | func newRsvpFrom(from int, i, vrnd int64, vval string) (*packet, int) { 67 | m := newRsvp(i, vrnd, vval) 68 | m.Seqn = proto.Int64(1) 69 | return &packet{&net.UDPAddr{Port: from}, *m}, from 70 | } 71 | 72 | // For testing convenience 73 | func newInvite(crnd int64) *msg { 74 | return &msg{Cmd: invite, Crnd: &crnd} 75 | } 76 | 77 | // For testing convenience 78 | func newInviteSeqn1(rnd int64) *msg { 79 | m := newInvite(rnd) 80 | m.Seqn = proto.Int64(1) 81 | return m 82 | } 83 | 84 | // For testing convenience 85 | func newPropose(val string) *msg { 86 | return &msg{Cmd: propose, Value: []byte(val)} 87 | } 88 | 89 | // For testing convenience 90 | func newLearn(val string) *msg { 91 | return &msg{Cmd: learn, Value: []byte(val)} 92 | } 93 | -------------------------------------------------------------------------------- /store/getter.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "sort" 5 | ) 6 | 7 | type Getter interface { 8 | Get(path string) (values []string, rev int64) 9 | Stat(path string) (ln int32, rev int64) 10 | } 11 | 12 | // Retrieves the body stored in `g` at `path` and returns it. If `path` is a 13 | // directory or does not exist, returns an empty string. 14 | // 15 | // Note, with this function it is impossible to distinguish between an empty 16 | // string stored at `path`, a missing entry, and a directory. If you need to 17 | // tell the difference, use `g.Get`. 18 | // 19 | // Also note, this function does not return the revision for `path`. If you 20 | // need the revision, use `g.Get`. 21 | func GetString(g Getter, path string) (body string) { 22 | v, rev := g.Get(path) 23 | if rev == Missing || rev == Dir { 24 | return "" 25 | } 26 | return v[0] 27 | } 28 | 29 | // Returns a list of entries in `g` in the directory at `path`. If `path` is 30 | // not a directory, returns an empty slice. 31 | // 32 | // Note, with this function it is impossible to distinguish between a string 33 | // stored at `path` and a missing entry. If you need to tell the difference, 34 | // use `g.Get`. 35 | func Getdir(g Getter, path string) (entries []string) { 36 | v, rev := g.Get(path) 37 | if rev != Dir { 38 | return nil 39 | } 40 | return v 41 | } 42 | 43 | type Visitor func(path, body string, rev int64) (stop bool) 44 | 45 | func walk(g Getter, path string, glob *Glob, f Visitor) (stopped bool) { 46 | v, rev := g.Get(path) 47 | if rev == Missing { 48 | return 49 | } 50 | 51 | if rev != Dir { 52 | return glob.Match(path) && f(path, v[0], rev) 53 | } 54 | 55 | if path == "/" { 56 | path = "" 57 | } 58 | 59 | sort.Strings(v) 60 | for _, ent := range v { 61 | stopped = walk(g, path+"/"+ent, glob, f) 62 | if stopped { 63 | return 64 | } 65 | } 66 | return 67 | } 68 | 69 | // Walk walks the entries in g, calling f for each file that matches glob. 70 | // Entries are visited in sorted order. 71 | // If f returns true, Walk will stop visiting entries and return immediately; 72 | // Walk won't call f again. 73 | // Walk returns true if f returned true. 74 | func Walk(g Getter, glob *Glob, f Visitor) (stopped bool) { 75 | // TODO find the longest non-glob prefix of glob.Pattern and start there 76 | return walk(g, "/", glob, f) 77 | } 78 | -------------------------------------------------------------------------------- /peer/liveness_test.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | import ( 4 | "github.com/bmizerany/assert" 5 | "net" 6 | "testing" 7 | ) 8 | 9 | func TestLivenessMark(t *testing.T) { 10 | a1, err := net.ResolveUDPAddr("udp", "127.0.0.1:8046") 11 | if err != nil { 12 | panic(err) 13 | } 14 | a2, err := net.ResolveUDPAddr("udp", "127.0.0.2:8046") 15 | if err != nil { 16 | panic(err) 17 | } 18 | lv := liveness{ 19 | timeout: 10, 20 | ival: 5, 21 | self: a1, 22 | shun: make(chan string, 100), 23 | } 24 | 25 | lv.mark(a1, 1) 26 | assert.Equal(t, []liverec{{a1, 1}}, lv.times) 27 | lv.mark(a2, 2) 28 | assert.Equal(t, []liverec{{a1, 1}, {a2, 2}}, lv.times) 29 | } 30 | 31 | func TestLivenessStaysAlive(t *testing.T) { 32 | shun := make(chan string, 1) 33 | a, _ := net.ResolveUDPAddr("udp", "1.2.3.4:5") 34 | lv := liveness{ 35 | prev: 0, 36 | ival: 1, 37 | timeout: 3, 38 | times: []liverec{{a, 5}}, 39 | shun: shun, 40 | } 41 | lv.check(7) 42 | assert.Equal(t, int64(7), lv.prev) 43 | assert.Equal(t, 0, len(shun)) 44 | assert.Equal(t, []liverec{{a, 5}}, lv.times) 45 | } 46 | 47 | func TestLivenessTimesOut(t *testing.T) { 48 | shun := make(chan string, 1) 49 | a, _ := net.ResolveUDPAddr("udp", "1.2.3.4:5") 50 | b, _ := net.ResolveUDPAddr("udp", "2.3.4.5:6") 51 | lv := liveness{ 52 | prev: 0, 53 | ival: 1, 54 | timeout: 3, 55 | times: []liverec{{a, 5}}, 56 | shun: shun, 57 | self: b, 58 | } 59 | lv.check(9) 60 | assert.Equal(t, int64(9), lv.prev) 61 | assert.Equal(t, 1, len(shun)) 62 | assert.Equal(t, "1.2.3.4:5", <-shun) 63 | assert.Equal(t, []liverec{}, lv.times) 64 | } 65 | 66 | func TestLivenessSelfStaysAlive(t *testing.T) { 67 | shun := make(chan string, 1) 68 | a, _ := net.ResolveUDPAddr("udp", "1.2.3.4:5") 69 | lv := liveness{ 70 | prev: 0, 71 | ival: 1, 72 | timeout: 3, 73 | times: []liverec{{a, 5}}, 74 | shun: shun, 75 | self: a, 76 | } 77 | lv.check(9) 78 | assert.Equal(t, int64(9), lv.prev) 79 | assert.Equal(t, 0, len(shun)) 80 | assert.Equal(t, []liverec{{a, 5}}, lv.times) 81 | } 82 | 83 | func TestLivenessNoCheck(t *testing.T) { 84 | a, _ := net.ResolveUDPAddr("udp", "1.2.3.4:5") 85 | lv := liveness{ 86 | prev: 5, 87 | ival: 3, 88 | times: []liverec{{a, 5}}, 89 | } 90 | lv.check(7) 91 | assert.Equal(t, int64(5), lv.prev) 92 | assert.Equal(t, []liverec{{a, 5}}, lv.times) 93 | } 94 | -------------------------------------------------------------------------------- /consensus/acceptor_test.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | import ( 4 | "github.com/bmizerany/assert" 5 | _ "github.com/ha/doozerd/quiet" 6 | "testing" 7 | ) 8 | 9 | func TestIgnoreOldMessages(t *testing.T) { 10 | tests := [][]*msg{ 11 | {newInviteSeqn1(11), newNominateSeqn1(1, "v")}, 12 | {newNominateSeqn1(11, "v"), newInviteSeqn1(1)}, 13 | {newInviteSeqn1(11), newInviteSeqn1(1)}, 14 | {newNominateSeqn1(11, "v"), newNominateSeqn1(1, "v")}, 15 | } 16 | 17 | for _, test := range tests { 18 | ac := acceptor{} 19 | 20 | ac.update(test[0]) 21 | 22 | got := ac.update(test[1]) 23 | assert.Equal(t, (*msg)(nil), got) 24 | } 25 | } 26 | 27 | func TestAcceptsInvite(t *testing.T) { 28 | ac := acceptor{} 29 | got := ac.update(newInviteSeqn1(1)) 30 | assert.Equal(t, newRsvp(1, 0, ""), got) 31 | } 32 | 33 | func TestItVotes(t *testing.T) { 34 | totest := [][]*msg{ 35 | {newNominateSeqn1(1, "foo"), newVote(1, "foo")}, 36 | {newNominateSeqn1(1, "bar"), newVote(1, "bar")}, 37 | } 38 | 39 | for _, test := range totest { 40 | ac := acceptor{} 41 | got := ac.update(test[0]) 42 | assert.Equal(t, test[1], got, test) 43 | } 44 | } 45 | 46 | func TestItVotesWithAnotherRound(t *testing.T) { 47 | ac := acceptor{} 48 | val := "bar" 49 | 50 | // According to paxos, we can omit Phase 1 in the first round 51 | got := ac.update(newNominateSeqn1(2, val)) 52 | assert.Equal(t, newVote(2, val), got) 53 | } 54 | 55 | func TestItVotesWithAnotherSelf(t *testing.T) { 56 | ac := acceptor{} 57 | val := "bar" 58 | 59 | // According to paxos, we can omit Phase 1 in the first round 60 | got := ac.update(newNominateSeqn1(2, val)) 61 | assert.Equal(t, newVote(2, val), got) 62 | } 63 | 64 | func TestVotedRoundsAndValuesAreTracked(t *testing.T) { 65 | ac := acceptor{} 66 | 67 | ac.update(newNominateSeqn1(1, "v")) 68 | 69 | got := ac.update(newInviteSeqn1(2)) 70 | assert.Equal(t, newRsvp(2, 1, "v"), got) 71 | } 72 | 73 | func TestVotesOnlyOncePerRound(t *testing.T) { 74 | ac := acceptor{} 75 | 76 | got := ac.update(newNominateSeqn1(1, "v")) 77 | assert.Equal(t, newVote(1, "v"), got) 78 | 79 | got = ac.update(newNominateSeqn1(1, "v")) 80 | assert.Equal(t, (*msg)(nil), got) 81 | } 82 | 83 | func TestAcceptorIgnoresBadMessages(t *testing.T) { 84 | ac := acceptor{} 85 | 86 | got := ac.update(&msg{Cmd: invite}) // missing Crnd 87 | assert.Equal(t, (*msg)(nil), got) 88 | 89 | got = ac.update(&msg{Cmd: nominate}) // missing Crnd 90 | assert.Equal(t, (*msg)(nil), got) 91 | } 92 | -------------------------------------------------------------------------------- /store/glob.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | // Glob holds a Unix-style glob pattern in a compiled form for efficient 9 | // matching against paths. 10 | // 11 | // Glob notation: 12 | // - `?` matches a single char in a single path component 13 | // - `*` matches zero or more chars in a single path component 14 | // - `**` matches zero or more chars in zero or more components 15 | // - any other sequence matches itself 16 | type Glob struct { 17 | Pattern string // original glob pattern 18 | s string // translated to regexp pattern 19 | r *regexp.Regexp // compiled regexp 20 | } 21 | 22 | var globRe = mustBuildRe(`(` + charPat + `|[\*\?])`) 23 | 24 | // Supports unix/ruby-style glob patterns: 25 | // - `?` matches a single char in a single path component 26 | // - `*` matches zero or more chars in a single path component 27 | // - `**` matches zero or more chars in zero or more components 28 | func translateGlob(pat string) (string, error) { 29 | if !globRe.MatchString(pat) { 30 | return "", GlobError(pat) 31 | } 32 | 33 | outs := make([]string, len(pat)) 34 | i, double := 0, false 35 | for _, c := range pat { 36 | switch c { 37 | default: 38 | outs[i] = string(c) 39 | double = false 40 | case '.', '+', '-', '^', '$', '[', ']', '(', ')': 41 | outs[i] = `\` + string(c) 42 | double = false 43 | case '?': 44 | outs[i] = `[^/]` 45 | double = false 46 | case '*': 47 | if double { 48 | outs[i-1] = `.*` 49 | } else { 50 | outs[i] = `[^/]*` 51 | } 52 | double = !double 53 | } 54 | i++ 55 | } 56 | outs = outs[0:i] 57 | 58 | return "^" + strings.Join(outs, "") + "$", nil 59 | } 60 | 61 | // CompileGlob translates pat into a form more convenient for 62 | // matching against paths in the store. 63 | func CompileGlob(pat string) (*Glob, error) { 64 | s, err := translateGlob(pat) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | r, err := regexp.Compile(s) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | return &Glob{pat, s, r}, nil 75 | } 76 | 77 | // MustCompileGlob is like CompileGlob, but it panics if an error occurs, 78 | // simplifying safe initialization of global variables holding glob patterns. 79 | func MustCompileGlob(pat string) *Glob { 80 | g, err := CompileGlob(pat) 81 | if err != nil { 82 | panic(err) 83 | } 84 | return g 85 | } 86 | 87 | func (g *Glob) Match(path string) bool { 88 | return g.r.MatchString(path) 89 | } 90 | 91 | type GlobError string 92 | 93 | func (e GlobError) Error() string { 94 | return "invalid glob pattern: " + string(e) 95 | } 96 | -------------------------------------------------------------------------------- /store/glob_test.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "github.com/bmizerany/assert" 5 | "testing" 6 | ) 7 | 8 | var globs = [][]string{ 9 | {"/", `^/$`}, 10 | {"/a", `^/a$`}, 11 | {"/a.b", `^/a\.b$`}, 12 | {"/a-b", `^/a\-b$`}, 13 | {"/a?", `^/a[^/]$`}, 14 | {"/a/b", `^/a/b$`}, 15 | {"/*", `^/[^/]*$`}, 16 | {"/*/a", `^/[^/]*/a$`}, 17 | {"/*a/b", `^/[^/]*a/b$`}, 18 | {"/a*/b", `^/a[^/]*/b$`}, 19 | {"/a*a/b", `^/a[^/]*a/b$`}, 20 | {"/*a*/b", `^/[^/]*a[^/]*/b$`}, 21 | {"/**", `^/.*$`}, 22 | {"/**/a", `^/.*/a$`}, 23 | } 24 | 25 | var matches = [][]string{ 26 | {"/a/b", "/a/b"}, 27 | {"/a?", "/ab", "/ac"}, 28 | {"/a*", "/a", "/ab", "/abc"}, 29 | {"/a**", "/a", "/ab", "/abc", "/a/", "/a/b", "/ab/c"}, 30 | } 31 | 32 | var nonMatches = [][]string{ 33 | {"/a/b", "/a/c", "/a/", "/a/b/", "/a/bc"}, 34 | {"/a?", "/", "/abc", "/a", "/a/"}, 35 | {"/a*", "/", "/a/", "/ba"}, 36 | {"/a**", "/", "/ba"}, 37 | } 38 | 39 | var dontCompile = []string{ 40 | "", 41 | "a", 42 | "a/", 43 | "/ ", 44 | "/:", 45 | "//", 46 | "/a/", 47 | "/a+b", 48 | "/a^b", 49 | "/a$b", 50 | "/a[b", 51 | "/a]b", 52 | "/a(b", 53 | "/a)b", 54 | "/a世界", 55 | } 56 | 57 | func TestGlobTranslateOk(t *testing.T) { 58 | for _, parts := range globs { 59 | pat, exp := parts[0], parts[1] 60 | got, err := translateGlob(pat) 61 | if got != exp { 62 | t.Errorf("expected %q, but got %q from %q", exp, got, pat) 63 | } 64 | if err != nil { 65 | t.Errorf("in %q, unexpected err %v", pat, err) 66 | } 67 | } 68 | } 69 | 70 | func TestGlobTranslateError(t *testing.T) { 71 | for _, pat := range dontCompile { 72 | re, err := translateGlob(pat) 73 | if err == nil { 74 | t.Errorf("pat %q shouldn't translate, but got %q", pat, re) 75 | continue 76 | } 77 | 78 | glob, err := CompileGlob(pat) 79 | if err == nil { 80 | t.Errorf("pat %q shouldn't compile, but got %#v", pat, glob) 81 | } 82 | } 83 | } 84 | 85 | func TestGlobMatches(t *testing.T) { 86 | for _, parts := range matches { 87 | pat, paths := parts[0], parts[1:] 88 | glob, err := CompileGlob(pat) 89 | assert.Equal(t, nil, err) 90 | for _, path := range paths { 91 | if !glob.Match(path) { 92 | t.Errorf("pat %q should match %q", pat, path) 93 | } 94 | } 95 | } 96 | } 97 | 98 | func TestGlobNonMatches(t *testing.T) { 99 | for _, parts := range nonMatches { 100 | pat, paths := parts[0], parts[1:] 101 | glob, err := CompileGlob(pat) 102 | assert.Equal(t, nil, err) 103 | for _, path := range paths { 104 | if glob.Match(path) { 105 | t.Errorf("pat %q should not match %q", pat, path) 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /server/server_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "code.google.com/p/goprotobuf/proto" 6 | "github.com/bmizerany/assert" 7 | "github.com/ha/doozerd/store" 8 | "io" 9 | 10 | "testing" 11 | ) 12 | 13 | var ( 14 | fooPath = "/foo" 15 | ) 16 | 17 | type bchan chan []byte 18 | 19 | func (b bchan) Write(buf []byte) (int, error) { 20 | b <- buf 21 | return len(buf), nil 22 | } 23 | 24 | func (b bchan) Read(buf []byte) (int, error) { 25 | return 0, io.EOF // not implemented 26 | } 27 | 28 | func mustUnmarshal(b []byte) (r *response) { 29 | r = new(response) 30 | err := proto.Unmarshal(b, r) 31 | if err != nil { 32 | panic(err) 33 | } 34 | return 35 | } 36 | 37 | func assertResponseErrCode(t *testing.T, exp response_Err, c *conn) { 38 | b := c.c.(*bytes.Buffer).Bytes() 39 | assert.T(t, len(b) > 4, b) 40 | assert.Equal(t, &exp, mustUnmarshal(b[4:]).ErrCode) 41 | } 42 | 43 | func TestDelNilFields(t *testing.T) { 44 | c := &conn{ 45 | c: &bytes.Buffer{}, 46 | canWrite: true, 47 | waccess: true, 48 | } 49 | tx := &txn{ 50 | c: c, 51 | req: request{Tag: proto.Int32(1)}, 52 | } 53 | tx.del() 54 | assertResponseErrCode(t, response_MISSING_ARG, c) 55 | } 56 | 57 | func TestSetNilFields(t *testing.T) { 58 | c := &conn{ 59 | c: &bytes.Buffer{}, 60 | canWrite: true, 61 | waccess: true, 62 | } 63 | tx := &txn{ 64 | c: c, 65 | req: request{Tag: proto.Int32(1)}, 66 | } 67 | tx.set() 68 | assertResponseErrCode(t, response_MISSING_ARG, c) 69 | } 70 | 71 | func TestServerNoAccess(t *testing.T) { 72 | b := make(bchan, 2) 73 | c := &conn{ 74 | c: b, 75 | canWrite: true, 76 | st: store.New(), 77 | } 78 | tx := &txn{ 79 | c: c, 80 | req: request{Tag: proto.Int32(1)}, 81 | } 82 | 83 | for i, op := range ops { 84 | if i != int32(request_ACCESS) { 85 | op(tx) 86 | var exp response_Err = response_OTHER 87 | assert.Equal(t, 4, len(<-b), request_Verb_name[i]) 88 | assert.Equal(t, &exp, mustUnmarshal(<-b).ErrCode, request_Verb_name[i]) 89 | } 90 | } 91 | } 92 | 93 | func TestServerRo(t *testing.T) { 94 | b := make(bchan, 2) 95 | c := &conn{ 96 | c: b, 97 | canWrite: true, 98 | st: store.New(), 99 | } 100 | tx := &txn{ 101 | c: c, 102 | req: request{Tag: proto.Int32(1)}, 103 | } 104 | 105 | wops := []int32{int32(request_DEL), int32(request_NOP), int32(request_SET)} 106 | 107 | for _, i := range wops { 108 | op := ops[i] 109 | op(tx) 110 | var exp response_Err = response_OTHER 111 | assert.Equal(t, 4, len(<-b), request_Verb_name[i]) 112 | assert.Equal(t, &exp, mustUnmarshal(<-b).ErrCode, request_Verb_name[i]) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /consensus/m.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. 2 | // source: m.proto 3 | // DO NOT EDIT! 4 | 5 | package consensus 6 | 7 | import proto "code.google.com/p/goprotobuf/proto" 8 | import json "encoding/json" 9 | import math "math" 10 | 11 | // Reference proto, json, and math imports to suppress error if they are not otherwise used. 12 | var _ = proto.Marshal 13 | var _ = &json.SyntaxError{} 14 | var _ = math.Inf 15 | 16 | type msg_Cmd int32 17 | 18 | const ( 19 | msg_NOP msg_Cmd = 0 20 | msg_INVITE msg_Cmd = 1 21 | msg_RSVP msg_Cmd = 2 22 | msg_NOMINATE msg_Cmd = 3 23 | msg_VOTE msg_Cmd = 4 24 | msg_TICK msg_Cmd = 5 25 | msg_PROPOSE msg_Cmd = 6 26 | msg_LEARN msg_Cmd = 7 27 | ) 28 | 29 | var msg_Cmd_name = map[int32]string{ 30 | 0: "NOP", 31 | 1: "INVITE", 32 | 2: "RSVP", 33 | 3: "NOMINATE", 34 | 4: "VOTE", 35 | 5: "TICK", 36 | 6: "PROPOSE", 37 | 7: "LEARN", 38 | } 39 | var msg_Cmd_value = map[string]int32{ 40 | "NOP": 0, 41 | "INVITE": 1, 42 | "RSVP": 2, 43 | "NOMINATE": 3, 44 | "VOTE": 4, 45 | "TICK": 5, 46 | "PROPOSE": 6, 47 | "LEARN": 7, 48 | } 49 | 50 | func (x msg_Cmd) Enum() *msg_Cmd { 51 | p := new(msg_Cmd) 52 | *p = x 53 | return p 54 | } 55 | func (x msg_Cmd) String() string { 56 | return proto.EnumName(msg_Cmd_name, int32(x)) 57 | } 58 | func (x msg_Cmd) MarshalJSON() ([]byte, error) { 59 | return json.Marshal(x.String()) 60 | } 61 | func (x *msg_Cmd) UnmarshalJSON(data []byte) error { 62 | value, err := proto.UnmarshalJSONEnum(msg_Cmd_value, data, "msg_Cmd") 63 | if err != nil { 64 | return err 65 | } 66 | *x = msg_Cmd(value) 67 | return nil 68 | } 69 | 70 | type msg struct { 71 | Cmd *msg_Cmd `protobuf:"varint,1,opt,name=cmd,enum=consensus.msg_Cmd" json:"cmd,omitempty"` 72 | Seqn *int64 `protobuf:"varint,2,opt,name=seqn" json:"seqn,omitempty"` 73 | Crnd *int64 `protobuf:"varint,3,opt,name=crnd" json:"crnd,omitempty"` 74 | Vrnd *int64 `protobuf:"varint,4,opt,name=vrnd" json:"vrnd,omitempty"` 75 | Value []byte `protobuf:"bytes,5,opt,name=value" json:"value,omitempty"` 76 | XXX_unrecognized []byte `json:"-"` 77 | } 78 | 79 | func (this *msg) Reset() { *this = msg{} } 80 | func (this *msg) String() string { return proto.CompactTextString(this) } 81 | func (*msg) ProtoMessage() {} 82 | 83 | func (this *msg) GetCmd() msg_Cmd { 84 | if this != nil && this.Cmd != nil { 85 | return *this.Cmd 86 | } 87 | return 0 88 | } 89 | 90 | func (this *msg) GetSeqn() int64 { 91 | if this != nil && this.Seqn != nil { 92 | return *this.Seqn 93 | } 94 | return 0 95 | } 96 | 97 | func (this *msg) GetCrnd() int64 { 98 | if this != nil && this.Crnd != nil { 99 | return *this.Crnd 100 | } 101 | return 0 102 | } 103 | 104 | func (this *msg) GetVrnd() int64 { 105 | if this != nil && this.Vrnd != nil { 106 | return *this.Vrnd 107 | } 108 | return 0 109 | } 110 | 111 | func (this *msg) GetValue() []byte { 112 | if this != nil { 113 | return this.Value 114 | } 115 | return nil 116 | } 117 | 118 | func init() { 119 | proto.RegisterEnum("consensus.msg_Cmd", msg_Cmd_name, msg_Cmd_value) 120 | } 121 | -------------------------------------------------------------------------------- /web/web.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "code.google.com/p/go.net/websocket" 5 | "encoding/json" 6 | "github.com/ha/doozerd/store" 7 | "io" 8 | "log" 9 | "net" 10 | "net/http" 11 | "runtime" 12 | "strings" 13 | "text/template" 14 | ) 15 | 16 | var Store *store.Store 17 | var ClusterName string 18 | 19 | var ( 20 | mainTpl = template.Must(template.New("main.html").Parse(main_html)) 21 | statsTpl = template.Must(template.New("stats.html").Parse(stats_html)) 22 | ) 23 | 24 | type info struct { 25 | Name string 26 | Path string 27 | } 28 | 29 | type stringHandler struct { 30 | contentType string 31 | body string 32 | } 33 | 34 | func (sh stringHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 35 | w.Header().Set("content-type", sh.contentType) 36 | io.WriteString(w, sh.body) 37 | } 38 | 39 | func Serve(listener net.Listener) { 40 | http.HandleFunc("/", viewHtml) 41 | http.HandleFunc("/$stats.html", statsHtml) 42 | http.Handle("/$main.js", stringHandler{"application/javascript", main_js}) 43 | http.Handle("/$main.css", stringHandler{"text/css", main_css}) 44 | http.HandleFunc("/$events/", evServer) 45 | 46 | http.Serve(listener, nil) 47 | } 48 | 49 | func send(ws *websocket.Conn, path string, evs <-chan store.Event) { 50 | l := len(path) - 1 51 | for ev := range evs { 52 | ev.Getter = nil // don't marshal the entire snapshot 53 | ev.Path = ev.Path[l:] 54 | b, err := json.Marshal(ev) 55 | if err != nil { 56 | log.Println(err) 57 | return 58 | } 59 | _, err = ws.Write(b) 60 | if err != nil { 61 | log.Println(err) 62 | return 63 | } 64 | } 65 | } 66 | 67 | func evServer(w http.ResponseWriter, r *http.Request) { 68 | wevs := make(chan store.Event) 69 | path := r.URL.Path[len("/$events"):] 70 | 71 | glob, err := store.CompileGlob(path + "**") 72 | if err != nil { 73 | w.WriteHeader(400) 74 | return 75 | } 76 | 77 | rev, _ := Store.Snap() 78 | 79 | go func() { 80 | walk(path, Store, wevs) 81 | for { 82 | ch, err := Store.Wait(glob, rev+1) 83 | if err != nil { 84 | break 85 | } 86 | ev, ok := <-ch 87 | if !ok { 88 | break 89 | } 90 | wevs <- ev 91 | rev = ev.Seqn 92 | } 93 | close(wevs) 94 | }() 95 | 96 | websocket.Handler(func(ws *websocket.Conn) { 97 | send(ws, path, wevs) 98 | ws.Close() 99 | }).ServeHTTP(w, r) 100 | } 101 | 102 | func viewHtml(w http.ResponseWriter, r *http.Request) { 103 | if !strings.HasSuffix(r.URL.Path, "/") { 104 | w.WriteHeader(404) 105 | return 106 | } 107 | var x info 108 | x.Name = ClusterName 109 | x.Path = r.URL.Path 110 | w.Header().Set("content-type", "text/html") 111 | mainTpl.Execute(w, x) 112 | } 113 | 114 | func statsHtml(w http.ResponseWriter, r *http.Request) { 115 | w.Header().Set("content-type", "text/html") 116 | memstats := new(runtime.MemStats) 117 | runtime.ReadMemStats(memstats) 118 | statsTpl.Execute(w, *memstats) 119 | } 120 | 121 | func walk(path string, st *store.Store, ch chan store.Event) { 122 | for path != "/" && strings.HasSuffix(path, "/") { 123 | // TODO generalize and factor this into pkg store. 124 | path = path[0 : len(path)-1] 125 | } 126 | v, rev := st.Get(path) 127 | if rev != store.Dir { 128 | ch <- store.Event{0, path, v[0], rev, "", nil, nil} 129 | return 130 | } 131 | if path == "/" { 132 | path = "" 133 | } 134 | for _, ent := range v { 135 | walk(path+"/"+ent, st, ch) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /store/node_test.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "github.com/bmizerany/assert" 5 | "syscall" 6 | "testing" 7 | ) 8 | 9 | func TestNodeApplySet(t *testing.T) { 10 | k, v, seqn, rev := "x", "a", int64(1), int64(1) 11 | p := "/" + k 12 | m := MustEncodeSet(p, v, Clobber) 13 | n, e := emptyDir.apply(seqn, m) 14 | exp := node{"", Dir, map[string]node{k: {v, rev, nil}}} 15 | assert.Equal(t, exp, n) 16 | assert.Equal(t, Event{seqn, p, v, rev, m, nil, n}, e) 17 | } 18 | 19 | func TestNodeApplyDel(t *testing.T) { 20 | k, seqn, rev := "x", int64(1), int64(1) 21 | r := node{"", Dir, map[string]node{k: {"a", rev, nil}}} 22 | p := "/" + k 23 | m := MustEncodeDel(p, rev) 24 | n, e := r.apply(seqn, m) 25 | assert.Equal(t, emptyDir, n) 26 | assert.Equal(t, Event{seqn, p, "", Missing, m, nil, n}, e) 27 | } 28 | 29 | func TestNodeApplyNop(t *testing.T) { 30 | seqn := int64(1) 31 | m := Nop 32 | n, e := emptyDir.apply(seqn, m) 33 | assert.Equal(t, emptyDir, n) 34 | assert.Equal(t, Event{seqn, "/", "", nop, m, nil, n}, e) 35 | } 36 | 37 | func TestNodeApplyBadMutation(t *testing.T) { 38 | seqn, rev := int64(1), int64(1) 39 | m := BadMutations[0] 40 | n, e := emptyDir.apply(seqn, m) 41 | exp := node{"", Dir, map[string]node{"ctl": {"", Dir, map[string]node{"err": {ErrBadMutation.Error(), rev, nil}}}}} 42 | assert.Equal(t, exp, n) 43 | assert.Equal(t, Event{seqn, ErrorPath, ErrBadMutation.Error(), rev, m, ErrBadMutation, n}, e) 44 | } 45 | 46 | func TestNodeApplyBadInstruction(t *testing.T) { 47 | seqn, rev := int64(1), int64(1) 48 | m := "-1:x" 49 | n, e := emptyDir.apply(seqn, m) 50 | err := ErrBadPath 51 | exp := node{"", Dir, map[string]node{"ctl": {"", Dir, map[string]node{"err": {err.Error(), rev, nil}}}}} 52 | assert.Equal(t, exp, n) 53 | assert.Equal(t, Event{seqn, ErrorPath, err.Error(), rev, m, err, n}, e) 54 | } 55 | 56 | func TestNodeApplyRevMismatch(t *testing.T) { 57 | k, v, seqn, rev := "x", "a", int64(1), int64(1) 58 | p := "/" + k 59 | 60 | // -123 is less that the current rev, which is zero; and not Clobber. 61 | m := MustEncodeSet(p, v, -123) 62 | n, e := emptyDir.apply(seqn, m) 63 | 64 | err := ErrRevMismatch 65 | exp := node{"", Dir, map[string]node{"ctl": {"", Dir, map[string]node{"err": {err.Error(), rev, nil}}}}} 66 | assert.Equal(t, exp, n) 67 | assert.Equal(t, Event{seqn, ErrorPath, err.Error(), rev, m, err, n}, e) 68 | } 69 | 70 | func TestNodeNotADirectory(t *testing.T) { 71 | r, _ := emptyDir.apply(1, MustEncodeSet("/x", "a", Clobber)) 72 | m := MustEncodeSet("/x/y", "b", Clobber) 73 | n, e := r.apply(2, m) 74 | err := syscall.ENOTDIR 75 | exp, _ := r.apply(2, MustEncodeSet("/ctl/err", err.Error(), Clobber)) 76 | assert.Equal(t, exp, n) 77 | assert.Equal(t, Event{2, ErrorPath, err.Error(), 2, m, err, n}, e) 78 | } 79 | 80 | func TestNodeNotADirectoryDeeper(t *testing.T) { 81 | r, _ := emptyDir.apply(1, MustEncodeSet("/x", "a", Clobber)) 82 | m := MustEncodeSet("/x/y/z/w", "b", Clobber) 83 | n, e := r.apply(2, m) 84 | err := syscall.ENOTDIR 85 | exp, _ := r.apply(2, MustEncodeSet("/ctl/err", err.Error(), Clobber)) 86 | assert.Equal(t, exp, n) 87 | assert.Equal(t, Event{2, ErrorPath, err.Error(), 2, m, err, n}, e) 88 | } 89 | 90 | func TestNodeIsADirectory(t *testing.T) { 91 | r, _ := emptyDir.apply(1, MustEncodeSet("/x/y", "a", Clobber)) 92 | m := MustEncodeSet("/x", "b", Clobber) 93 | n, e := r.apply(2, m) 94 | err := syscall.EISDIR 95 | exp, _ := r.apply(2, MustEncodeSet("/ctl/err", err.Error(), Clobber)) 96 | assert.Equal(t, exp, n) 97 | assert.Equal(t, Event{2, ErrorPath, err.Error(), 2, m, err, n}, e) 98 | } 99 | -------------------------------------------------------------------------------- /bin/doozer_init: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o pipefail 3 | 4 | function log() { 5 | echo "[`date +'%Y%m%d %H:%M:%S'`] $@" 6 | } 7 | 8 | # doozer cluster initialization script 9 | 10 | DOOZERCLI="/usr/local/bin/doozer" 11 | DOOZERD_NODES="" 12 | HOST="127.0.0.1" 13 | PORT="9200" 14 | 15 | while [ "$1" != "" ]; do 16 | PARAM=`echo $1 | awk -F= '{print $1}'` 17 | VALUE=`echo $1 | awk -F= '{print $2}'` 18 | case $PARAM in 19 | --doozer-cli) 20 | DOOZERCLI="$VALUE" 21 | ;; 22 | --doozerd-nodes) 23 | DOOZERD_NODES="$VALUE" 24 | ;; 25 | --port) 26 | PORT="$VALUE" 27 | ;; 28 | --host) 29 | HOST="$VALUE" 30 | ;; 31 | esac 32 | shift 33 | done 34 | 35 | if [ -z $PORT ]; then 36 | log "ERROR: --port cannot be empty" 37 | exit 1 38 | fi 39 | 40 | if [ -z $HOST ]; then 41 | log "ERROR: --host cannot be empty" 42 | exit 1 43 | fi 44 | 45 | if [ -z $DOOZERD_NODES ]; then 46 | log "ERROR: --doozerd-nodes cannot be empty" 47 | exit 1 48 | fi 49 | 50 | log "DOOZERD_NODES: $DOOZERD_NODES" 51 | 52 | # first identify which node to bind to (ie. the root node) 53 | bind_node="" 54 | for doozerd_node in $DOOZERD_NODES; do 55 | doozerd_node_hostname=`echo $doozerd_node | awk -F: '{print $1}'` 56 | doozerd_node_port=`echo $doozerd_node | awk -F: '{print $2}'` 57 | 58 | # resolve the ip (doozerd needs the exact ip to bind to, does not work with 0.0.0.0) 59 | doozerd_node_ip=`host $doozerd_node_hostname 2>/dev/null | tail -1 | awk '{print $NF}'` 60 | if $DOOZERCLI -a="doozer:?ca=$doozerd_node_ip:$doozerd_node_port" nop; then 61 | bind_node="$doozerd_node_ip:$doozerd_node_port" 62 | break 63 | fi 64 | done 65 | 66 | if [ -z $bind_node ]; then 67 | log "ERROR: could not find node to bind to" 68 | exit 1 69 | fi 70 | 71 | log "bind_node: $bind_node" 72 | 73 | this_node_ip=`host $FQ_HOSTNAME 2>/dev/null | tail -1 | awk '{print $NF}'` 74 | host_node="$this_node_ip:$PORT" 75 | 76 | log "host_node: $host_node" 77 | 78 | my_self=`$DOOZERCLI -a="doozer:?ca=$host_node" self` 79 | if [ -z $my_self ]; then 80 | log "ERROR: could not identify self" 81 | exit 1 82 | fi 83 | 84 | log "my_self: $my_self" 85 | 86 | my_cal=$(printf %d $(expr $(echo $FQ_HOSTNAME | awk -F. '{print $1}' | tail -c -3) - 1)) 87 | if [ -z $my_cal ]; then 88 | log "ERROR: could not identify cal" 89 | exit 1 90 | fi 91 | 92 | log "my_cal: $my_cal" 93 | 94 | log "checking /ctl/cal/$my_cal" 95 | stat_output=`$DOOZERCLI -a="doozer:?ca=$bind_node" stat /ctl/cal/$my_cal 2>/dev/null` 96 | if [ "$?" == "0" ]; then 97 | log "/ctl/cal/$my_cal exists" 98 | cal_self=`$DOOZERCLI -a="doozer:?ca=$bind_node" get /ctl/cal/$my_cal 2>/dev/null` 99 | if [ "$cal_self" == "$my_self" ]; then 100 | log "NOTICE: nothing to do (we're already active in the cluster)" 101 | exit 0 102 | fi 103 | rev=`echo $stat_output | awk '{print $1}'` 104 | log "deleting /ctl/cal/$my_cal @ $rev" 105 | $DOOZERCLI -a="doozer:?ca=$bind_node" del /ctl/cal/$my_cal $rev >/dev/null 2>&1 106 | if [ "$?" != "0" ]; then 107 | log "ERROR: failed to delete /ctl/cal/$my_cal" 108 | exit 1 109 | fi 110 | fi 111 | 112 | log "adding /ctl/cal/$my_cal" 113 | echo -n | $DOOZERCLI -a="doozer:?ca=$bind_node" add /ctl/cal/$my_cal 114 | if [ "$?" != "0" ]; then 115 | log "ERROR: failed to add /ctl/cal/$my_cal" 116 | exit 1 117 | fi 118 | 119 | log "SUCCESS" 120 | -------------------------------------------------------------------------------- /store/getter_test.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "github.com/bmizerany/assert" 5 | "sort" 6 | "testing" 7 | ) 8 | 9 | func TestGetString(t *testing.T) { 10 | st := New() 11 | st.Ops <- Op{1, MustEncodeSet("/x", "a", Clobber)} 12 | sync(st, 1) 13 | assert.Equal(t, "a", GetString(st, "/x")) 14 | } 15 | 16 | func TestGetStringMissing(t *testing.T) { 17 | st := New() 18 | assert.Equal(t, "", GetString(st, "/x")) 19 | } 20 | 21 | func TestGetStringDir(t *testing.T) { 22 | st := New() 23 | st.Ops <- Op{1, MustEncodeSet("/x/y", "a", Clobber)} 24 | sync(st, 1) 25 | assert.Equal(t, "", GetString(st, "/x")) 26 | } 27 | 28 | func TestGetdir(t *testing.T) { 29 | st := New() 30 | st.Ops <- Op{1, MustEncodeSet("/x/y", "a", Clobber)} 31 | sync(st, 1) 32 | assert.Equal(t, []string{"y"}, Getdir(st, "/x")) 33 | } 34 | 35 | func TestGetdirMissing(t *testing.T) { 36 | st := New() 37 | assert.Equal(t, []string(nil), Getdir(st, "/x")) 38 | } 39 | 40 | func TestGetdirString(t *testing.T) { 41 | st := New() 42 | st.Ops <- Op{1, MustEncodeSet("/x", "a", Clobber)} 43 | sync(st, 1) 44 | assert.Equal(t, []string(nil), Getdir(st, "/x")) 45 | } 46 | 47 | func TestWalk(t *testing.T) { 48 | exp := map[string]string{ 49 | "/d/x": "1", 50 | "/d/y": "2", 51 | "/d/z/a": "3", 52 | } 53 | var expPaths []string 54 | for p := range exp { 55 | expPaths = append(expPaths, p) 56 | } 57 | sort.Strings(expPaths) 58 | 59 | st := New() 60 | st.Ops <- Op{1, MustEncodeSet("/d/x", "1", Clobber)} 61 | st.Ops <- Op{2, MustEncodeSet("/d/y", "2", Clobber)} 62 | st.Ops <- Op{3, MustEncodeSet("/d/z/a", "3", Clobber)} 63 | st.Ops <- Op{4, MustEncodeSet("/m/y", "", Clobber)} 64 | st.Ops <- Op{5, MustEncodeSet("/n", "", Clobber)} 65 | glob, err := CompileGlob("/d/**") 66 | assert.Equal(t, nil, err) 67 | var c int 68 | b := Walk(st, glob, func(path, body string, rev int64) bool { 69 | assert.Equal(t, expPaths[0], path) 70 | assert.Equal(t, exp[path], body) 71 | c++ 72 | expPaths = expPaths[1:] 73 | return false 74 | }) 75 | assert.Equal(t, false, b) 76 | assert.Equal(t, 3, c) 77 | } 78 | 79 | func TestWalkOneLevel(t *testing.T) { 80 | exp := [][2]string{ 81 | {"/d/a/z", "3"}, 82 | } 83 | 84 | st := New() 85 | st.Ops <- Op{1, MustEncodeSet("/d/x", "1", Clobber)} 86 | st.Ops <- Op{2, MustEncodeSet("/d/y", "2", Clobber)} 87 | st.Ops <- Op{3, MustEncodeSet("/d/a/z", "3", Clobber)} 88 | sync(st, 3) 89 | got := [][2]string{} 90 | Walk(st, MustCompileGlob("/d/*/*"), func(path, body string, rev int64) bool { 91 | got = append(got, [2]string{path, body}) 92 | return false 93 | }) 94 | assert.Equal(t, exp, got) 95 | } 96 | 97 | func TestWalkStop(t *testing.T) { 98 | exp := map[string]string{ 99 | "/d/x": "1", 100 | "/d/y": "2", 101 | "/d/z/a": "3", 102 | } 103 | var expPaths []string 104 | for p := range exp { 105 | expPaths = append(expPaths, p) 106 | } 107 | sort.Strings(expPaths) 108 | 109 | st := New() 110 | st.Ops <- Op{1, MustEncodeSet("/d/x", "1", Clobber)} 111 | st.Ops <- Op{2, MustEncodeSet("/d/y", "2", Clobber)} 112 | st.Ops <- Op{3, MustEncodeSet("/d/z/a", "3", Clobber)} 113 | st.Ops <- Op{4, MustEncodeSet("/m/y", "", Clobber)} 114 | st.Ops <- Op{5, MustEncodeSet("/n", "", Clobber)} 115 | glob, err := CompileGlob("/d/**") 116 | assert.Equal(t, nil, err) 117 | var c int 118 | b := Walk(st, glob, func(path, body string, rev int64) bool { 119 | assert.Equal(t, expPaths[0], path) 120 | assert.Equal(t, exp[path], body) 121 | c++ 122 | expPaths = expPaths[1:] 123 | return true 124 | }) 125 | assert.Equal(t, true, b) 126 | assert.Equal(t, 1, c) 127 | } 128 | -------------------------------------------------------------------------------- /web/main.js.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | // This file was generated from web/main.js. 4 | 5 | var main_js string = "var deadline = 0, retry_interval = 0;\nvar ti;\n\nfunction insert(parent, child) {\n var existing = parent.children();\n var before = null;\n existing.each(function () {\n var jq = $(this);\n if (jq.attr('name') < child.attr('name')) {\n before = jq;\n }\n });\n if (before === null) {\n parent.prepend(child);\n } else {\n before.after(child);\n }\n}\n\nfunction apply(ev) {\n var parts = ev.Path.split(\"/\")\n if (parts.length < 2) {\n return\n }\n parts = parts.slice(1); // omit leading empty string\n var dir_parts = parts.slice(0, parts.length - 1);\n var dir = $('#root');\n for (var i = 0; i < dir_parts.length; i++) {\n var part = dir_parts[i];\n var next = dir.find('> dl > div[name=\"'+part+'\"] > dd');\n if (next.length < 1) {\n var div = $('
').attr('name', part);\n var dd = $('
');\n div.append($('
').text(part+'/')).append(dd);\n insert(dir.children('dl'), div);\n dd.append('
').append('');\n next = dd;\n }\n dir = next;\n }\n\n var basename = parts[parts.length - 1];\n var entry = dir.find('tr[name=\"'+basename+'\"]');\n if (entry.length < 1) {\n var tr = $('').attr('name', basename);\n insert(dir.children('table').children('tbody'), tr);\n tr.append($('
').text(basename)).\n append('').\n append('').\n append('');\n entry = tr;\n }\n entry.children('td.rev').text('('+ev.Rev+')');\n entry.children('td.body').text(ev.Body);\n entry.addClass('new');\n\n // Kick off the transition in a bit.\n setTimeout(function() { entry.removeClass('new') }, 550);\n}\n\nfunction time_interval(s) {\n if (s < 120) return Math.ceil(s) + 's';\n if (s < 7200) return Math.round(s/60) + 'm';\n return Math.round(s/3600) + 'h';\n}\n\nfunction countdown() {\n var body = $('body');\n var eta = (deadline - new Date().getTime())/1000;\n if (eta < 0) {\n body.removeClass('waiting');\n open();\n } else {\n $('#retrymsg').text(\"retrying in \" + time_interval(eta));\n body.addClass('waiting');\n ti = setTimeout(countdown, Math.max(100, eta*9));\n }\n}\n\nfunction retry() {\n deadline = ((new Date()).getTime()) + retry_interval * 1000;\n retry_interval += (retry_interval + 5) * (Math.random() + .5);\n countdown();\n}\n\nfunction open() {\n var body = $('body');\n var status = $('#status');\n status.text(\"connecting\");\n var ws = new WebSocket(\"ws://\"+location.host+\"/$events\"+path);\n ws.onmessage = function (ev) {\n var jev = JSON.parse(ev.data);\n apply(jev);\n };\n ws.onopen = function(ev) {\n if (retry_interval > 0) {\n body.addClass('wereback');\n setTimeout(function () { body.removeClass('wereback') }, 8000);\n }\n retry_interval = 0;\n status.text('open')\n body.addClass('open').removeClass('loading closed error');\n $('#root > dl > *, #root > table > tbody > *').remove();\n };\n ws.onclose = function(ev) {\n status.text('closed')\n body.addClass('closed').removeClass('loading open error wereback');\n retry();\n };\n ws.onerror = function(ev) {\n status.text('error ' + ev)\n body.addClass('error').removeClass('loading open closed wereback');\n retry();\n };\n}\n\nfunction dr() {\n $('#trynow').click(function() {\n clearTimeout(ti);\n deadline = 0;\n countdown();\n });\n\n if (\"WebSocket\" in window) {\n open();\n } else {\n $('#status').text(\"your browser does not provide websockets\");\n $('body').addClass('error nows').removeClass('loading open closed wereback');\n }\n}\n\nfunction jerr() {\n const m = 'could not load jquery (is your network link down?)';\n document.getElementById('status').innerText = m;\n document.getElementsByTagName('body')[0].className = 'error';\n}\n" 6 | -------------------------------------------------------------------------------- /consensus/consensus_test.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | import ( 4 | "errors" 5 | "github.com/bmizerany/assert" 6 | "github.com/ha/doozerd/store" 7 | "net" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestConsensusOne(t *testing.T) { 13 | self := "test" 14 | const alpha = 1 15 | st := store.New() 16 | 17 | st.Ops <- store.Op{1, store.MustEncodeSet("/ctl/node/"+self+"/addr", "1.2.3.4:5", 0)} 18 | st.Ops <- store.Op{2, store.MustEncodeSet("/ctl/cal/1", self, 0)} 19 | <-st.Seqns 20 | 21 | in := make(chan Packet) 22 | out := make(chan Packet) 23 | seqns := make(chan int64, alpha) 24 | props := make(chan *Prop) 25 | 26 | m := &Manager{ 27 | Self: self, 28 | DefRev: 2, 29 | Alpha: alpha, 30 | In: in, 31 | Out: out, 32 | Ops: st.Ops, 33 | PSeqn: seqns, 34 | Props: props, 35 | TFill: 10e9, 36 | Store: st, 37 | Ticker: time.Tick(10e6), 38 | } 39 | go m.Run() 40 | 41 | go func() { 42 | for o := range out { 43 | in <- o 44 | } 45 | }() 46 | 47 | n := <-seqns 48 | w, err := st.Wait(store.Any, n) 49 | if err != nil { 50 | panic(err) 51 | } 52 | props <- &Prop{n, []byte("foo")} 53 | e := <-w 54 | 55 | exp := store.Event{ 56 | Seqn: 3, 57 | Path: "/ctl/err", 58 | Body: "bad mutation", 59 | Rev: 3, 60 | Mut: "foo", 61 | Err: errors.New("bad mutation"), 62 | } 63 | 64 | e.Getter = nil 65 | assert.Equal(t, exp, e) 66 | } 67 | 68 | func TestConsensusTwo(t *testing.T) { 69 | a := "a" 70 | b := "b" 71 | x, _ := net.ResolveUDPAddr("udp", "1.2.3.4:5") 72 | xs := "1.2.3.4:5" 73 | y, _ := net.ResolveUDPAddr("udp", "2.3.4.5:6") 74 | ys := "2.3.4.5:6" 75 | const alpha = 1 76 | st := store.New() 77 | 78 | st.Ops <- store.Op{1, store.Nop} 79 | st.Ops <- store.Op{2, store.MustEncodeSet("/ctl/node/a/addr", xs, 0)} 80 | st.Ops <- store.Op{3, store.MustEncodeSet("/ctl/cal/1", a, 0)} 81 | st.Ops <- store.Op{4, store.MustEncodeSet("/ctl/node/b/addr", ys, 0)} 82 | st.Ops <- store.Op{5, store.MustEncodeSet("/ctl/cal/2", b, 0)} 83 | 84 | ain := make(chan Packet) 85 | aout := make(chan Packet) 86 | aseqns := make(chan int64, alpha) 87 | aprops := make(chan *Prop) 88 | am := &Manager{ 89 | Self: a, 90 | DefRev: 5, 91 | Alpha: alpha, 92 | In: ain, 93 | Out: aout, 94 | Ops: st.Ops, 95 | PSeqn: aseqns, 96 | Props: aprops, 97 | TFill: 10e9, 98 | Store: st, 99 | Ticker: time.Tick(10e6), 100 | } 101 | go am.Run() 102 | 103 | bin := make(chan Packet) 104 | bout := make(chan Packet) 105 | bseqns := make(chan int64, alpha) 106 | bprops := make(chan *Prop) 107 | bm := &Manager{ 108 | Self: b, 109 | DefRev: 5, 110 | Alpha: alpha, 111 | In: bin, 112 | Out: bout, 113 | Ops: st.Ops, 114 | PSeqn: bseqns, 115 | Props: bprops, 116 | TFill: 10e9, 117 | Store: st, 118 | Ticker: time.Tick(10e6), 119 | } 120 | go bm.Run() 121 | 122 | go func() { 123 | for o := range aout { 124 | if o.Addr.Port == x.Port && o.Addr.IP.Equal(x.IP) { 125 | go func(o Packet) { ain <- o }(o) 126 | } else { 127 | o.Addr = x 128 | go func(o Packet) { bin <- o }(o) 129 | } 130 | } 131 | }() 132 | 133 | go func() { 134 | for o := range bout { 135 | if o.Addr.Port == y.Port && o.Addr.IP.Equal(y.IP) { 136 | go func(o Packet) { bin <- o }(o) 137 | } else { 138 | o.Addr = y 139 | go func(o Packet) { ain <- o }(o) 140 | } 141 | } 142 | }() 143 | 144 | n := <-aseqns 145 | assert.Equal(t, int64(6), n) 146 | w, err := st.Wait(store.Any, n) 147 | if err != nil { 148 | panic(err) 149 | } 150 | aprops <- &Prop{n, []byte("foo")} 151 | e := <-w 152 | 153 | exp := store.Event{ 154 | Seqn: 6, 155 | Path: "/ctl/err", 156 | Body: "bad mutation", 157 | Rev: 6, 158 | Mut: "foo", 159 | Err: errors.New("bad mutation"), 160 | } 161 | 162 | e.Getter = nil 163 | assert.Equal(t, exp, e) 164 | } 165 | -------------------------------------------------------------------------------- /store/node.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "syscall" 5 | ) 6 | 7 | var emptyDir = node{V: "", Ds: make(map[string]node), Rev: Dir} 8 | 9 | const ErrorPath = "/ctl/err" 10 | 11 | const Nop = "nop:" 12 | 13 | // This structure should be kept immutable. 14 | type node struct { 15 | V string 16 | Rev int64 17 | Ds map[string]node 18 | } 19 | 20 | func (n node) String() string { 21 | return "" 22 | } 23 | 24 | func (n node) readdir() []string { 25 | names := make([]string, len(n.Ds)) 26 | i := 0 27 | for name := range n.Ds { 28 | names[i] = name 29 | i++ 30 | } 31 | return names 32 | } 33 | 34 | func (n node) at(parts []string) (node, error) { 35 | switch len(parts) { 36 | case 0: 37 | return n, nil 38 | default: 39 | if n.Ds != nil { 40 | if m, ok := n.Ds[parts[0]]; ok { 41 | return m.at(parts[1:]) 42 | } 43 | } 44 | return node{}, syscall.ENOENT 45 | } 46 | panic("unreachable") 47 | } 48 | 49 | func (n node) get(parts []string) ([]string, int64) { 50 | switch m, err := n.at(parts); err { 51 | case syscall.ENOENT: 52 | return []string{""}, Missing 53 | default: 54 | if len(m.Ds) > 0 { 55 | return m.readdir(), m.Rev 56 | } else { 57 | return []string{m.V}, m.Rev 58 | } 59 | } 60 | panic("unreachable") 61 | } 62 | 63 | func (n node) Get(path string) ([]string, int64) { 64 | return n.get(split(path)) 65 | } 66 | 67 | func (n node) stat(parts []string) (int32, int64) { 68 | switch m, err := n.at(parts); err { 69 | case syscall.ENOENT: 70 | return 0, Missing 71 | default: 72 | l := len(m.Ds) 73 | if l > 0 { 74 | return int32(l), m.Rev 75 | } else { 76 | return int32(len(m.V)), m.Rev 77 | } 78 | } 79 | panic("unreachable") 80 | } 81 | 82 | func (n node) Stat(path string) (int32, int64) { 83 | if err := checkPath(path); err != nil { 84 | return 0, Missing 85 | } 86 | 87 | return n.stat(split(path)) 88 | } 89 | 90 | func copyMap(a map[string]node) map[string]node { 91 | b := make(map[string]node) 92 | for k, v := range a { 93 | b[k] = v 94 | } 95 | return b 96 | } 97 | 98 | // Return value is replacement node 99 | func (n node) set(parts []string, v string, rev int64, keep bool) (node, bool) { 100 | if len(parts) == 0 { 101 | return node{v, rev, n.Ds}, keep 102 | } 103 | 104 | n.Ds = copyMap(n.Ds) 105 | p, ok := n.Ds[parts[0]].set(parts[1:], v, rev, keep) 106 | if ok { 107 | n.Ds[parts[0]] = p 108 | } else { 109 | delete(n.Ds, parts[0]) 110 | } 111 | n.Rev = Dir 112 | return n, len(n.Ds) > 0 113 | } 114 | 115 | func (n node) setp(k, v string, rev int64, keep bool) node { 116 | if err := checkPath(k); err != nil { 117 | return n 118 | } 119 | 120 | n, _ = n.set(split(k), v, rev, keep) 121 | return n 122 | } 123 | 124 | func (n node) apply(seqn int64, mut string) (rep node, ev Event) { 125 | ev.Seqn, ev.Rev, ev.Mut = seqn, seqn, mut 126 | if mut == Nop { 127 | ev.Path = "/" 128 | ev.Rev = nop 129 | rep = n 130 | ev.Getter = rep 131 | return 132 | } 133 | 134 | var rev int64 135 | var keep bool 136 | ev.Path, ev.Body, rev, keep, ev.Err = decode(mut) 137 | 138 | if ev.Err == nil && keep { 139 | components := split(ev.Path) 140 | for i := 0; i < len(components)-1; i++ { 141 | _, dirRev := n.get(components[0 : i+1]) 142 | if dirRev == Missing { 143 | break 144 | } 145 | if dirRev != Dir { 146 | ev.Err = syscall.ENOTDIR 147 | break 148 | } 149 | } 150 | } 151 | 152 | if ev.Err == nil { 153 | _, curRev := n.Get(ev.Path) 154 | if rev != Clobber && rev < curRev { 155 | ev.Err = ErrRevMismatch 156 | } else if curRev == Dir { 157 | ev.Err = syscall.EISDIR 158 | } 159 | } 160 | 161 | if ev.Err != nil { 162 | ev.Path, ev.Body, rev, keep = ErrorPath, ev.Err.Error(), Clobber, true 163 | } 164 | 165 | if !keep { 166 | ev.Rev = Missing 167 | } 168 | 169 | rep = n.setp(ev.Path, ev.Body, ev.Rev, keep) 170 | ev.Getter = rep 171 | return 172 | } 173 | -------------------------------------------------------------------------------- /doozerd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | _ "expvar" 6 | "flag" 7 | "fmt" 8 | "github.com/ha/doozer" 9 | "github.com/ha/doozerd/peer" 10 | "log" 11 | "net" 12 | "os" 13 | "strconv" 14 | ) 15 | 16 | const defWebPort = 8000 17 | 18 | type strings []string 19 | 20 | func (a *strings) Set(s string) error { 21 | *a = append(*a, s) 22 | return nil 23 | } 24 | 25 | func (a *strings) String() string { 26 | return fmt.Sprint(*a) 27 | } 28 | 29 | var ( 30 | laddr = flag.String("l", "127.0.0.1:8046", "The address to bind to.") 31 | aaddrs = strings{} 32 | buri = flag.String("b", "", "boot cluster uri (tried after -a)") 33 | waddr = flag.String("w", "", "web listen addr (default: see below)") 34 | name = flag.String("c", "local", "The non-empty cluster name.") 35 | showVersion = flag.Bool("v", false, "print doozerd's version string") 36 | pi = flag.Float64("pulse", 1, "how often (in seconds) to set applied key") 37 | fd = flag.Float64("fill", .1, "delay (in seconds) to fill unowned seqns") 38 | kt = flag.Float64("timeout", 60, "timeout (in seconds) to kick inactive nodes") 39 | hi = flag.Int64("hist", 2000, "length of history/revisions to keep") 40 | certFile = flag.String("tlscert", "", "TLS public certificate") 41 | keyFile = flag.String("tlskey", "", "TLS private key") 42 | ) 43 | 44 | var ( 45 | rwsk = os.Getenv("DOOZER_RWSECRET") 46 | rosk = os.Getenv("DOOZER_ROSECRET") 47 | ) 48 | 49 | func init() { 50 | flag.Var(&aaddrs, "a", "attach address (may be given multiple times)") 51 | } 52 | 53 | func Usage() { 54 | fmt.Fprintf(os.Stderr, "Usage: %s [OPTIONS]\n", os.Args[0]) 55 | fmt.Fprintf(os.Stderr, "\nOptions:\n") 56 | flag.PrintDefaults() 57 | fmt.Fprintf(os.Stderr, ` 58 | The default for -w is to use the addr from -l, 59 | and change the port to 8000. If you give "-w false", 60 | doozerd will not listen for for web connections. 61 | `) 62 | } 63 | 64 | func main() { 65 | *buri = os.Getenv("DOOZER_BOOT_URI") 66 | 67 | flag.Usage = Usage 68 | flag.Parse() 69 | 70 | if *showVersion { 71 | fmt.Println("doozerd", peer.Version) 72 | return 73 | } 74 | 75 | if *laddr == "" { 76 | fmt.Fprintln(os.Stderr, "require a listen address") 77 | flag.Usage() 78 | os.Exit(1) 79 | } 80 | 81 | log.SetPrefix("DOOZER ") 82 | log.SetFlags(log.Ldate | log.Lmicroseconds) 83 | 84 | tsock, err := net.Listen("tcp", *laddr) 85 | if err != nil { 86 | panic(err) 87 | } 88 | 89 | if *certFile != "" || *keyFile != "" { 90 | tsock = tlsWrap(tsock, *certFile, *keyFile) 91 | } 92 | 93 | uaddr, err := net.ResolveUDPAddr("udp", *laddr) 94 | if err != nil { 95 | panic(err) 96 | } 97 | 98 | usock, err := net.ListenUDP("udp", uaddr) 99 | if err != nil { 100 | panic(err) 101 | } 102 | 103 | var wsock net.Listener 104 | if *waddr == "" { 105 | wa, err := net.ResolveTCPAddr("tcp", *laddr) 106 | if err != nil { 107 | panic(err) 108 | } 109 | wa.Port = defWebPort 110 | *waddr = wa.String() 111 | } 112 | if b, err := strconv.ParseBool(*waddr); err != nil && !b { 113 | wsock, err = net.Listen("tcp", *waddr) 114 | if err != nil { 115 | panic(err) 116 | } 117 | } 118 | 119 | id := randId() 120 | var cl *doozer.Conn 121 | switch { 122 | case len(aaddrs) > 0 && *buri != "": 123 | cl = attach(*name, aaddrs) 124 | if cl == nil { 125 | cl = boot(*name, id, *laddr, *buri) 126 | } 127 | case len(aaddrs) > 0: 128 | cl = attach(*name, aaddrs) 129 | if cl == nil { 130 | panic("failed to attach") 131 | } 132 | case *buri != "": 133 | cl = boot(*name, id, *laddr, *buri) 134 | } 135 | 136 | peer.Main(*name, id, *buri, rwsk, rosk, cl, usock, tsock, wsock, ns(*pi), ns(*fd), ns(*kt), *hi) 137 | panic("main exit") 138 | } 139 | 140 | func ns(x float64) int64 { 141 | return int64(x * 1e9) 142 | } 143 | 144 | func tlsWrap(l net.Listener, cfile, kfile string) net.Listener { 145 | if cfile == "" || kfile == "" { 146 | panic("need both cert file and key file") 147 | } 148 | 149 | cert, err := tls.LoadX509KeyPair(cfile, kfile) 150 | if err != nil { 151 | panic(err) 152 | } 153 | 154 | tc := new(tls.Config) 155 | tc.Certificates = append(tc.Certificates, cert) 156 | return tls.NewListener(l, tc) 157 | } 158 | -------------------------------------------------------------------------------- /web/main.js: -------------------------------------------------------------------------------- 1 | var deadline = 0, retry_interval = 0; 2 | var ti; 3 | 4 | function insert(parent, child) { 5 | var existing = parent.children(); 6 | var before = null; 7 | existing.each(function () { 8 | var jq = $(this); 9 | if (jq.attr('name') < child.attr('name')) { 10 | before = jq; 11 | } 12 | }); 13 | if (before === null) { 14 | parent.prepend(child); 15 | } else { 16 | before.after(child); 17 | } 18 | } 19 | 20 | function apply(ev) { 21 | var parts = ev.Path.split("/") 22 | if (parts.length < 2) { 23 | return 24 | } 25 | parts = parts.slice(1); // omit leading empty string 26 | var dir_parts = parts.slice(0, parts.length - 1); 27 | var dir = $('#root'); 28 | for (var i = 0; i < dir_parts.length; i++) { 29 | var part = dir_parts[i]; 30 | var next = dir.find('> dl > div[name="'+part+'"] > dd'); 31 | if (next.length < 1) { 32 | var div = $('
').attr('name', part); 33 | var dd = $('
'); 34 | div.append($('
').text(part+'/')).append(dd); 35 | insert(dir.children('dl'), div); 36 | dd.append('
').append(''); 37 | next = dd; 38 | } 39 | dir = next; 40 | } 41 | 42 | var basename = parts[parts.length - 1]; 43 | var entry = dir.find('tr[name="'+basename+'"]'); 44 | if (entry.length < 1) { 45 | var tr = $('').attr('name', basename); 46 | insert(dir.children('table').children('tbody'), tr); 47 | tr.append($('
').text(basename)). 48 | append(''). 49 | append(''). 50 | append(''); 51 | entry = tr; 52 | } 53 | entry.children('td.rev').text('('+ev.Rev+')'); 54 | entry.children('td.body').text(ev.Body); 55 | entry.addClass('new'); 56 | 57 | // Kick off the transition in a bit. 58 | setTimeout(function() { entry.removeClass('new') }, 550); 59 | } 60 | 61 | function time_interval(s) { 62 | if (s < 120) return Math.ceil(s) + 's'; 63 | if (s < 7200) return Math.round(s/60) + 'm'; 64 | return Math.round(s/3600) + 'h'; 65 | } 66 | 67 | function countdown() { 68 | var body = $('body'); 69 | var eta = (deadline - new Date().getTime())/1000; 70 | if (eta < 0) { 71 | body.removeClass('waiting'); 72 | open(); 73 | } else { 74 | $('#retrymsg').text("retrying in " + time_interval(eta)); 75 | body.addClass('waiting'); 76 | ti = setTimeout(countdown, Math.max(100, eta*9)); 77 | } 78 | } 79 | 80 | function retry() { 81 | deadline = ((new Date()).getTime()) + retry_interval * 1000; 82 | retry_interval += (retry_interval + 5) * (Math.random() + .5); 83 | countdown(); 84 | } 85 | 86 | function open() { 87 | var body = $('body'); 88 | var status = $('#status'); 89 | status.text("connecting"); 90 | var ws = new WebSocket("ws://"+location.host+"/$events"+path); 91 | ws.onmessage = function (ev) { 92 | var jev = JSON.parse(ev.data); 93 | apply(jev); 94 | }; 95 | ws.onopen = function(ev) { 96 | if (retry_interval > 0) { 97 | body.addClass('wereback'); 98 | setTimeout(function () { body.removeClass('wereback') }, 8000); 99 | } 100 | retry_interval = 0; 101 | status.text('open') 102 | body.addClass('open').removeClass('loading closed error'); 103 | $('#root > dl > *, #root > table > tbody > *').remove(); 104 | }; 105 | ws.onclose = function(ev) { 106 | status.text('closed') 107 | body.addClass('closed').removeClass('loading open error wereback'); 108 | retry(); 109 | }; 110 | ws.onerror = function(ev) { 111 | status.text('error ' + ev) 112 | body.addClass('error').removeClass('loading open closed wereback'); 113 | retry(); 114 | }; 115 | } 116 | 117 | function dr() { 118 | $('#trynow').click(function() { 119 | clearTimeout(ti); 120 | deadline = 0; 121 | countdown(); 122 | }); 123 | 124 | if ("WebSocket" in window) { 125 | open(); 126 | } else { 127 | $('#status').text("your browser does not provide websockets"); 128 | $('body').addClass('error nows').removeClass('loading open closed wereback'); 129 | } 130 | } 131 | 132 | function jerr() { 133 | const m = 'could not load jquery (is your network link down?)'; 134 | document.getElementById('status').innerText = m; 135 | document.getElementsByTagName('body')[0].className = 'error'; 136 | } 137 | -------------------------------------------------------------------------------- /boot.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base32" 6 | "github.com/ha/doozer" 7 | "time" 8 | ) 9 | 10 | const attachTimeout = 1e9 11 | 12 | func boot(name, id, laddr, buri string) *doozer.Conn { 13 | b, err := doozer.DialUri(buri, "") 14 | if err != nil { 15 | panic(err) 16 | } 17 | 18 | err = b.Access(rwsk) 19 | if err != nil { 20 | panic(err) 21 | } 22 | 23 | cl := lookupAndAttach(b, name) 24 | if cl == nil { 25 | return elect(name, id, laddr, b) 26 | } 27 | 28 | return cl 29 | } 30 | 31 | // Elect chooses a seed node, and returns a connection to a cal. 32 | // If this process is the seed, returns nil. 33 | func elect(name, id, laddr string, b *doozer.Conn) *doozer.Conn { 34 | // advertise our presence, since we might become a cal 35 | nspath := "/ctl/ns/" + name + "/" + id 36 | r, err := b.Set(nspath, 0, []byte(laddr)) 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | // fight to be the seed 42 | _, err = b.Set("/ctl/boot/"+name, 0, []byte(id)) 43 | if err, ok := err.(*doozer.Error); ok && err.Err == doozer.ErrOldRev { 44 | // we lost, lookup addresses again 45 | cl := lookupAndAttach(b, name) 46 | if cl == nil { 47 | panic("failed to attach after losing election") 48 | } 49 | 50 | // also delete our entry, since we're not officially a cal yet. 51 | // it gets set again in peer.Main when we become a cal. 52 | err := b.Del(nspath, r) 53 | if err != nil { 54 | panic(err) 55 | } 56 | 57 | return cl 58 | } else if err != nil { 59 | panic(err) 60 | } 61 | 62 | return nil // we are the seed node -- don't attach 63 | } 64 | 65 | func lookupAndAttach(b *doozer.Conn, name string) *doozer.Conn { 66 | as := lookup(b, name) 67 | if len(as) > 0 { 68 | cl := attach(name, as) 69 | if cl != nil { 70 | return cl 71 | } 72 | } 73 | return nil 74 | } 75 | 76 | func attach(name string, addrs []string) *doozer.Conn { 77 | ch := make(chan *doozer.Conn, 1) 78 | 79 | for _, a := range addrs { 80 | go func(a string) { 81 | if c, _ := isCal(name, a); c != nil { 82 | ch <- c 83 | } 84 | }(a) 85 | } 86 | 87 | go func() { 88 | <-time.After(attachTimeout) 89 | ch <- nil 90 | }() 91 | 92 | return <-ch 93 | } 94 | 95 | // IsCal checks if addr is a CAL in the cluster named name. 96 | // Returns a client if so, nil if not. 97 | func isCal(name, addr string) (*doozer.Conn, error) { 98 | c, err := doozer.Dial(addr) 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | err = c.Access(rwsk) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | v, _, _ := c.Get("/ctl/name", nil) 109 | if string(v) != name { 110 | return nil, nil 111 | } 112 | 113 | rev, err := c.Rev() 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | var cals []string 119 | names, err := c.Getdir("/ctl/cal", rev, 0, -1) 120 | if err != nil { 121 | return nil, err 122 | } 123 | for _, name := range names { 124 | cals = append(cals, name) 125 | } 126 | 127 | for _, cal := range cals { 128 | body, _, err := c.Get("/ctl/cal/"+cal, nil) 129 | if err != nil || len(body) == 0 { 130 | continue 131 | } 132 | 133 | id := string(body) 134 | 135 | v, _, err := c.Get("/ctl/node/"+id+"/addr", nil) 136 | if err != nil { 137 | return nil, err 138 | } 139 | if string(v) == addr { 140 | return c, nil 141 | } 142 | } 143 | 144 | return nil, nil 145 | } 146 | 147 | // Find possible addresses for cluster named name. 148 | func lookup(b *doozer.Conn, name string) (as []string) { 149 | rev, err := b.Rev() 150 | if err != nil { 151 | panic(err) 152 | } 153 | 154 | path := "/ctl/ns/" + name 155 | names, err := b.Getdir(path, rev, 0, -1) 156 | if err == doozer.ErrNoEnt { 157 | return nil 158 | } else if err, ok := err.(*doozer.Error); ok && err.Err == doozer.ErrNoEnt { 159 | return nil 160 | } else if err != nil { 161 | panic(err) 162 | } 163 | 164 | path += "/" 165 | for _, name := range names { 166 | body, _, err := b.Get(path+name, &rev) 167 | if err != nil { 168 | panic(err) 169 | } 170 | as = append(as, string(body)) 171 | } 172 | return as 173 | } 174 | 175 | func randId() string { 176 | const bits = 80 // enough for 10**8 ids with p(collision) < 10**-8 177 | rnd := make([]byte, bits/8) 178 | 179 | n, err := rand.Read(rnd) 180 | if err != nil { 181 | panic(err) 182 | } 183 | if n != len(rnd) { 184 | panic("io.ReadFull len mismatch") 185 | } 186 | 187 | enc := make([]byte, base32.StdEncoding.EncodedLen(len(rnd))) 188 | base32.StdEncoding.Encode(enc, rnd) 189 | return string(enc) 190 | } 191 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Doozer 2 | 3 | ![logo](doc/doozer.png) 4 | 5 | [![Build Status](https://secure.travis-ci.org/ha/doozerd.png)](http://travis-ci.org/ha/doozerd) 6 | 7 | ## What Is It? 8 | 9 | Doozer is a highly-available, completely consistent 10 | store for small amounts of extremely important data. 11 | When the data changes, it can notify connected clients 12 | immediately (no polling), making it ideal for 13 | infrequently-updated data for which clients want 14 | real-time updates. Doozer is good for name service, 15 | database master elections, and configuration data shared 16 | between several machines. See *When Should I Use It?*, 17 | below, for details. 18 | 19 | See the [mailing list][mail] to discuss doozer with 20 | other users and developers. 21 | 22 | ## Quick Start 23 | 24 | 1. Download [doozerd](https://github.com/ha/doozerd/downloads) 25 | 2. Unpack the archive and put `doozerd` in your `PATH` 26 | 3. Repeat for [doozer](https://github.com/ha/doozer/downloads) 27 | 4. Start a doozerd with a WebView listening on `:8080` 28 | 29 | $ doozerd -w ":8080" 30 | 31 | 5. Set a key and read it back 32 | 33 | $ echo "hello, world" | doozer add /message 34 | $ doozer get /message 35 | hello, world 36 | 37 | 6. Open and see your message 38 | 39 | ![doozer web view](doc/webview.png) 40 | 41 | ## How Does It Work? 42 | 43 | Doozer is a network service. A handful of machines 44 | (usually three, five, or seven) each run one doozer 45 | server process. These processes communicate with each 46 | other using a standard fully-consistent distributed 47 | consensus algorithm. Clients dial in to one or more of 48 | the doozer servers, issue commands, such as GET, SET, 49 | and WATCH, and receive responses. 50 | 51 | (insert network diagram here) 52 | 53 | Each doozerd process has a complete copy of the 54 | datastore and serves both read and write requests; there 55 | is no distinguished "master" or "leader". Doozer is 56 | designed to store data that fits entirely in memory; it 57 | never writes data to permanent files. A separate tool 58 | provides durable storage for backup and recovery. 59 | 60 | ## When Should I Use It? 61 | 62 | Here are some example scenarios: 63 | 64 | 1. *Name Service* 65 | 66 | You have a set of machines that serve incoming HTTP 67 | requests. Due to hardware failure, occasionally one 68 | of these machines will fail and you replace it with a 69 | new machine at a new network address. A change to DNS 70 | data would take time to reach all clients, because 71 | the TTL of the old DNS record would cause it to 72 | remain in client caches for some time. 73 | 74 | Instead of DNS, you could use Doozer. Clients can 75 | subscribe to the names they are interested in, and 76 | they will get notified when any of those names’ 77 | addresses change. 78 | 79 | 2. *Database Master Election* 80 | 81 | You are deploying a MySQL system. You want it to have 82 | high availability, so you add slaves on separate 83 | physical machines. When the master fails, you might 84 | promote one slave to become the new master. At any 85 | given time, clients need to know which machine is the 86 | master, and the slaves must coordinate with each 87 | other during failover. 88 | 89 | You can use doozer to store the address of the 90 | current master and all information necessary to 91 | coordinate failover. 92 | 93 | 3. *Configuration* 94 | 95 | You have processes on several different machines, and 96 | you want them all to use the same config file, which 97 | you must occasionally update. It is important that 98 | they all use the same configuration. 99 | 100 | Store the config file in doozer, and have the 101 | processes read their configuration directly from 102 | doozer. 103 | 104 | ## What can I do with it? 105 | 106 | We have a detailed description of the [data model](doc/data-model.md). 107 | 108 | For ways to manipulate or read the data, see the [protocol spec](doc/proto.md). 109 | 110 | Try out doozer's fault-tolerance with some [fire drills](doc/firedrill.md). 111 | 112 | ## Similar Projects 113 | 114 | Doozer is similar to the following pieces of software: 115 | 116 | * Apache Zookeeper 117 | * Google Chubby 118 | 119 | ## Hacking on Doozer 120 | 121 | * [hacking on doozer](doc/hacking.md) 122 | * [mailing list][mail] 123 | 124 | ## License and Authors 125 | 126 | Doozer is distributed under the terms of the MIT 127 | License. See [LICENSE](LICENSE) for details. 128 | 129 | Doozer was created by Blake Mizerany and Keith Rarick. 130 | Type `git shortlog -s` for a full list of contributors. 131 | 132 | [mail]: https://groups.google.com/group/doozer 133 | -------------------------------------------------------------------------------- /peer/bench_test.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | import ( 4 | "github.com/ha/doozer" 5 | "github.com/ha/doozerd/store" 6 | "testing" 7 | ) 8 | 9 | func Benchmark1DoozerClientSet(b *testing.B) { 10 | b.StopTimer() 11 | l := mustListen() 12 | defer l.Close() 13 | a := l.Addr().String() 14 | u := mustListenUDP(a) 15 | defer u.Close() 16 | 17 | go Main("a", "X", "", "", "", nil, u, l, nil, 1e9, 2e9, 3e9, 101) 18 | 19 | cl := dial(l.Addr().String()) 20 | 21 | b.StartTimer() 22 | for i := 0; i < b.N; i++ { 23 | cl.Set("/test", store.Clobber, nil) 24 | } 25 | } 26 | 27 | func Benchmark1DoozerConClientSet(b *testing.B) { 28 | b.StopTimer() 29 | l := mustListen() 30 | defer l.Close() 31 | a := l.Addr().String() 32 | u := mustListenUDP(a) 33 | defer u.Close() 34 | 35 | go Main("a", "X", "", "", "", nil, u, l, nil, 1e9, 2e9, 3e9, 101) 36 | 37 | cl := dial(l.Addr().String()) 38 | 39 | c := make(chan bool, b.N) 40 | b.StartTimer() 41 | for i := 0; i < b.N; i++ { 42 | go func() { 43 | cl.Set("/test", store.Clobber, nil) 44 | c <- true 45 | }() 46 | } 47 | for i := 0; i < b.N; i++ { 48 | <-c 49 | } 50 | } 51 | 52 | func Benchmark5DoozerClientSet(b *testing.B) { 53 | b.StopTimer() 54 | l := mustListen() 55 | defer l.Close() 56 | a := l.Addr().String() 57 | u := mustListenUDP(a) 58 | defer u.Close() 59 | 60 | l1 := mustListen() 61 | defer l1.Close() 62 | u1 := mustListenUDP(l1.Addr().String()) 63 | defer u1.Close() 64 | l2 := mustListen() 65 | defer l2.Close() 66 | u2 := mustListenUDP(l2.Addr().String()) 67 | defer u2.Close() 68 | l3 := mustListen() 69 | defer l3.Close() 70 | u3 := mustListenUDP(l3.Addr().String()) 71 | defer u3.Close() 72 | l4 := mustListen() 73 | defer l4.Close() 74 | u4 := mustListenUDP(l4.Addr().String()) 75 | defer u4.Close() 76 | 77 | go Main("a", "X", "", "", "", nil, u, l, nil, 1e9, 1e8, 3e9, 101) 78 | go Main("a", "Y", "", "", "", dial(a), u1, l1, nil, 1e9, 1e8, 3e9, 101) 79 | go Main("a", "Z", "", "", "", dial(a), u2, l2, nil, 1e9, 1e8, 3e9, 101) 80 | go Main("a", "V", "", "", "", dial(a), u3, l3, nil, 1e9, 1e8, 3e9, 101) 81 | go Main("a", "W", "", "", "", dial(a), u4, l4, nil, 1e9, 1e8, 3e9, 101) 82 | 83 | cl := dial(l.Addr().String()) 84 | cl.Set("/ctl/cal/1", store.Missing, nil) 85 | cl.Set("/ctl/cal/2", store.Missing, nil) 86 | cl.Set("/ctl/cal/3", store.Missing, nil) 87 | cl.Set("/ctl/cal/4", store.Missing, nil) 88 | 89 | // make sure all the peers have started up 90 | dial(l1.Addr().String()).Set("/foo", store.Clobber, nil) 91 | dial(l2.Addr().String()).Set("/foo", store.Clobber, nil) 92 | dial(l3.Addr().String()).Set("/foo", store.Clobber, nil) 93 | dial(l4.Addr().String()).Set("/foo", store.Clobber, nil) 94 | 95 | b.StartTimer() 96 | for i := 0; i < b.N; i++ { 97 | cl.Set("/test", store.Clobber, nil) 98 | } 99 | } 100 | 101 | func Benchmark5DoozerConClientSet(b *testing.B) { 102 | if b.N < 5 { 103 | return 104 | } 105 | const C = 20 106 | b.StopTimer() 107 | l := mustListen() 108 | defer l.Close() 109 | a := l.Addr().String() 110 | u := mustListenUDP(a) 111 | defer u.Close() 112 | 113 | l1 := mustListen() 114 | defer l1.Close() 115 | u1 := mustListenUDP(l1.Addr().String()) 116 | defer u1.Close() 117 | l2 := mustListen() 118 | defer l2.Close() 119 | u2 := mustListenUDP(l2.Addr().String()) 120 | defer u2.Close() 121 | l3 := mustListen() 122 | defer l3.Close() 123 | u3 := mustListenUDP(l3.Addr().String()) 124 | defer u3.Close() 125 | l4 := mustListen() 126 | defer l4.Close() 127 | u4 := mustListenUDP(l4.Addr().String()) 128 | defer u4.Close() 129 | 130 | go Main("a", "X", "", "", "", nil, u, l, nil, 1e9, 1e10, 3e12, 1e9) 131 | go Main("a", "Y", "", "", "", dial(a), u1, l1, nil, 1e9, 1e10, 3e12, 1e9) 132 | go Main("a", "Z", "", "", "", dial(a), u2, l2, nil, 1e9, 1e10, 3e12, 1e9) 133 | go Main("a", "V", "", "", "", dial(a), u3, l3, nil, 1e9, 1e10, 3e12, 1e9) 134 | go Main("a", "W", "", "", "", dial(a), u4, l4, nil, 1e9, 1e10, 3e12, 1e9) 135 | 136 | cl := dial(l.Addr().String()) 137 | cl.Set("/ctl/cal/1", store.Missing, nil) 138 | cl.Set("/ctl/cal/2", store.Missing, nil) 139 | cl.Set("/ctl/cal/3", store.Missing, nil) 140 | cl.Set("/ctl/cal/4", store.Missing, nil) 141 | 142 | waitFor(cl, "/ctl/node/X/writable") 143 | waitFor(cl, "/ctl/node/Y/writable") 144 | waitFor(cl, "/ctl/node/Z/writable") 145 | waitFor(cl, "/ctl/node/V/writable") 146 | waitFor(cl, "/ctl/node/W/writable") 147 | 148 | cls := []*doozer.Conn{ 149 | cl, 150 | dial(l1.Addr().String()), 151 | dial(l2.Addr().String()), 152 | dial(l3.Addr().String()), 153 | dial(l4.Addr().String()), 154 | } 155 | 156 | done := make(chan bool, C) 157 | f := func(i int, cl *doozer.Conn) { 158 | for ; i < b.N; i += C { 159 | _, err := cl.Set("/test", store.Clobber, nil) 160 | if e, ok := err.(*doozer.Error); ok && e.Err == doozer.ErrReadonly { 161 | } else if err != nil { 162 | panic(err) 163 | } 164 | } 165 | done <- true 166 | } 167 | b.StartTimer() 168 | for i := 0; i < C; i++ { 169 | go f(i, cls[i%len(cls)]) 170 | } 171 | for i := 0; i < C; i++ { 172 | <-done 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /doc/doozerd.1.ronn: -------------------------------------------------------------------------------- 1 | doozerd(1) -- A consistent, fault-tolerent, distributed data store. 2 | =================================================================== 3 | 4 | ## SYNOPSIS 5 | 6 | `doozerd` [options]
7 | `doozerd` [-c ] [-l ] [-a | -b ]
8 | 9 | ## DESCRIPTION 10 | 11 | *Doozerd* stores data in a cluster of doozerds consistently. A cluster has one 12 | or more members and zero or more slaves. When there is more than one doozerd in 13 | a cluster, it takes on the property of fault-tolerance. Each doozerd 14 | participating in consensus is called a member. One or more members make up a 15 | *cluster*. A doozerd attached to a cluster, and that is not participating in 16 | consensus, is a *slave*. Slaves watch the `/ctl/cal` directory for an empty 17 | file to appear. If this happens, slaves will attempt to set the contents of 18 | that file to their identity. If the file is written successfully, the slave 19 | will become a member of the cluster. Each doozerd process keeps a complete, 20 | consistent copy of the entire data store by changing their own stores in the 21 | same order as the others. (See [Data 22 | Model](https://github.com/ha/doozerd/blob/master/doc/data-model.md) for more 23 | information.) 24 | 25 | **Consensus** 26 | 27 | For members and slaves to change their stores in the same order, each write 28 | operation given to a member goes through consensus. This guarantees the order 29 | of the writes to all stores. Doozerd employs the 30 | [Paxos](http://en.wikipedia.org/wiki/Paxos_\(computer_science\)) algorithm for 31 | consensus. 32 | 33 | ## OPTIONS 34 | 35 | * `-a`=: 36 | Attach to a member in a cluster at address . 37 | 38 | * `-b`=: 39 | A uri containing the address of a DzNS cluster. If members are found under 40 | `/ctl/ns/`, doozerd will attempt to connect to each until it succeeds. 41 | See [doozer-uri(7)](https://github.com/ha/doozerd/blob/master/doc/uri.md)). 42 | 43 | * `-c`=: 44 | The name of a cluster. This is used for ensuring slaves connect to the 45 | correct cluster and for looking up addresses in DzNS. 46 | 47 | * `-fill`=: 48 | The number of seconds to wait before filling in unknown sequence numbers. 49 | 50 | * `-hist`=: 51 | The length of history/revisions to keep in the store. 52 | 53 | * `-l`=: 54 | The address to bind to. An is formatted as "host:port". It is important 55 | to note that doozerd uses the address given to `-l` as an identifier. It is not 56 | sufficient to use `0.0.0.0`. The must be the address others will connect 57 | to it with. 58 | 59 | 60 | * `-pulse`=: 61 | How often (in seconds) to set applied key. The key is listed in the store under 62 | `/ctl/node//applied`. The contents of the file represents the current 63 | revision of this process's copy of the store at the time of writing. 64 | 65 | * `-timeout`=: 66 | The timeout (in seconds) to kick inactive members. 67 | 68 | * `-tlscert`=: 69 | TLS public certificate. If both a `-tlscert` and `-tlskey` are given, all 70 | client traffic is encrypted with TLS. 71 | 72 | * `-tlskey`=: 73 | TLS private key. If both a `-tlscert` and `-tlskey` are given, all client 74 | traffic is encrypted with TLS. 75 | 76 | * `-v`: 77 | Print doozerd's version string and exit. 78 | 79 | * `-w`=: 80 | The listen address for the web view. The default is to use the addr from `-l`, 81 | and change the port to 8000. If you give `-w false`, doozerd will not listen 82 | for for web connections. 83 | 84 | ## ENVIRONMENT 85 | 86 | * `DOOZER_BOOT_URI`=: 87 | See CLUSTERING > With DzNS. 88 | 89 | ## CLUSTERING 90 | 91 | To start a cluster, you will need to start the initial member for others to 92 | attach to, or use a *Doozer Name Service* (DzNS). 93 | 94 | **Without DzNS** 95 | 96 | Start the initial member: 97 | 98 | $ doozerd -l 127.0.0.1:8046 99 | 100 | We can now slave our initial instance. 101 | 102 | $ doozerd -l 127.0.0.2:8064 -a 127.0.0.1:8046 103 | 104 | Open http://127.0.0.2:8000 to view its web view. Note it sees itself and the 105 | initial member. 106 | 107 | Add the third slave: 108 | 109 | $ doozerd -l 127.0.0.3:8064 -a 127.0.0.1:8046 110 | 111 | NOTE: Once the initial member is booted. Slaves can connect at anytime, 112 | meaning you can launch them in parallel. 113 | 114 | **Adding member slots** 115 | 116 | We need to use a Doozer client to add member slots. Here we will use the 117 | `doozer` command: 118 | 119 | $ export DOOZER_URI="doozer?:ca=127.0.0.1:8046" 120 | $ printf '' | doozer set /ctl/cal/1 0 121 | $ printf '' | doozer set /ctl/cal/2 0 122 | 123 | Open any of the web views and see that each id is the body of one of the three 124 | files under `/ctl/cal`. 125 | 126 | **With DzNS** 127 | 128 | A DzNS cluster is a doozer cluster used by other doozerd processes to discover 129 | members of the cluster they want to join and decide who the initial member will 130 | be when creating new clusters. 131 | 132 | A DzNS is created the same way you create any other doozer cluster. 133 | 134 | To boot a cluster using a DzNS, start a doozerd with the `-b` flag or the 135 | `DOOZER_BOOT_URI` environment variable set. If your cluster name is not the 136 | default name for `-c`, you will also need to set it. 137 | 138 | The newly started doozerd will first connect to a member of the DzNS. Once 139 | connected, it will lookup the address of the doozerd listed under 140 | `/ctl/boot/` in DzNS. If `/ctl/boot/` does not exist, the doozerd 141 | will attempt to create it with its identity as the contents. If succesfull, it 142 | will become the initial member of the cluster and the others will slave it. 143 | 144 | 145 | $ export DOOZER_BOOT_URI=" 146 | doozer:? 147 | ca=127.0.0.1:8046& 148 | ca=127.0.0.2:8046& 149 | ca=127.0.0.3:8046 150 | " 151 | 152 | NOTE: All doozerds can be started in parellel when using a DzNS. 153 | 154 | $ doozerd -c example -l 127.0.0.10:8046 155 | $ doozerd -c example -l 127.0.0.20:8046 156 | $ doozerd -c example -l 127.0.0.30:8046 157 | $ doozerd -c example -l 127.0.0.40:8046 158 | 159 | 160 | Now we can create the member slots under `/ctl/cal` in the new doozerd 161 | processes. We will, again, use our DzNS to determine which is the member we 162 | need to write to. 163 | 164 | First, we need to set the cluster name we want to use in the `DOOZER_URI` 165 | environment variable, then we create the empty files. The `DOOZER_BOOT_URI` 166 | will remain unchanged. 167 | 168 | $ export DOOZER_URI="doozer:?cn=example" 169 | $ printf '' | doozer set /ctl/cal/1 0 170 | $ printf '' | doozer set /ctl/cal/2 0 171 | 172 | ## EXIT STATUS 173 | 174 | **doozerd** exits 0 on success, and >0 if an error occurs. 175 | 176 | ## AUTHORS 177 | Keith Rarick , Blake Mizerany 178 | 179 | ## SOURCE 180 | 181 | -------------------------------------------------------------------------------- /doc/proto.md: -------------------------------------------------------------------------------- 1 | # Client Protocol 2 | 3 | ## Overview 4 | 5 | Doozer is a highly-available, consistent lock service. 6 | It also lets you store small amounts of metadata as 7 | files in a directory tree. See [data model][data] for a complete 8 | description. 9 | 10 | The doozer protocol is used for messages between clients 11 | and servers. A client connects to doozerd by TCP and 12 | transmits *request* messages to a server, which 13 | subsequently returns *response* messages to the client. 14 | 15 | (Note: this protocol is partially based on [9P][], 16 | the Plan 9 file protocol. Parts of this document 17 | are paraphrased from the 9P man pages.) 18 | 19 | Each message consists of a sequence of bytes comprising 20 | two parts. First, a four-byte header field holds an 21 | unsigned integer, *n*, in big-endian order (most 22 | significant byte first). This is followed by *n* bytes 23 | of data; these *n* bytes represent structured data 24 | encoded in [Protocol Buffer][protobuf] format. 25 | 26 | Two Protocol Buffer structures, `Request` and 27 | `Response`, are used for *requests* and *responses*, 28 | respectively. See `src/pkg/server/msg.proto` for their 29 | definitions. 30 | 31 | Each request contains at least a tag, described below, 32 | and a verb, to identify what action is desired. 33 | The other fields may or may not be required; their 34 | meanings depend on the verb, as described below. 35 | 36 | The tag is chosen and used by the client to identify 37 | the message. The reply to the message 38 | will have the same tag. Clients must arrange that no 39 | two outstanding requests on the same connection have 40 | the same tag. 41 | 42 | Each response contains at least a tag. 43 | Other response fields may or may not be present, 44 | depending on the verb of the request. 45 | 46 | A client can send multiple requests without waiting for 47 | the corresponding responses, but all outstanding 48 | requests must specify different tags. The server may 49 | delay the response to a request and respond to later 50 | ones; this is sometimes necessary, for example when the 51 | client has issued a `WAIT` request and the response 52 | is sent after a file is modified in the future. 53 | 54 | ### Data Model 55 | 56 | For a thorough description of Doozer's data model, 57 | see [Data Model][data]. Briefly, doozer's store holds 58 | a tree structure of files identified by paths similar 59 | to paths in Unix, and performs only whole-file reads 60 | and writes, which are atomic. The store also records 61 | the *revision* of each write. 62 | This number can be given to a subsequent write 63 | operation to ensure that no 64 | intervening writes have happened. 65 | 66 | ## Glob Notation 67 | 68 | Some of the requests take a glob pattern that can match 69 | zero or more concrete path names. 70 | 71 | - `?` matches a single char in a single path component 72 | - `*` matches zero or more chars in a single path component 73 | - `**` matches zero or more chars in zero or more components 74 | - any other sequence matches itself 75 | 76 | ## Verbs 77 | 78 | Each verb shows the set of request fields it uses, 79 | followed by the set of response fields it provides. 80 | 81 | * `DEL` *path*, *rev* ⇒ ∅ 82 | 83 | Del deletes the file at *path* if *rev* is greater than 84 | or equal to the file's revision. 85 | 86 | * `GET` *path*, *rev* ⇒ *value*, *rev* 87 | 88 | Gets the contents (*value*) and revision (*rev*) 89 | of the file at *path* in the specified revision (*rev*). 90 | If *rev* is not provided, get uses the current revision. 91 | 92 | * `GETDIR` *path*, *rev*, *offset* ⇒ *path* 93 | 94 | Returns the *n*th entry in *path* (a directory) in 95 | the specified revision (*rev*), where *n* is 96 | *offset*. It is an error if *path* is not a 97 | directory. 98 | 99 | * `NOP` (deprecated) 100 | 101 | * `REV` ∅ ⇒ *rev* 102 | 103 | Returns the current revision. 104 | 105 | * `SET` *path*, *rev*, *value* ⇒ *rev* 106 | 107 | Sets the contents of the file at *path* to *value*, 108 | as long as *rev* is greater than or equal to the file's 109 | revision. 110 | Returns the file's new revision. 111 | 112 | * `WAIT` *path*, *rev* ⇒ *path*, *rev*, *value*, *flags* 113 | 114 | Responds with the first change made to any file 115 | matching *path*, a glob pattern, on or after *rev*. 116 | The response *path* is the file that was changed; 117 | the response *rev* is the revision of the change. 118 | *Value* is the new contents of the file. 119 | 120 | *Flags* is a bitwise combination of values with the 121 | following meanings (values 1 and 2 are not used): 122 | 123 | * *set* = 4 124 | 125 | The file was changed or created. 126 | 127 | * *del* = 8 128 | 129 | The file was deleted. 130 | 131 | * `WALK` *path*, *rev*, *offset* ⇒ *path*, *rev*, *value* 132 | 133 | Returns the *n*th file with a name matching *path* 134 | (a glob pattern) in the specified revision (*rev*), 135 | where *n* is *offset*. 136 | 137 | ## Errors 138 | 139 | The server might send a response with the `err_code` field 140 | set. In that case, `err_detail` might also be set, and 141 | the other optional response fields will be unset. 142 | 143 | If `err_detail` is set, it provides extra information as 144 | defined below. 145 | 146 | Error codes are defined with the following meanings: 147 | 148 | * `TAG_IN_USE` 149 | 150 | The server has noticed that the client sent two 151 | or more requests with the same tag. This is a 152 | serious error and always indicates a bug in the 153 | client. 154 | 155 | The server is not guaranteed to send this error. 156 | 157 | * `UNKNOWN_VERB` 158 | 159 | The verb used in the request is not in the list of 160 | verbs defined in the server. 161 | 162 | * `READONLY` 163 | 164 | The Doozer connection is read-only. Clients can attempt a 165 | connection to a new server if writes a needed. 166 | 167 | * `TOO_LATE` 168 | 169 | The rev given in the request is invalid; 170 | it has been garbage collected. 171 | 172 | The current default of history kept is 360,000 revs. 173 | 174 | * `REV_MISMATCH` 175 | 176 | A write operation has failed because the revision given 177 | was less than the revision of the file being set. 178 | 179 | * `BAD_PATH` 180 | 181 | The given path contains invalid characters. 182 | 183 | * `MISSING_ARG` 184 | 185 | The request's verb requires certain fields to be set 186 | and at least one of those fields was not set. 187 | 188 | * `RANGE` 189 | 190 | The `offset` provided is out of range. 191 | 192 | * `NOTDIR` 193 | 194 | The request operates only on a directory, but the 195 | given path is not a directory (either because it is a 196 | file or it is missing). 197 | 198 | * `ISDIR` 199 | 200 | The request operates only on a regular file, but the 201 | given path is a directory. 202 | 203 | * `NOENT` 204 | 205 | Some component of `path` doesn't exist. 206 | 207 | * `OTHER` 208 | 209 | Some other error has occurred. The `err_detail` 210 | string provides a description. 211 | 212 | Error value 0 is reserved. 213 | 214 | [protobuf]: http://code.google.com/p/protobuf/ 215 | [9P]: http://plan9.bell-labs.com/magic/man2html/5/intro 216 | [data]: data-model.md 217 | -------------------------------------------------------------------------------- /server/txn.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "code.google.com/p/goprotobuf/proto" 5 | "github.com/ha/doozerd/consensus" 6 | "github.com/ha/doozerd/store" 7 | "io" 8 | "log" 9 | "sort" 10 | "syscall" 11 | ) 12 | 13 | type txn struct { 14 | c *conn 15 | req request 16 | resp response 17 | } 18 | 19 | var ops = map[int32]func(*txn){ 20 | int32(request_DEL): (*txn).del, 21 | int32(request_GET): (*txn).get, 22 | int32(request_GETDIR): (*txn).getdir, 23 | int32(request_NOP): (*txn).nop, 24 | int32(request_REV): (*txn).rev, 25 | int32(request_SET): (*txn).set, 26 | int32(request_STAT): (*txn).stat, 27 | int32(request_SELF): (*txn).self, 28 | int32(request_WAIT): (*txn).wait, 29 | int32(request_WALK): (*txn).walk, 30 | int32(request_ACCESS): (*txn).access, 31 | } 32 | 33 | // response flags 34 | const ( 35 | _ = 1 << iota 36 | _ 37 | set 38 | del 39 | ) 40 | 41 | func (t *txn) run() { 42 | verb := int32(t.req.GetVerb()) 43 | if f, ok := ops[verb]; ok { 44 | f(t) 45 | } else { 46 | t.respondErrCode(response_UNKNOWN_VERB) 47 | } 48 | } 49 | 50 | func (t *txn) get() { 51 | if !t.c.raccess { 52 | t.respondOsError(syscall.EACCES) 53 | return 54 | } 55 | 56 | if t.req.Path == nil { 57 | t.respondErrCode(response_MISSING_ARG) 58 | return 59 | } 60 | 61 | go func() { 62 | g, err := t.getter() 63 | if err != nil { 64 | t.respondOsError(err) 65 | return 66 | } 67 | 68 | v, rev := g.Get(*t.req.Path) 69 | if rev == store.Dir { 70 | t.respondErrCode(response_ISDIR) 71 | return 72 | } 73 | 74 | t.resp.Rev = &rev 75 | if len(v) == 1 { // not missing 76 | t.resp.Value = []byte(v[0]) 77 | } 78 | t.respond() 79 | }() 80 | } 81 | 82 | func (t *txn) set() { 83 | if !t.c.waccess { 84 | t.respondOsError(syscall.EACCES) 85 | return 86 | } 87 | 88 | if !t.c.canWrite { 89 | t.respondErrCode(response_READONLY) 90 | return 91 | } 92 | 93 | if t.req.Path == nil || t.req.Rev == nil { 94 | t.respondErrCode(response_MISSING_ARG) 95 | return 96 | } 97 | 98 | go func() { 99 | ev := consensus.Set(t.c.p, *t.req.Path, t.req.Value, *t.req.Rev) 100 | if ev.Err != nil { 101 | t.respondOsError(ev.Err) 102 | return 103 | } 104 | t.resp.Rev = &ev.Seqn 105 | t.respond() 106 | }() 107 | } 108 | 109 | func (t *txn) del() { 110 | if !t.c.waccess { 111 | t.respondOsError(syscall.EACCES) 112 | return 113 | } 114 | 115 | if !t.c.canWrite { 116 | t.respondErrCode(response_READONLY) 117 | return 118 | } 119 | 120 | if t.req.Path == nil || t.req.Rev == nil { 121 | t.respondErrCode(response_MISSING_ARG) 122 | return 123 | } 124 | 125 | go func() { 126 | ev := consensus.Del(t.c.p, *t.req.Path, *t.req.Rev) 127 | if ev.Err != nil { 128 | t.respondOsError(ev.Err) 129 | return 130 | } 131 | t.respond() 132 | }() 133 | } 134 | 135 | func (t *txn) nop() { 136 | if !t.c.waccess { 137 | t.respondOsError(syscall.EACCES) 138 | return 139 | } 140 | 141 | if !t.c.canWrite { 142 | t.respondErrCode(response_READONLY) 143 | return 144 | } 145 | 146 | go func() { 147 | t.c.p.Propose([]byte(store.Nop)) 148 | t.respond() 149 | }() 150 | } 151 | 152 | func (t *txn) rev() { 153 | rev := <-t.c.st.Seqns 154 | t.resp.Rev = &rev 155 | t.respond() 156 | } 157 | 158 | func (t *txn) self() { 159 | t.resp.Value = []byte(t.c.self) 160 | t.respond() 161 | } 162 | 163 | func (t *txn) stat() { 164 | if !t.c.raccess { 165 | t.respondOsError(syscall.EACCES) 166 | return 167 | } 168 | 169 | go func() { 170 | g, err := t.getter() 171 | if err != nil { 172 | t.respondOsError(err) 173 | return 174 | } 175 | 176 | len, rev := g.Stat(t.req.GetPath()) 177 | t.resp.Len = &len 178 | t.resp.Rev = &rev 179 | t.respond() 180 | }() 181 | } 182 | 183 | func (t *txn) getdir() { 184 | if !t.c.raccess { 185 | t.respondOsError(syscall.EACCES) 186 | return 187 | } 188 | 189 | if t.req.Path == nil || t.req.Offset == nil { 190 | t.respondErrCode(response_MISSING_ARG) 191 | return 192 | } 193 | 194 | go func() { 195 | g, err := t.getter() 196 | if err != nil { 197 | t.respondOsError(err) 198 | return 199 | } 200 | 201 | ents, rev := g.Get(*t.req.Path) 202 | if rev == store.Missing { 203 | t.respondErrCode(response_NOENT) 204 | return 205 | } 206 | if rev != store.Dir { 207 | t.respondErrCode(response_NOTDIR) 208 | return 209 | } 210 | 211 | sort.Strings(ents) 212 | offset := int(*t.req.Offset) 213 | if offset < 0 || offset >= len(ents) { 214 | t.respondErrCode(response_RANGE) 215 | return 216 | } 217 | 218 | t.resp.Path = &ents[offset] 219 | t.respond() 220 | }() 221 | } 222 | 223 | func (t *txn) wait() { 224 | if !t.c.raccess { 225 | t.respondOsError(syscall.EACCES) 226 | return 227 | } 228 | 229 | if t.req.Path == nil || t.req.Rev == nil { 230 | t.respondErrCode(response_MISSING_ARG) 231 | return 232 | } 233 | 234 | glob, err := store.CompileGlob(*t.req.Path) 235 | if err != nil { 236 | t.respondOsError(err) 237 | return 238 | } 239 | 240 | ch, err := t.c.st.Wait(glob, *t.req.Rev) 241 | if err != nil { 242 | t.respondOsError(err) 243 | return 244 | } 245 | 246 | go func() { 247 | ev := <-ch 248 | t.resp.Path = &ev.Path 249 | t.resp.Value = []byte(ev.Body) 250 | t.resp.Rev = &ev.Seqn 251 | switch { 252 | case ev.IsSet(): 253 | t.resp.Flags = proto.Int32(set) 254 | case ev.IsDel(): 255 | t.resp.Flags = proto.Int32(del) 256 | default: 257 | t.resp.Flags = proto.Int32(0) 258 | } 259 | t.respond() 260 | }() 261 | } 262 | 263 | func (t *txn) walk() { 264 | if !t.c.raccess { 265 | t.respondOsError(syscall.EACCES) 266 | return 267 | } 268 | 269 | if t.req.Path == nil || t.req.Offset == nil { 270 | t.respondErrCode(response_MISSING_ARG) 271 | return 272 | } 273 | 274 | glob, err := store.CompileGlob(*t.req.Path) 275 | if err != nil { 276 | t.respondOsError(err) 277 | return 278 | } 279 | 280 | offset := *t.req.Offset 281 | if offset < 0 { 282 | t.respondErrCode(response_RANGE) 283 | return 284 | } 285 | 286 | go func() { 287 | g, err := t.getter() 288 | if err != nil { 289 | t.respondOsError(err) 290 | return 291 | } 292 | 293 | f := func(path, body string, rev int64) (stop bool) { 294 | if offset == 0 { 295 | t.resp.Path = &path 296 | t.resp.Value = []byte(body) 297 | t.resp.Rev = &rev 298 | t.resp.Flags = proto.Int32(set) 299 | t.respond() 300 | return true 301 | } 302 | offset-- 303 | return false 304 | } 305 | if !store.Walk(g, glob, f) { 306 | t.respondErrCode(response_RANGE) 307 | } 308 | }() 309 | } 310 | 311 | func (t *txn) access() { 312 | if t.c.grant(string(t.req.Value)) { 313 | t.respond() 314 | } else { 315 | t.respondOsError(syscall.EACCES) 316 | } 317 | } 318 | 319 | func (t *txn) respondOsError(err error) { 320 | switch err { 321 | case store.ErrBadPath: 322 | t.respondErrCode(response_BAD_PATH) 323 | case store.ErrRevMismatch: 324 | t.respondErrCode(response_REV_MISMATCH) 325 | case store.ErrTooLate: 326 | t.respondErrCode(response_TOO_LATE) 327 | case syscall.EISDIR: 328 | t.respondErrCode(response_ISDIR) 329 | case syscall.ENOTDIR: 330 | t.respondErrCode(response_NOTDIR) 331 | default: 332 | t.resp.ErrDetail = proto.String(err.Error()) 333 | t.respondErrCode(response_OTHER) 334 | } 335 | } 336 | 337 | func (t *txn) respondErrCode(e response_Err) { 338 | t.resp.ErrCode = &e 339 | t.respond() 340 | } 341 | 342 | func (t *txn) respond() { 343 | t.resp.Tag = t.req.Tag 344 | err := t.c.write(&t.resp) 345 | if err != nil && err != io.EOF { 346 | log.Println(err) 347 | } 348 | } 349 | 350 | func (t *txn) getter() (store.Getter, error) { 351 | if t.req.Rev == nil { 352 | _, g := t.c.st.Snap() 353 | return g, nil 354 | } 355 | 356 | ch, err := t.c.st.Wait(store.Any, *t.req.Rev) 357 | if err != nil { 358 | return nil, err 359 | } 360 | return <-ch, nil 361 | } 362 | -------------------------------------------------------------------------------- /consensus/run_test.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | import ( 4 | "code.google.com/p/goprotobuf/proto" 5 | "github.com/bmizerany/assert" 6 | "github.com/ha/doozerd/store" 7 | "net" 8 | "testing" 9 | ) 10 | 11 | const ( 12 | node = "/ctl/node" 13 | cal = "/ctl/cal" 14 | ) 15 | 16 | func MustResolveUDPAddr(n, addr string) *net.UDPAddr { 17 | udp, err := net.ResolveUDPAddr(n, addr) 18 | if err != nil { 19 | panic(err) 20 | } 21 | 22 | return udp 23 | } 24 | 25 | type msgSlot struct { 26 | *msg 27 | } 28 | 29 | func (ms msgSlot) Put(m *msg) { 30 | *ms.msg = *m 31 | } 32 | 33 | func TestQuorum(t *testing.T) { 34 | assert.Equal(t, 1, (&run{cals: []string{"a"}}).quorum()) 35 | assert.Equal(t, 2, (&run{cals: []string{"a", "b"}}).quorum()) 36 | assert.Equal(t, 2, (&run{cals: []string{"a", "b", "c"}}).quorum()) 37 | assert.Equal(t, 3, (&run{cals: []string{"a", "b", "c", "d"}}).quorum()) 38 | assert.Equal(t, 3, (&run{cals: []string{"a", "b", "c", "d", "e"}}).quorum()) 39 | assert.Equal(t, 4, (&run{cals: []string{"a", "b", "c", "d", "e", "f"}}).quorum()) 40 | assert.Equal(t, 4, (&run{cals: []string{"a", "b", "c", "d", "e", "f", "g"}}).quorum()) 41 | assert.Equal(t, 5, (&run{cals: []string{"a", "b", "c", "d", "e", "f", "g", "h"}}).quorum()) 42 | } 43 | 44 | func TestRunVoteDelivered(t *testing.T) { 45 | r := run{} 46 | r.out = make(chan Packet, 100) 47 | r.ops = make(chan store.Op, 100) 48 | r.l.init(1, 1) 49 | 50 | p := packet{ 51 | msg: msg{ 52 | Seqn: proto.Int64(1), 53 | Cmd: vote, 54 | Vrnd: proto.Int64(1), 55 | Value: []byte("foo"), 56 | }, 57 | Addr: MustResolveUDPAddr("udp", "1.2.3.4:5"), 58 | } 59 | 60 | r.update(&p, 0, new(triggers)) 61 | 62 | assert.Equal(t, true, r.l.done) 63 | assert.Equal(t, "foo", r.l.v) 64 | } 65 | 66 | func TestRunInviteDelivered(t *testing.T) { 67 | var r run 68 | r.out = make(chan Packet, 100) 69 | r.ops = make(chan store.Op, 100) 70 | 71 | r.update(&packet{msg: *newInviteSeqn1(1)}, 0, new(triggers)) 72 | 73 | assert.Equal(t, int64(1), r.a.rnd) 74 | } 75 | 76 | func TestRunProposeDelivered(t *testing.T) { 77 | var r run 78 | r.out = make(chan Packet, 100) 79 | r.ops = make(chan store.Op, 100) 80 | 81 | r.update(&packet{msg: msg{Cmd: propose}}, -1, new(triggers)) 82 | assert.Equal(t, true, r.c.begun) 83 | } 84 | 85 | func TestRunSendsCoordPacket(t *testing.T) { 86 | c := make(chan Packet, 100) 87 | x := MustResolveUDPAddr("udp", "1.2.3.4:5") 88 | y := MustResolveUDPAddr("udp", "2.3.4.5:6") 89 | var r run 90 | r.c.crnd = 1 91 | r.out = c 92 | r.addr = []*net.UDPAddr{x, y} 93 | 94 | var got msg 95 | exp := msg{ 96 | Seqn: proto.Int64(0), 97 | Cmd: invite, 98 | Crnd: proto.Int64(1), 99 | } 100 | 101 | r.update(&packet{msg: *newPropose("foo")}, -1, new(triggers)) 102 | <-c 103 | err := proto.Unmarshal((<-c).Data, &got) 104 | assert.Equal(t, nil, err) 105 | assert.Equal(t, exp, got) 106 | assert.Equal(t, 0, len(c)) 107 | } 108 | 109 | func TestRunSchedulesTick(t *testing.T) { 110 | var r run 111 | r.seqn = 1 112 | r.bound = 10 113 | r.out = make(chan Packet, 100) 114 | ticks := new(triggers) 115 | 116 | r.update(&packet{msg: *newPropose("foo")}, -1, ticks) 117 | 118 | assert.Equal(t, 1, ticks.Len()) 119 | } 120 | 121 | func TestRunSendsAcceptorPacket(t *testing.T) { 122 | c := make(chan Packet, 100) 123 | x := MustResolveUDPAddr("udp", "1.2.3.4:5") 124 | y := MustResolveUDPAddr("udp", "2.3.4.5:6") 125 | var r run 126 | r.out = c 127 | r.addr = []*net.UDPAddr{x, y} 128 | 129 | var got msg 130 | exp := msg{ 131 | Seqn: proto.Int64(0), 132 | Cmd: rsvp, 133 | Crnd: proto.Int64(1), 134 | Vrnd: proto.Int64(0), 135 | Value: []byte{}, 136 | } 137 | 138 | r.update(&packet{msg: *newInviteSeqn1(1)}, 0, new(triggers)) 139 | <-c 140 | err := proto.Unmarshal((<-c).Data, &got) 141 | assert.Equal(t, nil, err) 142 | assert.Equal(t, exp, got) 143 | assert.Equal(t, 0, len(c)) 144 | } 145 | 146 | func TestRunSendsLearnerPacket(t *testing.T) { 147 | c := make(chan Packet, 100) 148 | var r run 149 | r.out = c 150 | r.ops = make(chan store.Op, 100) 151 | r.addr = []*net.UDPAddr{nil, nil} 152 | r.l.init(1, 1) 153 | 154 | var got msg 155 | exp := msg{ 156 | Seqn: proto.Int64(0), 157 | Cmd: learn, 158 | Value: []byte("foo"), 159 | } 160 | 161 | r.update(&packet{msg: *newVote(1, "foo")}, 0, new(triggers)) 162 | assert.Equal(t, 2, len(c)) 163 | err := proto.Unmarshal((<-c).Data, &got) 164 | assert.Equal(t, nil, err) 165 | assert.Equal(t, exp, got) 166 | } 167 | 168 | func TestRunAppliesOp(t *testing.T) { 169 | c := make(chan store.Op, 100) 170 | var r run 171 | r.seqn = 1 172 | r.out = make(chan Packet, 100) 173 | r.ops = c 174 | r.l.init(1, 1) 175 | 176 | r.update(&packet{msg: *newVote(1, "foo")}, 0, new(triggers)) 177 | assert.Equal(t, store.Op{1, "foo"}, <-c) 178 | } 179 | 180 | func TestRunBroadcastThree(t *testing.T) { 181 | c := make(chan Packet, 100) 182 | var r run 183 | r.seqn = 1 184 | r.out = c 185 | r.addr = []*net.UDPAddr{ 186 | MustResolveUDPAddr("udp", "1.2.3.4:5"), 187 | MustResolveUDPAddr("udp", "2.3.4.5:6"), 188 | MustResolveUDPAddr("udp", "3.4.5.6:7"), 189 | } 190 | 191 | r.broadcast(newInvite(1)) 192 | c <- Packet{} 193 | 194 | exp := msg{ 195 | Seqn: proto.Int64(1), 196 | Cmd: invite, 197 | Crnd: proto.Int64(1), 198 | } 199 | 200 | addr := make([]*net.UDPAddr, len(r.addr)) 201 | for i := 0; i < len(r.addr); i++ { 202 | p := <-c 203 | addr[i] = p.Addr 204 | var got msg 205 | err := proto.Unmarshal(p.Data, &got) 206 | assert.Equal(t, nil, err) 207 | assert.Equal(t, exp, got) 208 | } 209 | 210 | assert.Equal(t, Packet{}, <-c) 211 | assert.Equal(t, r.addr, addr) 212 | } 213 | 214 | func TestRunBroadcastFive(t *testing.T) { 215 | c := make(chan Packet, 100) 216 | var r run 217 | r.seqn = 1 218 | r.out = c 219 | r.addr = []*net.UDPAddr{ 220 | MustResolveUDPAddr("udp", "1.2.3.4:5"), 221 | MustResolveUDPAddr("udp", "2.3.4.5:6"), 222 | MustResolveUDPAddr("udp", "3.4.5.6:7"), 223 | MustResolveUDPAddr("udp", "4.5.6.7:8"), 224 | MustResolveUDPAddr("udp", "5.6.7.8:9"), 225 | } 226 | 227 | r.broadcast(newInvite(1)) 228 | c <- Packet{} 229 | 230 | exp := msg{ 231 | Seqn: proto.Int64(1), 232 | Cmd: invite, 233 | Crnd: proto.Int64(1), 234 | } 235 | 236 | addr := make([]*net.UDPAddr, len(r.addr)) 237 | for i := 0; i < len(r.addr); i++ { 238 | p := <-c 239 | addr[i] = p.Addr 240 | var got msg 241 | err := proto.Unmarshal(p.Data, &got) 242 | assert.Equal(t, nil, err) 243 | assert.Equal(t, exp, got) 244 | } 245 | 246 | assert.Equal(t, Packet{}, <-c) 247 | assert.Equal(t, r.addr, addr) 248 | } 249 | 250 | func TestRunBroadcastNil(t *testing.T) { 251 | c := make(chan Packet, 100) 252 | var r run 253 | r.out = c 254 | r.addr = []*net.UDPAddr{ 255 | MustResolveUDPAddr("udp", "1.2.3.4:5"), 256 | MustResolveUDPAddr("udp", "2.3.4.5:6"), 257 | MustResolveUDPAddr("udp", "3.4.5.6:7"), 258 | } 259 | 260 | r.broadcast(nil) 261 | c <- Packet{} 262 | assert.Equal(t, Packet{}, <-c) 263 | } 264 | 265 | func TestRunIsLeader(t *testing.T) { 266 | r := &run{ 267 | cals: []string{"a", "b", "c"}, // len(cals) == 3 268 | seqn: 3, // 3 % 3 == 0 269 | } 270 | 271 | assert.T(t, r.isLeader("a")) // index == 0 272 | assert.T(t, !r.isLeader("b")) // index == 1 273 | assert.T(t, !r.isLeader("c")) // index == 2 274 | assert.T(t, !r.isLeader("x")) // index DNE 275 | } 276 | 277 | func TestRunReturnTrueIfLearned(t *testing.T) { 278 | r := run{} 279 | r.out = make(chan Packet, 100) 280 | r.ops = make(chan store.Op, 100) 281 | 282 | p := packet{msg: msg{ 283 | Seqn: proto.Int64(1), 284 | Cmd: learn, 285 | Value: []byte("foo"), 286 | }} 287 | 288 | r.update(&p, 0, new(triggers)) 289 | assert.T(t, r.l.done) 290 | } 291 | 292 | func TestRunReturnFalseIfNotLearned(t *testing.T) { 293 | r := run{} 294 | r.out = make(chan Packet, 100) 295 | r.ops = make(chan store.Op, 100) 296 | 297 | p := packet{msg: msg{ 298 | Seqn: proto.Int64(1), 299 | Cmd: invite, 300 | Value: []byte("foo"), 301 | }} 302 | 303 | r.update(&p, 0, new(triggers)) 304 | assert.T(t, !r.l.done) 305 | } 306 | 307 | func TestRunIndexOfNilAddr(t *testing.T) { 308 | r := run{addr: []*net.UDPAddr{new(net.UDPAddr)}} 309 | assert.Equal(t, -1, r.indexOfAddr(nil)) 310 | } 311 | -------------------------------------------------------------------------------- /peer/peer.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | import ( 4 | "github.com/ha/doozer" 5 | "github.com/ha/doozerd/consensus" 6 | "github.com/ha/doozerd/gc" 7 | "github.com/ha/doozerd/member" 8 | "github.com/ha/doozerd/server" 9 | "github.com/ha/doozerd/store" 10 | "github.com/ha/doozerd/web" 11 | "io" 12 | "log" 13 | "net" 14 | "os" 15 | "strings" 16 | "time" 17 | ) 18 | 19 | const ( 20 | alpha = 50 21 | maxUDPLen = 3000 22 | ) 23 | 24 | const calDir = "/ctl/cal" 25 | 26 | var calGlob = store.MustCompileGlob(calDir + "/*") 27 | 28 | type proposer struct { 29 | seqns chan int64 30 | props chan *consensus.Prop 31 | st *store.Store 32 | } 33 | 34 | func (p *proposer) Propose(v []byte) (e store.Event) { 35 | for e.Mut != string(v) { 36 | n := <-p.seqns 37 | w, err := p.st.Wait(store.Any, n) 38 | if err != nil { 39 | panic(err) // can't happen 40 | } 41 | p.props <- &consensus.Prop{n, v} 42 | e = <-w 43 | } 44 | return 45 | } 46 | 47 | func Main(clusterName, self, buri, rwsk, rosk string, cl *doozer.Conn, udpConn *net.UDPConn, listener, webListener net.Listener, pulseInterval, fillDelay, kickTimeout int64, hi int64) { 48 | listenAddr := listener.Addr().String() 49 | 50 | canWrite := make(chan bool, 1) 51 | in := make(chan consensus.Packet, 50) 52 | out := make(chan consensus.Packet, 50) 53 | 54 | st := store.New() 55 | pr := &proposer{ 56 | seqns: make(chan int64, alpha), 57 | props: make(chan *consensus.Prop), 58 | st: st, 59 | } 60 | 61 | calSrv := func(start int64) { 62 | go gc.Pulse(self, st.Seqns, pr, pulseInterval) 63 | go gc.Clean(st, hi, time.Tick(1e9)) 64 | var m consensus.Manager 65 | m.Self = self 66 | m.DefRev = start 67 | m.Alpha = alpha 68 | m.In = in 69 | m.Out = out 70 | m.Ops = st.Ops 71 | m.PSeqn = pr.seqns 72 | m.Props = pr.props 73 | m.TFill = fillDelay 74 | m.Store = st 75 | m.Ticker = time.Tick(10e6) 76 | go m.Run() 77 | } 78 | 79 | hostname, err := os.Hostname() 80 | if err != nil { 81 | hostname = "unknown" 82 | } 83 | 84 | if cl == nil { // we are the only node in a new cluster 85 | set(st, "/ctl/name", clusterName, store.Missing) 86 | set(st, "/ctl/node/"+self+"/addr", listenAddr, store.Missing) 87 | set(st, "/ctl/node/"+self+"/hostname", hostname, store.Missing) 88 | set(st, "/ctl/node/"+self+"/version", Version, store.Missing) 89 | set(st, "/ctl/cal/0", self, store.Missing) 90 | if buri == "" { 91 | set(st, "/ctl/ns/"+clusterName+"/"+self, listenAddr, store.Missing) 92 | } 93 | calSrv(<-st.Seqns) 94 | // Skip ahead alpha steps so that the registrar can provide a 95 | // meaningful cluster. 96 | for i := 0; i < alpha; i++ { 97 | st.Ops <- store.Op{1 + <-st.Seqns, store.Nop} 98 | } 99 | canWrite <- true 100 | go setReady(pr, self) 101 | } else { 102 | setC(cl, "/ctl/node/"+self+"/addr", listenAddr, store.Clobber) 103 | setC(cl, "/ctl/node/"+self+"/hostname", hostname, store.Clobber) 104 | setC(cl, "/ctl/node/"+self+"/version", Version, store.Clobber) 105 | 106 | rev, err := cl.Rev() 107 | if err != nil { 108 | panic(err) 109 | } 110 | 111 | stop := make(chan bool, 1) 112 | go follow(st, cl, rev+1, stop) 113 | 114 | errs := make(chan error) 115 | go func() { 116 | e, ok := <-errs 117 | if ok { 118 | panic(e) 119 | } 120 | }() 121 | doozer.Walk(cl, rev, "/", cloner{st.Ops, cl, rev}, errs) 122 | close(errs) 123 | st.Flush() 124 | 125 | ch, err := st.Wait(store.Any, rev+1) 126 | if err == nil { 127 | <-ch 128 | } 129 | 130 | go func() { 131 | n := activate(st, self, cl) 132 | calSrv(n) 133 | advanceUntil(cl, st.Seqns, n+alpha) 134 | stop <- true 135 | canWrite <- true 136 | go setReady(pr, self) 137 | if buri != "" { 138 | b, err := doozer.DialUri(buri, "") 139 | if err != nil { 140 | panic(err) 141 | } 142 | setC( 143 | b, 144 | "/ctl/ns/"+clusterName+"/"+self, 145 | listenAddr, 146 | store.Missing, 147 | ) 148 | } 149 | }() 150 | } 151 | 152 | shun := make(chan string, 3) // sufficient for a cluster of 7 153 | go member.Clean(shun, st, pr) 154 | go server.ListenAndServe(listener, canWrite, st, pr, rwsk, rosk, self) 155 | 156 | if rwsk == "" && rosk == "" && webListener != nil { 157 | web.Store = st 158 | web.ClusterName = clusterName 159 | go web.Serve(webListener) 160 | } 161 | 162 | go func() { 163 | for p := range out { 164 | n, err := udpConn.WriteTo(p.Data, p.Addr) 165 | if err != nil { 166 | log.Println(err) 167 | continue 168 | } 169 | if n != len(p.Data) { 170 | log.Println("packet len too long:", len(p.Data)) 171 | continue 172 | } 173 | } 174 | }() 175 | 176 | selfAddr, ok := udpConn.LocalAddr().(*net.UDPAddr) 177 | if !ok { 178 | panic("no UDP addr") 179 | } 180 | lv := liveness{ 181 | timeout: kickTimeout, 182 | ival: kickTimeout / 2, 183 | self: selfAddr, 184 | shun: shun, 185 | } 186 | for { 187 | t := time.Now().UnixNano() 188 | 189 | buf := make([]byte, maxUDPLen) 190 | n, addr, err := udpConn.ReadFromUDP(buf) 191 | if err != nil && strings.Contains(err.Error(), "use of closed network connection") { 192 | log.Printf("<<<< EXITING >>>>") 193 | return 194 | } 195 | if err != nil { 196 | log.Println(err) 197 | continue 198 | } 199 | 200 | buf = buf[:n] 201 | 202 | lv.mark(addr, t) 203 | lv.check(t) 204 | 205 | in <- consensus.Packet{addr, buf} 206 | } 207 | } 208 | 209 | func activate(st *store.Store, self string, c *doozer.Conn) int64 { 210 | rev, _ := st.Snap() 211 | 212 | for _, base := range store.Getdir(st, calDir) { 213 | p := calDir + "/" + base 214 | v, rev := st.Get(p) 215 | if rev != store.Dir && v[0] == "" { 216 | seqn, err := c.Set(p, rev, []byte(self)) 217 | if err != nil { 218 | log.Println(err) 219 | continue 220 | } 221 | 222 | return seqn 223 | } 224 | } 225 | 226 | for { 227 | ch, err := st.Wait(calGlob, rev+1) 228 | if err != nil { 229 | panic(err) 230 | } 231 | ev, ok := <-ch 232 | if !ok { 233 | panic(io.EOF) 234 | } 235 | rev = ev.Rev 236 | // TODO ev.IsEmpty() 237 | if ev.IsSet() && ev.Body == "" { 238 | seqn, err := c.Set(ev.Path, ev.Rev, []byte(self)) 239 | if err != nil { 240 | log.Println(err) 241 | continue 242 | } 243 | return seqn 244 | } else if ev.IsSet() && ev.Body == self { 245 | return ev.Seqn 246 | } 247 | } 248 | 249 | return 0 250 | } 251 | 252 | func advanceUntil(cl *doozer.Conn, ver <-chan int64, done int64) { 253 | for <-ver < done { 254 | cl.Nop() 255 | } 256 | } 257 | 258 | func set(st *store.Store, path, body string, rev int64) { 259 | mut := store.MustEncodeSet(path, body, rev) 260 | st.Ops <- store.Op{1 + <-st.Seqns, mut} 261 | } 262 | 263 | func setC(cl *doozer.Conn, path, body string, rev int64) { 264 | _, err := cl.Set(path, rev, []byte(body)) 265 | if err != nil { 266 | panic(err) 267 | } 268 | } 269 | 270 | func follow(st *store.Store, cl *doozer.Conn, rev int64, stop chan bool) { 271 | for { 272 | ev, err := cl.Wait("/**", rev) 273 | if err != nil { 274 | panic(err) 275 | } 276 | 277 | // store.Clobber is okay here because the event 278 | // has already passed through another store 279 | mut := store.MustEncodeSet(ev.Path, string(ev.Body), store.Clobber) 280 | st.Ops <- store.Op{ev.Rev, mut} 281 | rev = ev.Rev + 1 282 | 283 | select { 284 | case <-stop: 285 | return 286 | default: 287 | } 288 | } 289 | } 290 | 291 | type cloner struct { 292 | ch chan<- store.Op 293 | cl *doozer.Conn 294 | storeRev int64 295 | } 296 | 297 | func (c cloner) VisitDir(path string, f *doozer.FileInfo) bool { 298 | return true 299 | } 300 | 301 | func (c cloner) VisitFile(path string, f *doozer.FileInfo) { 302 | // store.Clobber is okay here because the event 303 | // has already passed through another store 304 | body, _, err := c.cl.Get(path, &c.storeRev) 305 | if err != nil { 306 | panic(err) 307 | } 308 | mut := store.MustEncodeSet(path, string(body), store.Clobber) 309 | c.ch <- store.Op{f.Rev, mut} 310 | } 311 | 312 | func setReady(p consensus.Proposer, self string) { 313 | m, err := store.EncodeSet("/ctl/node/"+self+"/writable", "true", 0) 314 | if err != nil { 315 | log.Println(err) 316 | return 317 | } 318 | p.Propose([]byte(m)) 319 | } 320 | -------------------------------------------------------------------------------- /server/msg.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. 2 | // source: msg.proto 3 | // DO NOT EDIT! 4 | 5 | package server 6 | 7 | import proto "code.google.com/p/goprotobuf/proto" 8 | import json "encoding/json" 9 | import math "math" 10 | 11 | // Reference proto, json, and math imports to suppress error if they are not otherwise used. 12 | var _ = proto.Marshal 13 | var _ = &json.SyntaxError{} 14 | var _ = math.Inf 15 | 16 | type request_Verb int32 17 | 18 | const ( 19 | request_GET request_Verb = 1 20 | request_SET request_Verb = 2 21 | request_DEL request_Verb = 3 22 | request_REV request_Verb = 5 23 | request_WAIT request_Verb = 6 24 | request_NOP request_Verb = 7 25 | request_WALK request_Verb = 9 26 | request_GETDIR request_Verb = 14 27 | request_STAT request_Verb = 16 28 | request_SELF request_Verb = 20 29 | request_ACCESS request_Verb = 99 30 | ) 31 | 32 | var request_Verb_name = map[int32]string{ 33 | 1: "GET", 34 | 2: "SET", 35 | 3: "DEL", 36 | 5: "REV", 37 | 6: "WAIT", 38 | 7: "NOP", 39 | 9: "WALK", 40 | 14: "GETDIR", 41 | 16: "STAT", 42 | 20: "SELF", 43 | 99: "ACCESS", 44 | } 45 | var request_Verb_value = map[string]int32{ 46 | "GET": 1, 47 | "SET": 2, 48 | "DEL": 3, 49 | "REV": 5, 50 | "WAIT": 6, 51 | "NOP": 7, 52 | "WALK": 9, 53 | "GETDIR": 14, 54 | "STAT": 16, 55 | "SELF": 20, 56 | "ACCESS": 99, 57 | } 58 | 59 | func (x request_Verb) Enum() *request_Verb { 60 | p := new(request_Verb) 61 | *p = x 62 | return p 63 | } 64 | func (x request_Verb) String() string { 65 | return proto.EnumName(request_Verb_name, int32(x)) 66 | } 67 | func (x request_Verb) MarshalJSON() ([]byte, error) { 68 | return json.Marshal(x.String()) 69 | } 70 | func (x *request_Verb) UnmarshalJSON(data []byte) error { 71 | value, err := proto.UnmarshalJSONEnum(request_Verb_value, data, "request_Verb") 72 | if err != nil { 73 | return err 74 | } 75 | *x = request_Verb(value) 76 | return nil 77 | } 78 | 79 | type response_Err int32 80 | 81 | const ( 82 | response_OTHER response_Err = 127 83 | response_TAG_IN_USE response_Err = 1 84 | response_UNKNOWN_VERB response_Err = 2 85 | response_READONLY response_Err = 3 86 | response_TOO_LATE response_Err = 4 87 | response_REV_MISMATCH response_Err = 5 88 | response_BAD_PATH response_Err = 6 89 | response_MISSING_ARG response_Err = 7 90 | response_RANGE response_Err = 8 91 | response_NOTDIR response_Err = 20 92 | response_ISDIR response_Err = 21 93 | response_NOENT response_Err = 22 94 | ) 95 | 96 | var response_Err_name = map[int32]string{ 97 | 127: "OTHER", 98 | 1: "TAG_IN_USE", 99 | 2: "UNKNOWN_VERB", 100 | 3: "READONLY", 101 | 4: "TOO_LATE", 102 | 5: "REV_MISMATCH", 103 | 6: "BAD_PATH", 104 | 7: "MISSING_ARG", 105 | 8: "RANGE", 106 | 20: "NOTDIR", 107 | 21: "ISDIR", 108 | 22: "NOENT", 109 | } 110 | var response_Err_value = map[string]int32{ 111 | "OTHER": 127, 112 | "TAG_IN_USE": 1, 113 | "UNKNOWN_VERB": 2, 114 | "READONLY": 3, 115 | "TOO_LATE": 4, 116 | "REV_MISMATCH": 5, 117 | "BAD_PATH": 6, 118 | "MISSING_ARG": 7, 119 | "RANGE": 8, 120 | "NOTDIR": 20, 121 | "ISDIR": 21, 122 | "NOENT": 22, 123 | } 124 | 125 | func (x response_Err) Enum() *response_Err { 126 | p := new(response_Err) 127 | *p = x 128 | return p 129 | } 130 | func (x response_Err) String() string { 131 | return proto.EnumName(response_Err_name, int32(x)) 132 | } 133 | func (x response_Err) MarshalJSON() ([]byte, error) { 134 | return json.Marshal(x.String()) 135 | } 136 | func (x *response_Err) UnmarshalJSON(data []byte) error { 137 | value, err := proto.UnmarshalJSONEnum(response_Err_value, data, "response_Err") 138 | if err != nil { 139 | return err 140 | } 141 | *x = response_Err(value) 142 | return nil 143 | } 144 | 145 | type request struct { 146 | Tag *int32 `protobuf:"varint,1,opt,name=tag" json:"tag,omitempty"` 147 | Verb *request_Verb `protobuf:"varint,2,opt,name=verb,enum=server.request_Verb" json:"verb,omitempty"` 148 | Path *string `protobuf:"bytes,4,opt,name=path" json:"path,omitempty"` 149 | Value []byte `protobuf:"bytes,5,opt,name=value" json:"value,omitempty"` 150 | OtherTag *int32 `protobuf:"varint,6,opt,name=other_tag" json:"other_tag,omitempty"` 151 | Offset *int32 `protobuf:"varint,7,opt,name=offset" json:"offset,omitempty"` 152 | Rev *int64 `protobuf:"varint,9,opt,name=rev" json:"rev,omitempty"` 153 | XXX_unrecognized []byte `json:"-"` 154 | } 155 | 156 | func (this *request) Reset() { *this = request{} } 157 | func (this *request) String() string { return proto.CompactTextString(this) } 158 | func (*request) ProtoMessage() {} 159 | 160 | func (this *request) GetTag() int32 { 161 | if this != nil && this.Tag != nil { 162 | return *this.Tag 163 | } 164 | return 0 165 | } 166 | 167 | func (this *request) GetVerb() request_Verb { 168 | if this != nil && this.Verb != nil { 169 | return *this.Verb 170 | } 171 | return 0 172 | } 173 | 174 | func (this *request) GetPath() string { 175 | if this != nil && this.Path != nil { 176 | return *this.Path 177 | } 178 | return "" 179 | } 180 | 181 | func (this *request) GetValue() []byte { 182 | if this != nil { 183 | return this.Value 184 | } 185 | return nil 186 | } 187 | 188 | func (this *request) GetOtherTag() int32 { 189 | if this != nil && this.OtherTag != nil { 190 | return *this.OtherTag 191 | } 192 | return 0 193 | } 194 | 195 | func (this *request) GetOffset() int32 { 196 | if this != nil && this.Offset != nil { 197 | return *this.Offset 198 | } 199 | return 0 200 | } 201 | 202 | func (this *request) GetRev() int64 { 203 | if this != nil && this.Rev != nil { 204 | return *this.Rev 205 | } 206 | return 0 207 | } 208 | 209 | type response struct { 210 | Tag *int32 `protobuf:"varint,1,opt,name=tag" json:"tag,omitempty"` 211 | Flags *int32 `protobuf:"varint,2,opt,name=flags" json:"flags,omitempty"` 212 | Rev *int64 `protobuf:"varint,3,opt,name=rev" json:"rev,omitempty"` 213 | Path *string `protobuf:"bytes,5,opt,name=path" json:"path,omitempty"` 214 | Value []byte `protobuf:"bytes,6,opt,name=value" json:"value,omitempty"` 215 | Len *int32 `protobuf:"varint,8,opt,name=len" json:"len,omitempty"` 216 | ErrCode *response_Err `protobuf:"varint,100,opt,name=err_code,enum=server.response_Err" json:"err_code,omitempty"` 217 | ErrDetail *string `protobuf:"bytes,101,opt,name=err_detail" json:"err_detail,omitempty"` 218 | XXX_unrecognized []byte `json:"-"` 219 | } 220 | 221 | func (this *response) Reset() { *this = response{} } 222 | func (this *response) String() string { return proto.CompactTextString(this) } 223 | func (*response) ProtoMessage() {} 224 | 225 | func (this *response) GetTag() int32 { 226 | if this != nil && this.Tag != nil { 227 | return *this.Tag 228 | } 229 | return 0 230 | } 231 | 232 | func (this *response) GetFlags() int32 { 233 | if this != nil && this.Flags != nil { 234 | return *this.Flags 235 | } 236 | return 0 237 | } 238 | 239 | func (this *response) GetRev() int64 { 240 | if this != nil && this.Rev != nil { 241 | return *this.Rev 242 | } 243 | return 0 244 | } 245 | 246 | func (this *response) GetPath() string { 247 | if this != nil && this.Path != nil { 248 | return *this.Path 249 | } 250 | return "" 251 | } 252 | 253 | func (this *response) GetValue() []byte { 254 | if this != nil { 255 | return this.Value 256 | } 257 | return nil 258 | } 259 | 260 | func (this *response) GetLen() int32 { 261 | if this != nil && this.Len != nil { 262 | return *this.Len 263 | } 264 | return 0 265 | } 266 | 267 | func (this *response) GetErrCode() response_Err { 268 | if this != nil && this.ErrCode != nil { 269 | return *this.ErrCode 270 | } 271 | return 0 272 | } 273 | 274 | func (this *response) GetErrDetail() string { 275 | if this != nil && this.ErrDetail != nil { 276 | return *this.ErrDetail 277 | } 278 | return "" 279 | } 280 | 281 | func init() { 282 | proto.RegisterEnum("server.request_Verb", request_Verb_name, request_Verb_value) 283 | proto.RegisterEnum("server.response_Err", response_Err_name, response_Err_value) 284 | } 285 | -------------------------------------------------------------------------------- /consensus/manager.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | import ( 4 | "code.google.com/p/goprotobuf/proto" 5 | "container/heap" 6 | "github.com/ha/doozerd/store" 7 | "log" 8 | "net" 9 | "sort" 10 | "time" 11 | ) 12 | 13 | type packet struct { 14 | Addr *net.UDPAddr 15 | msg 16 | } 17 | 18 | type packets []*packet 19 | 20 | func (p *packets) Len() int { 21 | return len(*p) 22 | } 23 | 24 | func (p *packets) Less(i, j int) bool { 25 | a := *p 26 | return *a[i].Seqn < *a[j].Seqn 27 | } 28 | 29 | func (p *packets) Push(x interface{}) { 30 | *p = append(*p, x.(*packet)) 31 | } 32 | 33 | func (p *packets) Pop() (x interface{}) { 34 | a := *p 35 | i := len(a) - 1 36 | *p, x = a[:i], a[i] 37 | return 38 | } 39 | 40 | func (p *packets) Swap(i, j int) { 41 | a := *p 42 | a[i], a[j] = a[j], a[i] 43 | } 44 | 45 | type Packet struct { 46 | Addr *net.UDPAddr 47 | Data []byte 48 | } 49 | 50 | type trigger struct { 51 | t int64 // trigger time 52 | n int64 // seqn 53 | } 54 | 55 | type triggers []trigger 56 | 57 | func (t *triggers) Len() int { 58 | return len(*t) 59 | } 60 | 61 | func (t *triggers) Less(i, j int) bool { 62 | a := *t 63 | if a[i].t == a[j].t { 64 | return a[i].n < a[j].n 65 | } 66 | return a[i].t < a[j].t 67 | } 68 | 69 | func (t *triggers) Push(x interface{}) { 70 | *t = append(*t, x.(trigger)) 71 | } 72 | 73 | func (t *triggers) Pop() (x interface{}) { 74 | a := *t 75 | i := len(a) - 1 76 | *t, x = a[:i], a[i] 77 | return nil 78 | } 79 | 80 | func (t *triggers) Swap(i, j int) { 81 | a := *t 82 | a[i], a[j] = a[j], a[i] 83 | } 84 | 85 | type Stats struct { 86 | // Current queue sizes 87 | Runs int 88 | WaitPackets int 89 | WaitTicks int 90 | 91 | // Totals over all time 92 | TotalRuns int64 93 | TotalFills int64 94 | TotalTicks int64 95 | TotalRecv [nmsg]int64 96 | } 97 | 98 | // DefRev is the rev in which this manager was defined; 99 | // it will participate starting at DefRev+Alpha. 100 | type Manager struct { 101 | Self string 102 | DefRev int64 103 | Alpha int64 104 | In <-chan Packet 105 | Out chan<- Packet 106 | Ops chan<- store.Op 107 | PSeqn chan<- int64 108 | Props <-chan *Prop 109 | TFill int64 110 | Store *store.Store 111 | Ticker <-chan time.Time 112 | Stats Stats 113 | run map[int64]*run 114 | next int64 // unused seqn 115 | fill triggers 116 | packet packets 117 | tick triggers 118 | } 119 | 120 | type Prop struct { 121 | Seqn int64 122 | Mut []byte 123 | } 124 | 125 | var tickTemplate = &msg{Cmd: tick} 126 | var fillTemplate = &msg{Cmd: propose, Value: []byte(store.Nop)} 127 | 128 | func (m *Manager) Run() { 129 | m.run = make(map[int64]*run) 130 | runCh, err := m.Store.Wait(store.Any, m.DefRev) 131 | if err != nil { 132 | panic(err) // can't happen 133 | } 134 | 135 | for { 136 | m.Stats.Runs = len(m.run) 137 | m.Stats.WaitPackets = len(m.packet) 138 | m.Stats.WaitTicks = len(m.tick) 139 | 140 | select { 141 | case e, ok := <-runCh: 142 | if !ok { 143 | return 144 | } 145 | log.Println("event", e) 146 | 147 | runCh, err = m.Store.Wait(store.Any, e.Seqn+1) 148 | if err != nil { 149 | panic(err) // can't happen 150 | } 151 | 152 | m.event(e) 153 | m.Stats.TotalRuns++ 154 | log.Println("runs:", fmtRuns(m.run)) 155 | log.Println("avg tick delay:", avg(m.tick)) 156 | log.Println("avg fill delay:", avg(m.fill)) 157 | case p := <-m.In: 158 | if p1 := recvPacket(&m.packet, p); p1 != nil { 159 | m.Stats.TotalRecv[*p1.msg.Cmd]++ 160 | } 161 | case pr := <-m.Props: 162 | m.propose(&m.packet, pr, time.Now().UnixNano()) 163 | case t := <-m.Ticker: 164 | m.doTick(t.UnixNano()) 165 | } 166 | 167 | m.pump() 168 | } 169 | } 170 | 171 | func (m *Manager) pump() { 172 | for len(m.packet) > 0 { 173 | p := m.packet[0] 174 | log.Printf("p.seqn=%d m.next=%d", *p.Seqn, m.next) 175 | if *p.Seqn >= m.next { 176 | break 177 | } 178 | heap.Pop(&m.packet) 179 | 180 | r := m.run[*p.Seqn] 181 | if r == nil || r.l.done { 182 | go sendLearn(m.Out, p, m.Store) 183 | } else { 184 | r.update(p, r.indexOfAddr(p.Addr), &m.tick) 185 | } 186 | } 187 | } 188 | 189 | func (m *Manager) doTick(t int64) { 190 | n := applyTriggers(&m.packet, &m.fill, t, fillTemplate) 191 | m.Stats.TotalFills += int64(n) 192 | if n > 0 { 193 | log.Println("applied fills", n) 194 | } 195 | 196 | n = applyTriggers(&m.packet, &m.tick, t, tickTemplate) 197 | m.Stats.TotalTicks += int64(n) 198 | if n > 0 { 199 | log.Println("applied m.tick", n) 200 | } 201 | } 202 | 203 | func (m *Manager) propose(q heap.Interface, pr *Prop, t int64) { 204 | log.Println("prop", pr) 205 | p := new(packet) 206 | p.msg.Seqn = &pr.Seqn 207 | p.msg.Cmd = propose 208 | p.msg.Value = pr.Mut 209 | heap.Push(q, p) 210 | for n := pr.Seqn - 1; ; n-- { 211 | r := m.run[n] 212 | if r == nil || r.isLeader(m.Self) { 213 | break 214 | } else { 215 | schedTrigger(&m.fill, n, t, m.TFill) 216 | } 217 | } 218 | } 219 | 220 | func sendLearn(out chan<- Packet, p *packet, st *store.Store) { 221 | if p.msg.Cmd != nil && *p.msg.Cmd == msg_INVITE { 222 | ch, err := st.Wait(store.Any, *p.Seqn) 223 | 224 | if err == store.ErrTooLate { 225 | log.Println(err) 226 | } else { 227 | e := <-ch 228 | m := msg{ 229 | Seqn: &e.Seqn, 230 | Cmd: learn, 231 | Value: []byte(e.Mut), 232 | } 233 | buf, _ := proto.Marshal(&m) 234 | out <- Packet{p.Addr, buf} 235 | } 236 | } 237 | } 238 | 239 | func recvPacket(q heap.Interface, P Packet) (p *packet) { 240 | p = new(packet) 241 | p.Addr = P.Addr 242 | 243 | err := proto.Unmarshal(P.Data, &p.msg) 244 | if err != nil { 245 | log.Println(err) 246 | return nil 247 | } 248 | 249 | if p.msg.Seqn == nil || p.msg.Cmd == nil { 250 | log.Printf("discarding %#v", p) 251 | return nil 252 | } 253 | 254 | heap.Push(q, p) 255 | return p 256 | } 257 | 258 | func avg(v []trigger) (n int64) { 259 | t := time.Now().UnixNano() 260 | if len(v) == 0 { 261 | return -1 262 | } 263 | for _, x := range v { 264 | n += x.t - t 265 | } 266 | return n / int64(len(v)) 267 | } 268 | 269 | func schedTrigger(q heap.Interface, n, t, tfill int64) { 270 | heap.Push(q, trigger{n: n, t: t + tfill}) 271 | } 272 | 273 | func applyTriggers(ps *packets, ticks *triggers, now int64, tpl *msg) (n int) { 274 | for ticks.Len() > 0 { 275 | tt := (*ticks)[0] 276 | if tt.t > now { 277 | break 278 | } 279 | 280 | heap.Pop(ticks) 281 | 282 | p := new(packet) 283 | p.msg = *tpl 284 | p.msg.Seqn = &tt.n 285 | log.Println("applying", *p.Seqn, msg_Cmd_name[int32(*p.Cmd)]) 286 | heap.Push(ps, p) 287 | n++ 288 | } 289 | return 290 | } 291 | 292 | func (m *Manager) event(e store.Event) { 293 | delete(m.run, e.Seqn) 294 | log.Printf("del run %d", e.Seqn) 295 | m.addRun(e) 296 | } 297 | 298 | func (m *Manager) addRun(e store.Event) (r *run) { 299 | r = new(run) 300 | r.self = m.Self 301 | r.out = m.Out 302 | r.ops = m.Ops 303 | r.bound = initialWaitBound 304 | r.seqn = e.Seqn + m.Alpha 305 | r.cals = getCals(e) 306 | r.addr = getAddrs(e, r.cals) 307 | if len(r.cals) < 1 { 308 | r.cals = m.run[r.seqn-1].cals 309 | r.addr = m.run[r.seqn-1].addr 310 | } 311 | r.c.size = len(r.cals) 312 | r.c.quor = r.quorum() 313 | r.c.crnd = r.indexOf(r.self) + int64(len(r.cals)) 314 | r.l.init(len(r.cals), int64(r.quorum())) 315 | m.run[r.seqn] = r 316 | if r.isLeader(m.Self) { 317 | log.Printf("pseqn %d", r.seqn) 318 | m.PSeqn <- r.seqn 319 | } 320 | log.Printf("add run %d", r.seqn) 321 | m.next = r.seqn + 1 322 | return r 323 | } 324 | 325 | func getCals(g store.Getter) []string { 326 | ents := store.Getdir(g, "/ctl/cal") 327 | cals := make([]string, len(ents)) 328 | 329 | i := 0 330 | for _, cal := range ents { 331 | id := store.GetString(g, "/ctl/cal/"+cal) 332 | if id != "" { 333 | cals[i] = id 334 | i++ 335 | } 336 | } 337 | 338 | cals = cals[0:i] 339 | sort.Strings(cals) 340 | 341 | return cals 342 | } 343 | 344 | func getAddrs(g store.Getter, cals []string) (a []*net.UDPAddr) { 345 | a = make([]*net.UDPAddr, len(cals)) 346 | var i int 347 | var err error 348 | for _, id := range cals { 349 | s := store.GetString(g, "/ctl/node/"+id+"/addr") 350 | a[i], err = net.ResolveUDPAddr("udp", s) 351 | if err != nil { 352 | log.Println(err) 353 | } else { 354 | i++ 355 | } 356 | } 357 | return a[:i] 358 | } 359 | 360 | func fmtRuns(rs map[int64]*run) (s string) { 361 | var ns []int 362 | for i := range rs { 363 | ns = append(ns, int(i)) 364 | } 365 | sort.Ints(ns) 366 | for _, i := range ns { 367 | r := rs[int64(i)] 368 | if r.l.done { 369 | s += "X" 370 | } else if r.prop { 371 | s += "o" 372 | } else { 373 | s += "." 374 | } 375 | } 376 | return s 377 | } 378 | -------------------------------------------------------------------------------- /store/store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "errors" 5 | "math" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // Special values for a revision. 12 | const ( 13 | Missing = int64(-iota) 14 | Clobber 15 | Dir 16 | nop 17 | ) 18 | 19 | // TODO revisit this when package regexp is more complete (e.g. do Unicode) 20 | const charPat = `[a-zA-Z0-9.\-]` 21 | 22 | var pathRe = mustBuildRe(charPat) 23 | 24 | var Any = MustCompileGlob("/**") 25 | 26 | var ErrTooLate = errors.New("too late") 27 | 28 | var ( 29 | ErrBadMutation = errors.New("bad mutation") 30 | ErrRevMismatch = errors.New("rev mismatch") 31 | ErrBadPath = errors.New("bad path") 32 | ) 33 | 34 | func mustBuildRe(p string) *regexp.Regexp { 35 | return regexp.MustCompile(`^/$|^(/` + p + `+)+$`) 36 | } 37 | 38 | // Applies mutations sent on Ops in sequence according to field Seqn. Any 39 | // errors that occur will be written to ErrorPath. Duplicate operations at a 40 | // given position are sliently ignored. 41 | type Store struct { 42 | Ops chan<- Op 43 | Seqns <-chan int64 44 | Waiting <-chan int 45 | watchCh chan *watch 46 | watches []*watch 47 | todo []Op 48 | state *state 49 | head int64 50 | log map[int64]Event 51 | cleanCh chan int64 52 | flush chan bool 53 | } 54 | 55 | // Represents an operation to apply to the store at position Seqn. 56 | // 57 | // If Mut is Nop, no change will be made, but an event will still be sent. 58 | type Op struct { 59 | Seqn int64 60 | Mut string 61 | } 62 | 63 | type state struct { 64 | ver int64 65 | root node 66 | } 67 | 68 | type watch struct { 69 | glob *Glob 70 | rev int64 71 | c chan<- Event 72 | } 73 | 74 | // Creates a new, empty data store. Mutations will be applied in order, 75 | // starting at number 1 (number 0 can be thought of as the creation of the 76 | // store). 77 | func New() *Store { 78 | ops := make(chan Op) 79 | seqns := make(chan int64) 80 | watches := make(chan int) 81 | 82 | st := &Store{ 83 | Ops: ops, 84 | Seqns: seqns, 85 | Waiting: watches, 86 | watchCh: make(chan *watch), 87 | watches: []*watch{}, 88 | state: &state{0, emptyDir}, 89 | log: map[int64]Event{}, 90 | cleanCh: make(chan int64), 91 | flush: make(chan bool), 92 | } 93 | 94 | go st.process(ops, seqns, watches) 95 | return st 96 | } 97 | 98 | func split(path string) []string { 99 | if path == "/" { 100 | return []string{} 101 | } 102 | return strings.Split(path[1:], "/") 103 | } 104 | 105 | func join(parts []string) string { 106 | return "/" + strings.Join(parts, "/") 107 | } 108 | 109 | func checkPath(k string) error { 110 | if !pathRe.MatchString(k) { 111 | return ErrBadPath 112 | } 113 | return nil 114 | } 115 | 116 | // Returns a mutation that can be applied to a `Store`. The mutation will set 117 | // the contents of the file at `path` to `body` iff `rev` is greater than 118 | // of equal to the file's revision at the time of application, with 119 | // one exception: if `rev` is Clobber, the file will be set unconditionally. 120 | func EncodeSet(path, body string, rev int64) (mutation string, err error) { 121 | if err = checkPath(path); err != nil { 122 | return 123 | } 124 | return strconv.FormatInt(rev, 10) + ":" + path + "=" + body, nil 125 | } 126 | 127 | // Returns a mutation that can be applied to a `Store`. The mutation will cause 128 | // the file at `path` to be deleted iff `rev` is greater than 129 | // of equal to the file's revision at the time of application, with 130 | // one exception: if `rev` is Clobber, the file will be deleted 131 | // unconditionally. 132 | func EncodeDel(path string, rev int64) (mutation string, err error) { 133 | if err = checkPath(path); err != nil { 134 | return 135 | } 136 | return strconv.FormatInt(rev, 10) + ":" + path, nil 137 | } 138 | 139 | // MustEncodeSet is like EncodeSet but panics if the mutation cannot be 140 | // encoded. It simplifies safe initialization of global variables holding 141 | // mutations. 142 | func MustEncodeSet(path, body string, rev int64) (mutation string) { 143 | m, err := EncodeSet(path, body, rev) 144 | if err != nil { 145 | panic(err) 146 | } 147 | return m 148 | } 149 | 150 | // MustEncodeDel is like EncodeDel but panics if the mutation cannot be 151 | // encoded. It simplifies safe initialization of global variables holding 152 | // mutations. 153 | func MustEncodeDel(path string, rev int64) (mutation string) { 154 | m, err := EncodeDel(path, rev) 155 | if err != nil { 156 | panic(err) 157 | } 158 | return m 159 | } 160 | 161 | func decode(mutation string) (path, v string, rev int64, keep bool, err error) { 162 | cm := strings.SplitN(mutation, ":", 2) 163 | 164 | if len(cm) != 2 { 165 | err = ErrBadMutation 166 | return 167 | } 168 | 169 | rev, err = strconv.ParseInt(cm[0], 10, 64) 170 | if err != nil { 171 | return 172 | } 173 | 174 | kv := strings.SplitN(cm[1], "=", 2) 175 | 176 | if err = checkPath(kv[0]); err != nil { 177 | return 178 | } 179 | 180 | switch len(kv) { 181 | case 1: 182 | return kv[0], "", rev, false, nil 183 | case 2: 184 | return kv[0], kv[1], rev, true, nil 185 | } 186 | panic("unreachable") 187 | } 188 | 189 | func (st *Store) notify(e Event, ws []*watch) (nws []*watch) { 190 | for _, w := range ws { 191 | if e.Seqn >= w.rev && w.glob.Match(e.Path) { 192 | w.c <- e 193 | } else { 194 | nws = append(nws, w) 195 | } 196 | } 197 | 198 | return nws 199 | } 200 | 201 | func (st *Store) closeWatches() { 202 | for _, w := range st.watches { 203 | close(w.c) 204 | } 205 | } 206 | 207 | func (st *Store) process(ops <-chan Op, seqns chan<- int64, watches chan<- int) { 208 | defer st.closeWatches() 209 | 210 | for { 211 | var flush bool 212 | ver, values := st.state.ver, st.state.root 213 | 214 | // Take any incoming requests and queue them up. 215 | select { 216 | case a, ok := <-ops: 217 | if !ok { 218 | return 219 | } 220 | 221 | if a.Seqn > ver { 222 | st.todo = append(st.todo, a) 223 | } 224 | case w := <-st.watchCh: 225 | n, ws := w.rev, []*watch{w} 226 | for ; len(ws) > 0 && n < st.head; n++ { 227 | ws = []*watch{} 228 | } 229 | for ; len(ws) > 0 && n <= ver; n++ { 230 | ws = st.notify(st.log[n], ws) 231 | } 232 | 233 | st.watches = append(st.watches, ws...) 234 | case seqn := <-st.cleanCh: 235 | for ; st.head <= seqn; st.head++ { 236 | delete(st.log, st.head) 237 | } 238 | case seqns <- ver: 239 | // nothing to do here 240 | case watches <- len(st.watches): 241 | // nothing to do here 242 | case flush = <-st.flush: 243 | // nothing 244 | } 245 | 246 | var ev Event 247 | // If we have any mutations that can be applied, do them. 248 | for len(st.todo) > 0 { 249 | i := firstTodo(st.todo) 250 | t := st.todo[i] 251 | if flush && ver < t.Seqn { 252 | ver = t.Seqn - 1 253 | } 254 | if t.Seqn > ver+1 { 255 | break 256 | } 257 | 258 | st.todo = append(st.todo[:i], st.todo[i+1:]...) 259 | if t.Seqn < ver+1 { 260 | continue 261 | } 262 | 263 | values, ev = values.apply(t.Seqn, t.Mut) 264 | st.state = &state{ev.Seqn, values} 265 | ver = ev.Seqn 266 | if !flush { 267 | st.log[ev.Seqn] = ev 268 | st.watches = st.notify(ev, st.watches) 269 | } 270 | } 271 | 272 | // A flush just gets one final event. 273 | if flush { 274 | st.log[ev.Seqn] = ev 275 | st.watches = st.notify(ev, st.watches) 276 | st.head = ver + 1 277 | } 278 | } 279 | } 280 | 281 | func firstTodo(a []Op) (pos int) { 282 | n := int64(math.MaxInt64) 283 | pos = -1 284 | for i, o := range a { 285 | if o.Seqn < n { 286 | n = o.Seqn 287 | pos = i 288 | } 289 | } 290 | return 291 | } 292 | 293 | // Returns a point-in-time snapshot of the contents of the store. 294 | func (st *Store) Snap() (ver int64, g Getter) { 295 | // WARNING: Be sure to read the pointer value of st.state only once. If you 296 | // need multiple accesses, copy the pointer first. 297 | p := st.state 298 | 299 | return p.ver, p.root 300 | } 301 | 302 | // Gets the value stored at `path`, if any. 303 | // 304 | // If no value is stored at `path`, `rev` will be `Missing` and `value` will be 305 | // nil. 306 | // 307 | // if `path` is a directory, `rev` will be `Dir` and `value` will be a list of 308 | // entries. 309 | // 310 | // Otherwise, `rev` is the revision and `value[0]` is the body. 311 | func (st *Store) Get(path string) (value []string, rev int64) { 312 | _, g := st.Snap() 313 | return g.Get(path) 314 | } 315 | 316 | func (st *Store) Stat(path string) (int32, int64) { 317 | _, g := st.Snap() 318 | return g.Stat(path) 319 | } 320 | 321 | // Apply all operations in the internal queue, even if there are gaps in the 322 | // sequence (gaps will be treated as no-ops). This is only useful for 323 | // bootstrapping a store from a point-in-time snapshot of another store. 324 | func (st *Store) Flush() { 325 | st.flush <- true 326 | } 327 | 328 | // Returns a chan that will receive a single event representing the 329 | // first change made to any file matching glob on or after rev. 330 | // 331 | // If rev is less than any value passed to st.Clean, Wait will return 332 | // ErrTooLate. 333 | func (st *Store) Wait(glob *Glob, rev int64) (<-chan Event, error) { 334 | if rev < 1 { 335 | rev = 1 336 | } 337 | 338 | ch := make(chan Event, 1) 339 | wt := &watch{ 340 | glob: glob, 341 | rev: rev, 342 | c: ch, 343 | } 344 | st.watchCh <- wt 345 | 346 | if rev < st.head { 347 | return nil, ErrTooLate 348 | } 349 | return ch, nil 350 | } 351 | 352 | func (st *Store) Clean(seqn int64) { 353 | st.cleanCh <- seqn 354 | } 355 | -------------------------------------------------------------------------------- /consensus/learner_test.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | import ( 4 | "github.com/bmizerany/assert" 5 | "testing" 6 | ) 7 | 8 | func TestLearnsAValueWithAQuorumOfOne(t *testing.T) { 9 | var ln learner 10 | ln.init(1, 1) 11 | 12 | m, v, ok := ln.update(newVoteFrom(0, 1, "foo")) 13 | assert.Equal(t, true, ln.done) 14 | assert.Equal(t, "foo", ln.v) 15 | assert.Equal(t, []byte("foo"), v) 16 | assert.Equal(t, true, ok) 17 | assert.Equal(t, &msg{Cmd: learn, Value: []byte("foo")}, m) 18 | } 19 | 20 | func TestLearnsOkStickyInSameRound(t *testing.T) { 21 | var ln learner 22 | ln.init(1, 1) 23 | 24 | m, v, ok := ln.update(newVoteFrom(0, 1, "foo")) 25 | assert.Equal(t, true, ln.done) 26 | assert.Equal(t, "foo", ln.v) 27 | assert.Equal(t, []byte("foo"), v) 28 | assert.Equal(t, true, ok) 29 | assert.Equal(t, &msg{Cmd: learn, Value: []byte("foo")}, m) 30 | 31 | m, v, ok = ln.update(newVoteFrom(1, 1, "bar")) 32 | assert.Equal(t, true, ln.done) 33 | assert.Equal(t, "foo", ln.v) 34 | assert.Equal(t, []byte(nil), v) 35 | assert.Equal(t, false, ok) 36 | assert.Equal(t, (*msg)(nil), m) 37 | } 38 | 39 | func TestLearnsOkStickyInNewRound(t *testing.T) { 40 | var ln learner 41 | ln.init(1, 1) 42 | 43 | m, v, ok := ln.update(newVoteFrom(0, 1, "foo")) 44 | assert.Equal(t, true, ln.done) 45 | assert.Equal(t, "foo", ln.v) 46 | assert.Equal(t, []byte("foo"), v) 47 | assert.Equal(t, true, ok) 48 | assert.Equal(t, &msg{Cmd: learn, Value: []byte("foo")}, m) 49 | 50 | m, v, ok = ln.update(newVoteFrom(0, 2, "bar")) 51 | assert.Equal(t, true, ln.done) 52 | assert.Equal(t, "foo", ln.v) 53 | assert.Equal(t, []byte(nil), v) 54 | assert.Equal(t, false, ok) 55 | assert.Equal(t, (*msg)(nil), m) 56 | } 57 | 58 | func TestLearnsAValueWithAQuorumOfTwo(t *testing.T) { 59 | var ln learner 60 | ln.init(3, 2) 61 | 62 | m, v, ok := ln.update(newVoteFrom(0, 1, "foo")) 63 | assert.Equal(t, false, ln.done) 64 | assert.Equal(t, []byte(nil), v) 65 | assert.Equal(t, false, ok) 66 | assert.Equal(t, (*msg)(nil), m) 67 | 68 | m, v, ok = ln.update(newVoteFrom(1, 1, "foo")) 69 | assert.Equal(t, true, ln.done) 70 | assert.Equal(t, "foo", ln.v) 71 | assert.Equal(t, []byte("foo"), v) 72 | assert.Equal(t, true, ok) 73 | assert.Equal(t, &msg{Cmd: learn, Value: []byte("foo")}, m) 74 | } 75 | 76 | func TestIgnoresMalformedMessageBadRoundNumber(t *testing.T) { 77 | var ln learner 78 | ln.init(1, 1) 79 | 80 | m, v, ok := ln.update(newVoteFrom(0, 0, "bar")) 81 | assert.Equal(t, false, ln.done) 82 | assert.Equal(t, []byte(nil), v) 83 | assert.Equal(t, false, ok) 84 | assert.Equal(t, (*msg)(nil), m) 85 | 86 | m, v, ok = ln.update(newVoteFrom(0, 1, "foo")) 87 | assert.Equal(t, true, ln.done) 88 | assert.Equal(t, "foo", ln.v) 89 | assert.Equal(t, []byte("foo"), v) 90 | assert.Equal(t, true, ok) 91 | assert.Equal(t, &msg{Cmd: learn, Value: []byte("foo")}, m) 92 | } 93 | 94 | func TestIgnoresMultipleMessagesFromSameSender(t *testing.T) { 95 | var ln learner 96 | ln.init(3, 2) 97 | 98 | m, v, ok := ln.update(newVoteFrom(0, 1, "foo")) 99 | assert.Equal(t, false, ln.done) 100 | assert.Equal(t, []byte(nil), v) 101 | assert.Equal(t, false, ok) 102 | assert.Equal(t, (*msg)(nil), m) 103 | 104 | m, v, ok = ln.update(newVoteFrom(0, 1, "foo")) 105 | assert.Equal(t, false, ln.done) 106 | assert.Equal(t, []byte(nil), v) 107 | assert.Equal(t, false, ok) 108 | assert.Equal(t, (*msg)(nil), m) 109 | 110 | m, v, ok = ln.update(newVoteFrom(1, 1, "foo")) 111 | assert.Equal(t, true, ln.done) 112 | assert.Equal(t, "foo", ln.v) 113 | assert.Equal(t, []byte("foo"), v) 114 | assert.Equal(t, true, ok) 115 | assert.Equal(t, &msg{Cmd: learn, Value: []byte("foo")}, m) 116 | } 117 | 118 | func TestIgnoresSenderInOldRound(t *testing.T) { 119 | var ln learner 120 | ln.init(3, 2) 121 | 122 | m, v, ok := ln.update(newVoteFrom(0, 2, "foo")) 123 | assert.Equal(t, false, ln.done) 124 | assert.Equal(t, []byte(nil), v) 125 | assert.Equal(t, false, ok) 126 | assert.Equal(t, (*msg)(nil), m) 127 | 128 | m, v, ok = ln.update(newVoteFrom(1, 1, "foo")) 129 | assert.Equal(t, false, ln.done) 130 | assert.Equal(t, []byte(nil), v) 131 | assert.Equal(t, false, ok) 132 | assert.Equal(t, (*msg)(nil), m) 133 | 134 | m, v, ok = ln.update(newVoteFrom(1, 2, "foo")) 135 | assert.Equal(t, true, ln.done) 136 | assert.Equal(t, "foo", ln.v) 137 | assert.Equal(t, []byte("foo"), v) 138 | assert.Equal(t, true, ok) 139 | assert.Equal(t, &msg{Cmd: learn, Value: []byte("foo")}, m) 140 | } 141 | 142 | func TestResetsVotedFlags(t *testing.T) { 143 | var ln learner 144 | ln.init(3, 2) 145 | 146 | m, v, ok := ln.update(newVoteFrom(0, 1, "foo")) 147 | assert.Equal(t, false, ln.done) 148 | assert.Equal(t, []byte(nil), v) 149 | assert.Equal(t, false, ok) 150 | assert.Equal(t, (*msg)(nil), m) 151 | 152 | m, v, ok = ln.update(newVoteFrom(0, 2, "foo")) 153 | assert.Equal(t, false, ln.done) 154 | assert.Equal(t, []byte(nil), v) 155 | assert.Equal(t, false, ok) 156 | assert.Equal(t, (*msg)(nil), m) 157 | 158 | m, v, ok = ln.update(newVoteFrom(1, 2, "foo")) 159 | assert.Equal(t, true, ln.done) 160 | assert.Equal(t, "foo", ln.v) 161 | assert.Equal(t, []byte("foo"), v) 162 | assert.Equal(t, true, ok) 163 | assert.Equal(t, &msg{Cmd: learn, Value: []byte("foo")}, m) 164 | } 165 | 166 | func TestResetsVoteCounts(t *testing.T) { 167 | var ln learner 168 | ln.init(5, 3) 169 | 170 | m, v, ok := ln.update(newVoteFrom(0, 1, "foo")) 171 | assert.Equal(t, false, ln.done) 172 | assert.Equal(t, []byte(nil), v) 173 | assert.Equal(t, false, ok) 174 | assert.Equal(t, (*msg)(nil), m) 175 | 176 | m, v, ok = ln.update(newVoteFrom(1, 1, "foo")) 177 | assert.Equal(t, false, ln.done) 178 | assert.Equal(t, []byte(nil), v) 179 | assert.Equal(t, false, ok) 180 | assert.Equal(t, (*msg)(nil), m) 181 | 182 | m, v, ok = ln.update(newVoteFrom(0, 2, "foo")) 183 | assert.Equal(t, false, ln.done) 184 | assert.Equal(t, []byte(nil), v) 185 | assert.Equal(t, false, ok) 186 | assert.Equal(t, (*msg)(nil), m) 187 | 188 | m, v, ok = ln.update(newVoteFrom(1, 2, "foo")) 189 | assert.Equal(t, false, ln.done) 190 | assert.Equal(t, []byte(nil), v) 191 | assert.Equal(t, false, ok) 192 | assert.Equal(t, (*msg)(nil), m) 193 | 194 | m, v, ok = ln.update(newVoteFrom(2, 2, "foo")) 195 | assert.Equal(t, true, ln.done) 196 | assert.Equal(t, "foo", ln.v) 197 | assert.Equal(t, []byte("foo"), v) 198 | assert.Equal(t, true, ok) 199 | assert.Equal(t, &msg{Cmd: learn, Value: []byte("foo")}, m) 200 | } 201 | 202 | func TestLearnsATheBestOfTwoValuesInSameRound(t *testing.T) { 203 | var ln learner 204 | ln.init(3, 2) 205 | 206 | m, v, ok := ln.update(newVoteFrom(0, 1, "foo")) 207 | assert.Equal(t, false, ln.done) 208 | assert.Equal(t, []byte(nil), v) 209 | assert.Equal(t, false, ok) 210 | assert.Equal(t, (*msg)(nil), m) 211 | 212 | m, v, ok = ln.update(newVoteFrom(2, 1, "bar")) 213 | assert.Equal(t, false, ln.done) 214 | assert.Equal(t, []byte(nil), v) 215 | assert.Equal(t, false, ok) 216 | assert.Equal(t, (*msg)(nil), m) 217 | 218 | m, v, ok = ln.update(newVoteFrom(1, 1, "foo")) 219 | assert.Equal(t, true, ln.done) 220 | assert.Equal(t, "foo", ln.v) 221 | } 222 | 223 | func TestBringsOrderOutOfChaos(t *testing.T) { 224 | var ln learner 225 | ln.init(3, 2) 226 | 227 | m, v, ok := ln.update(newVoteFrom(0, 1, "bar")) //valid 228 | assert.Equal(t, false, ln.done) 229 | assert.Equal(t, []byte(nil), v) 230 | assert.Equal(t, false, ok) 231 | assert.Equal(t, (*msg)(nil), m) 232 | m, v, ok = ln.update(newVoteFrom(2, 2, "funk")) //reset 233 | assert.Equal(t, false, ln.done) 234 | assert.Equal(t, []byte(nil), v) 235 | assert.Equal(t, false, ok) 236 | assert.Equal(t, (*msg)(nil), m) 237 | m, v, ok = ln.update(newVoteFrom(1, 1, "bar")) //ignored 238 | assert.Equal(t, false, ln.done) 239 | assert.Equal(t, []byte(nil), v) 240 | assert.Equal(t, false, ok) 241 | assert.Equal(t, (*msg)(nil), m) 242 | 243 | m, v, ok = ln.update(newVoteFrom(2, 1, "foo")) //ignored 244 | assert.Equal(t, false, ln.done) 245 | assert.Equal(t, []byte(nil), v) 246 | assert.Equal(t, false, ok) 247 | assert.Equal(t, (*msg)(nil), m) 248 | m, v, ok = ln.update(newVoteFrom(1, 2, "foo")) //valid 249 | assert.Equal(t, false, ln.done) 250 | assert.Equal(t, []byte(nil), v) 251 | assert.Equal(t, false, ok) 252 | assert.Equal(t, (*msg)(nil), m) 253 | m, v, ok = ln.update(newVoteFrom(0, 2, "foo")) //valid (at quorum) 254 | assert.Equal(t, true, ln.done) 255 | assert.Equal(t, "foo", ln.v) 256 | assert.Equal(t, []byte("foo"), v) 257 | assert.Equal(t, true, ok) 258 | assert.Equal(t, &msg{Cmd: learn, Value: []byte("foo")}, m) 259 | } 260 | 261 | func TestLearnerIgnoresBadMessages(t *testing.T) { 262 | var ln learner 263 | 264 | m, v, ok := ln.update(&packet{msg: msg{Cmd: vote}}, -1) // missing Vrnd 265 | assert.Equal(t, false, ln.done) 266 | assert.Equal(t, []byte(nil), v) 267 | assert.Equal(t, false, ok) 268 | assert.Equal(t, (*msg)(nil), m) 269 | } 270 | 271 | func TestSinkLearnsAValue(t *testing.T) { 272 | var ln learner 273 | 274 | m, v, ok := ln.update(&packet{msg: *newLearn("foo")}, -1) 275 | assert.Equal(t, true, ln.done) 276 | assert.Equal(t, "foo", ln.v) 277 | assert.Equal(t, []byte("foo"), v) 278 | assert.Equal(t, true, ok) 279 | assert.Equal(t, (*msg)(nil), m) 280 | } 281 | 282 | func TestSinkLearnsOkSticky(t *testing.T) { 283 | var ln learner 284 | 285 | m, v, ok := ln.update(&packet{msg: *newLearn("foo")}, -1) 286 | assert.Equal(t, true, ln.done) 287 | assert.Equal(t, "foo", ln.v) 288 | assert.Equal(t, []byte("foo"), v) 289 | assert.Equal(t, true, ok) 290 | assert.Equal(t, (*msg)(nil), m) 291 | 292 | m, v, ok = ln.update(&packet{msg: *newLearn("bar")}, -1) 293 | assert.Equal(t, true, ln.done) 294 | assert.Equal(t, "foo", ln.v) 295 | assert.Equal(t, []byte(nil), v) 296 | assert.Equal(t, false, ok) 297 | assert.Equal(t, (*msg)(nil), m) 298 | } 299 | -------------------------------------------------------------------------------- /consensus/coordinator_test.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | import ( 4 | "github.com/bmizerany/assert" 5 | "testing" 6 | ) 7 | 8 | func TestCoordIgnoreOldMessages(t *testing.T) { 9 | var co coordinator 10 | co.size = 10 11 | 12 | co.update(&packet{msg: *newPropose("foo")}, -1) 13 | 14 | co.update(&packet{msg: *msgTick}, -1) // force the start of a new round 15 | 16 | got, tick := co.update(newRsvpFrom(0, 1, 0, "")) 17 | assert.Equal(t, (*msg)(nil), got) 18 | assert.Equal(t, false, tick) 19 | 20 | got, tick = co.update(newRsvpFrom(1, 1, 0, "")) 21 | assert.Equal(t, (*msg)(nil), got) 22 | assert.Equal(t, false, tick) 23 | 24 | got, tick = co.update(newRsvpFrom(2, 1, 0, "")) 25 | assert.Equal(t, (*msg)(nil), got) 26 | assert.Equal(t, false, tick) 27 | 28 | got, tick = co.update(newRsvpFrom(3, 1, 0, "")) 29 | assert.Equal(t, (*msg)(nil), got) 30 | assert.Equal(t, false, tick) 31 | 32 | got, tick = co.update(newRsvpFrom(4, 1, 0, "")) 33 | assert.Equal(t, (*msg)(nil), got) 34 | assert.Equal(t, false, tick) 35 | 36 | got, tick = co.update(newRsvpFrom(5, 1, 0, "")) 37 | assert.Equal(t, (*msg)(nil), got) 38 | assert.Equal(t, false, tick) 39 | } 40 | 41 | func TestCoordStart(t *testing.T) { 42 | co := coordinator{crnd: 1} 43 | 44 | got, tick := co.update(&packet{msg: *newPropose("foo")}, -1) 45 | assert.Equal(t, newInvite(1), got) 46 | assert.Equal(t, true, tick) 47 | } 48 | 49 | func TestCoordQuorum(t *testing.T) { 50 | co := coordinator{ 51 | size: 10, 52 | quor: 6, 53 | crnd: 1, 54 | } 55 | 56 | co.update(&packet{msg: *newPropose("foo")}, -1) 57 | 58 | got, tick := co.update(newRsvpFrom(1, 1, 0, "")) 59 | assert.Equal(t, (*msg)(nil), got) 60 | assert.Equal(t, false, tick) 61 | 62 | got, tick = co.update(newRsvpFrom(2, 1, 0, "")) 63 | assert.Equal(t, (*msg)(nil), got) 64 | assert.Equal(t, false, tick) 65 | 66 | got, tick = co.update(newRsvpFrom(3, 1, 0, "")) 67 | assert.Equal(t, (*msg)(nil), got) 68 | assert.Equal(t, false, tick) 69 | 70 | got, tick = co.update(newRsvpFrom(4, 1, 0, "")) 71 | assert.Equal(t, (*msg)(nil), got) 72 | assert.Equal(t, false, tick) 73 | 74 | got, tick = co.update(newRsvpFrom(5, 1, 0, "")) 75 | assert.Equal(t, (*msg)(nil), got) 76 | assert.Equal(t, false, tick) 77 | } 78 | 79 | func TestCoordDuplicateRsvp(t *testing.T) { 80 | co := coordinator{ 81 | size: 10, 82 | quor: 6, 83 | crnd: 1, 84 | } 85 | 86 | co.update(&packet{msg: *newPropose("foo")}, -1) 87 | 88 | got, tick := co.update(newRsvpFrom(1, 1, 0, "")) 89 | assert.Equal(t, (*msg)(nil), got) 90 | assert.Equal(t, false, tick) 91 | 92 | got, tick = co.update(newRsvpFrom(2, 1, 0, "")) 93 | assert.Equal(t, (*msg)(nil), got) 94 | assert.Equal(t, false, tick) 95 | 96 | got, tick = co.update(newRsvpFrom(3, 1, 0, "")) 97 | assert.Equal(t, (*msg)(nil), got) 98 | assert.Equal(t, false, tick) 99 | 100 | got, tick = co.update(newRsvpFrom(4, 1, 0, "")) 101 | assert.Equal(t, (*msg)(nil), got) 102 | assert.Equal(t, false, tick) 103 | 104 | got, tick = co.update(newRsvpFrom(5, 1, 0, "")) // from 5 105 | assert.Equal(t, (*msg)(nil), got) 106 | assert.Equal(t, false, tick) 107 | 108 | got, tick = co.update(newRsvpFrom(5, 1, 0, "")) // from 5 109 | assert.Equal(t, (*msg)(nil), got) 110 | assert.Equal(t, false, tick) 111 | } 112 | 113 | func TestCoordTargetNomination(t *testing.T) { 114 | co := coordinator{crnd: 1, quor: 6, size: 10} 115 | 116 | co.update(&packet{msg: *newPropose("foo")}, -1) 117 | 118 | co.update(newRsvpFrom(1, 1, 0, "")) 119 | co.update(newRsvpFrom(2, 1, 0, "")) 120 | co.update(newRsvpFrom(3, 1, 0, "")) 121 | co.update(newRsvpFrom(4, 1, 0, "")) 122 | co.update(newRsvpFrom(5, 1, 0, "")) 123 | 124 | got, tick := co.update(newRsvpFrom(6, 1, 0, "")) 125 | assert.Equal(t, newNominate(1, "foo"), got) 126 | assert.Equal(t, false, tick) 127 | } 128 | 129 | func TestCoordRetry(t *testing.T) { 130 | co := coordinator{ 131 | size: 10, 132 | crnd: 1, 133 | } 134 | 135 | co.update(&packet{msg: *newPropose("foo")}, -1) 136 | 137 | // message from a future round and another proposer 138 | got, tick := co.update(newRsvpFrom(1, 2, 0, "")) 139 | assert.Equal(t, (*msg)(nil), got) 140 | assert.Equal(t, false, tick) 141 | 142 | // second message from a future round and another proposer 143 | got, tick = co.update(newRsvpFrom(1, 2, 0, "")) 144 | assert.Equal(t, (*msg)(nil), got) 145 | assert.Equal(t, false, tick) 146 | 147 | got, tick = co.update(&packet{msg: *msgTick}, -1) // force the start of a new round 148 | assert.Equal(t, newInvite(11), got) 149 | assert.Equal(t, true, tick) 150 | } 151 | 152 | func TestCoordNonTargetNomination(t *testing.T) { 153 | co := coordinator{ 154 | quor: 6, 155 | size: 10, 156 | crnd: 1, 157 | } 158 | 159 | co.update(&packet{msg: *newPropose("foo")}, -1) 160 | 161 | co.update(newRsvpFrom(0, 1, 0, "")) 162 | co.update(newRsvpFrom(1, 1, 0, "")) 163 | co.update(newRsvpFrom(2, 1, 0, "")) 164 | co.update(newRsvpFrom(3, 1, 0, "")) 165 | co.update(newRsvpFrom(4, 1, 0, "")) 166 | got, tick := co.update(newRsvpFrom(5, 1, 1, "bar")) 167 | assert.Equal(t, newNominate(1, "bar"), got) 168 | assert.Equal(t, false, tick) 169 | } 170 | 171 | func TestCoordOneNominationPerRound(t *testing.T) { 172 | co := coordinator{ 173 | quor: 6, 174 | crnd: 1, 175 | size: 10, 176 | } 177 | 178 | co.update(&packet{msg: *newPropose("foo")}, -1) 179 | 180 | got, tick := co.update(newRsvpFrom(0, 1, 0, "")) 181 | assert.Equal(t, (*msg)(nil), got) 182 | assert.Equal(t, false, tick) 183 | 184 | got, tick = co.update(newRsvpFrom(1, 1, 0, "")) 185 | assert.Equal(t, (*msg)(nil), got) 186 | assert.Equal(t, false, tick) 187 | 188 | got, tick = co.update(newRsvpFrom(2, 1, 0, "")) 189 | assert.Equal(t, (*msg)(nil), got) 190 | assert.Equal(t, false, tick) 191 | 192 | got, tick = co.update(newRsvpFrom(3, 1, 0, "")) 193 | assert.Equal(t, (*msg)(nil), got) 194 | assert.Equal(t, false, tick) 195 | 196 | got, tick = co.update(newRsvpFrom(4, 1, 0, "")) 197 | assert.Equal(t, (*msg)(nil), got) 198 | assert.Equal(t, false, tick) 199 | 200 | got, tick = co.update(newRsvpFrom(5, 1, 0, "")) 201 | assert.Equal(t, newNominate(1, "foo"), got) 202 | assert.Equal(t, false, tick) 203 | 204 | got, tick = co.update(newRsvpFrom(6, 1, 0, "")) 205 | assert.Equal(t, (*msg)(nil), got) 206 | assert.Equal(t, false, tick) 207 | } 208 | 209 | func TestCoordEachRoundResetsCval(t *testing.T) { 210 | co := coordinator{ 211 | quor: 6, 212 | size: 10, 213 | crnd: 1, 214 | } 215 | 216 | co.update(&packet{msg: *newPropose("foo")}, -1) 217 | 218 | co.update(newRsvpFrom(0, 1, 0, "")) 219 | co.update(newRsvpFrom(1, 1, 0, "")) 220 | co.update(newRsvpFrom(2, 1, 0, "")) 221 | co.update(newRsvpFrom(3, 1, 0, "")) 222 | co.update(newRsvpFrom(4, 1, 0, "")) 223 | co.update(newRsvpFrom(5, 1, 0, "")) 224 | 225 | co.update(&packet{msg: *msgTick}, -1) // force the start of a new round 226 | 227 | got, tick := co.update(newRsvpFrom(0, 11, 0, "")) 228 | assert.Equal(t, (*msg)(nil), got) 229 | assert.Equal(t, false, tick) 230 | 231 | got, tick = co.update(newRsvpFrom(1, 11, 0, "")) 232 | assert.Equal(t, (*msg)(nil), got) 233 | assert.Equal(t, false, tick) 234 | 235 | got, tick = co.update(newRsvpFrom(2, 11, 0, "")) 236 | assert.Equal(t, (*msg)(nil), got) 237 | assert.Equal(t, false, tick) 238 | 239 | got, tick = co.update(newRsvpFrom(3, 11, 0, "")) 240 | assert.Equal(t, (*msg)(nil), got) 241 | assert.Equal(t, false, tick) 242 | 243 | got, tick = co.update(newRsvpFrom(4, 11, 0, "")) 244 | assert.Equal(t, (*msg)(nil), got) 245 | assert.Equal(t, false, tick) 246 | 247 | got, tick = co.update(newRsvpFrom(5, 11, 0, "")) 248 | assert.Equal(t, newNominate(11, "foo"), got) 249 | assert.Equal(t, false, tick) 250 | } 251 | 252 | func TestCoordStartRsvp(t *testing.T) { 253 | co := coordinator{ 254 | quor: 1, 255 | crnd: 1, 256 | } 257 | 258 | got, tick := co.update(newRsvpFrom(0, 1, 0, "")) 259 | assert.Equal(t, (*msg)(nil), got) 260 | assert.Equal(t, false, tick) 261 | 262 | got, tick = co.update(&packet{msg: *newPropose("foo")}, -1) 263 | 264 | // If the RSVPs were ignored, this will be an invite. 265 | // Otherwise, it'll be a nominate. 266 | assert.Equal(t, newInvite(1), got) 267 | assert.Equal(t, true, tick) 268 | } 269 | 270 | func TestCoordDuel(t *testing.T) { 271 | co := coordinator{ 272 | quor: 2, 273 | size: 3, 274 | crnd: 1, 275 | } 276 | 277 | co.update(&packet{msg: *newPropose("foo")}, -1) 278 | 279 | got, tick := co.update(newRsvpFrom(1, 1, 0, "")) 280 | assert.Equal(t, (*msg)(nil), got) 281 | assert.Equal(t, false, tick) 282 | 283 | got, tick = co.update(newRsvpFrom(2, 2, 0, "")) 284 | assert.Equal(t, (*msg)(nil), got) 285 | assert.Equal(t, false, tick) 286 | 287 | got, tick = co.update(newRsvpFrom(3, 2, 0, "")) 288 | assert.Equal(t, (*msg)(nil), got) 289 | assert.Equal(t, false, tick) 290 | 291 | got, tick = co.update(newRsvpFrom(4, 2, 0, "")) 292 | assert.Equal(t, (*msg)(nil), got) 293 | assert.Equal(t, false, tick) 294 | 295 | got, tick = co.update(newRsvpFrom(5, 2, 0, "")) 296 | assert.Equal(t, (*msg)(nil), got) 297 | assert.Equal(t, false, tick) 298 | 299 | got, tick = co.update(newRsvpFrom(6, 2, 0, "")) 300 | assert.Equal(t, (*msg)(nil), got) 301 | assert.Equal(t, false, tick) 302 | } 303 | 304 | func TestCoordinatorIgnoresBadMessages(t *testing.T) { 305 | co := coordinator{begun: true} 306 | 307 | // missing Crnd 308 | got, tick := co.update(&packet{msg: msg{Cmd: rsvp, Vrnd: new(int64)}}, -1) 309 | assert.Equal(t, (*msg)(nil), got) 310 | assert.Equal(t, false, tick) 311 | assert.Equal(t, coordinator{begun: true}, co) 312 | 313 | // missing Vrnd 314 | got, tick = co.update(&packet{msg: msg{Cmd: rsvp, Crnd: new(int64)}}, -1) 315 | assert.Equal(t, (*msg)(nil), got) 316 | assert.Equal(t, false, tick) 317 | assert.Equal(t, coordinator{begun: true}, co) 318 | } 319 | -------------------------------------------------------------------------------- /peer/peer_test.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | import ( 4 | "github.com/bmizerany/assert" 5 | "github.com/ha/doozer" 6 | "github.com/ha/doozerd/store" 7 | "os/exec" 8 | 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestDoozerNop(t *testing.T) { 14 | l := mustListen() 15 | defer l.Close() 16 | u := mustListenUDP(l.Addr().String()) 17 | defer u.Close() 18 | 19 | go Main("a", "X", "", "", "", nil, u, l, nil, 1e9, 2e9, 3e9, 101) 20 | 21 | cl := dial(l.Addr().String()) 22 | err := cl.Nop() 23 | assert.Equal(t, nil, err) 24 | } 25 | 26 | func TestDoozerGet(t *testing.T) { 27 | l := mustListen() 28 | defer l.Close() 29 | u := mustListenUDP(l.Addr().String()) 30 | defer u.Close() 31 | 32 | go Main("a", "X", "", "", "", nil, u, l, nil, 1e9, 2e9, 3e9, 101) 33 | 34 | cl := dial(l.Addr().String()) 35 | 36 | _, err := cl.Set("/x", store.Missing, []byte{'a'}) 37 | assert.Equal(t, nil, err) 38 | 39 | ents, rev, err := cl.Get("/x", nil) 40 | assert.Equal(t, nil, err) 41 | assert.NotEqual(t, store.Dir, rev) 42 | assert.Equal(t, []byte{'a'}, ents) 43 | 44 | //cl.Set("/test/a", store.Missing, []byte{'1'}) 45 | //cl.Set("/test/b", store.Missing, []byte{'2'}) 46 | //cl.Set("/test/c", store.Missing, []byte{'3'}) 47 | 48 | //ents, rev, err = cl.Get("/test", 0) 49 | //sort.SortStrings(ents) 50 | //assert.Equal(t, store.Dir, rev) 51 | //assert.Equal(t, nil, err) 52 | //assert.Equal(t, []string{"a", "b", "c"}, ents) 53 | } 54 | 55 | func TestDoozerSet(t *testing.T) { 56 | l := mustListen() 57 | defer l.Close() 58 | u := mustListenUDP(l.Addr().String()) 59 | defer u.Close() 60 | 61 | go Main("a", "X", "", "", "", nil, u, l, nil, 1e9, 2e9, 3e9, 101) 62 | 63 | cl := dial(l.Addr().String()) 64 | 65 | for i := byte(0); i < 10; i++ { 66 | _, err := cl.Set("/x", store.Clobber, []byte{'0' + i}) 67 | assert.Equal(t, nil, err) 68 | } 69 | 70 | _, err := cl.Set("/x", 0, []byte{'X'}) 71 | assert.Equal(t, &doozer.Error{doozer.ErrOldRev, ""}, err) 72 | } 73 | 74 | func TestDoozerGetWithRev(t *testing.T) { 75 | l := mustListen() 76 | defer l.Close() 77 | u := mustListenUDP(l.Addr().String()) 78 | defer u.Close() 79 | 80 | go Main("a", "X", "", "", "", nil, u, l, nil, 1e9, 2e9, 3e9, 101) 81 | 82 | cl := dial(l.Addr().String()) 83 | 84 | rev1, err := cl.Set("/x", store.Missing, []byte{'a'}) 85 | assert.Equal(t, nil, err) 86 | 87 | v, rev, err := cl.Get("/x", &rev1) // Use the snapshot. 88 | assert.Equal(t, nil, err) 89 | assert.Equal(t, rev1, rev) 90 | assert.Equal(t, []byte{'a'}, v) 91 | 92 | rev2, err := cl.Set("/x", rev, []byte{'b'}) 93 | assert.Equal(t, nil, err) 94 | 95 | v, rev, err = cl.Get("/x", nil) // Read the new value. 96 | assert.Equal(t, nil, err) 97 | assert.Equal(t, rev2, rev) 98 | assert.Equal(t, []byte{'b'}, v) 99 | 100 | v, rev, err = cl.Get("/x", &rev1) // Read the saved value again. 101 | assert.Equal(t, nil, err) 102 | assert.Equal(t, rev1, rev) 103 | assert.Equal(t, []byte{'a'}, v) 104 | } 105 | 106 | func TestDoozerWaitSimple(t *testing.T) { 107 | l := mustListen() 108 | defer l.Close() 109 | u := mustListenUDP(l.Addr().String()) 110 | defer u.Close() 111 | 112 | go Main("a", "X", "", "", "", nil, u, l, nil, 1e9, 2e9, 3e9, 101) 113 | 114 | cl := dial(l.Addr().String()) 115 | var rev int64 = 1 116 | 117 | cl.Set("/test/foo", store.Clobber, []byte("bar")) 118 | ev, err := cl.Wait("/test/**", rev) 119 | assert.Equal(t, nil, err) 120 | assert.Equal(t, "/test/foo", ev.Path) 121 | assert.Equal(t, []byte("bar"), ev.Body) 122 | assert.T(t, ev.IsSet()) 123 | rev = ev.Rev + 1 124 | 125 | cl.Set("/test/fun", store.Clobber, []byte("house")) 126 | ev, err = cl.Wait("/test/**", rev) 127 | assert.Equal(t, nil, err) 128 | assert.Equal(t, "/test/fun", ev.Path) 129 | assert.Equal(t, []byte("house"), ev.Body) 130 | assert.T(t, ev.IsSet()) 131 | rev = ev.Rev + 1 132 | 133 | cl.Del("/test/foo", store.Clobber) 134 | ev, err = cl.Wait("/test/**", rev) 135 | assert.Equal(t, nil, err) 136 | assert.Equal(t, "/test/foo", ev.Path) 137 | assert.T(t, ev.IsDel()) 138 | } 139 | 140 | func TestDoozerWaitWithRev(t *testing.T) { 141 | l := mustListen() 142 | defer l.Close() 143 | u := mustListenUDP(l.Addr().String()) 144 | defer u.Close() 145 | 146 | go Main("a", "X", "", "", "", nil, u, l, nil, 1e9, 2e9, 3e9, 101) 147 | 148 | cl := dial(l.Addr().String()) 149 | 150 | // Create some history 151 | cl.Set("/test/foo", store.Clobber, []byte("bar")) 152 | cl.Set("/test/fun", store.Clobber, []byte("house")) 153 | 154 | ev, err := cl.Wait("/test/**", 1) 155 | assert.Equal(t, nil, err) 156 | assert.Equal(t, "/test/foo", ev.Path) 157 | assert.Equal(t, []byte("bar"), ev.Body) 158 | assert.T(t, ev.IsSet()) 159 | rev := ev.Rev + 1 160 | 161 | ev, err = cl.Wait("/test/**", rev) 162 | assert.Equal(t, nil, err) 163 | assert.Equal(t, "/test/fun", ev.Path) 164 | assert.Equal(t, []byte("house"), ev.Body) 165 | assert.T(t, ev.IsSet()) 166 | } 167 | 168 | func TestDoozerStat(t *testing.T) { 169 | l := mustListen() 170 | defer l.Close() 171 | u := mustListenUDP(l.Addr().String()) 172 | defer u.Close() 173 | 174 | go Main("a", "X", "", "", "", nil, u, l, nil, 1e9, 2e9, 3e9, 101) 175 | 176 | cl := dial(l.Addr().String()) 177 | 178 | cl.Set("/test/foo", store.Clobber, []byte("bar")) 179 | setRev, _ := cl.Set("/test/fun", store.Clobber, []byte("house")) 180 | 181 | ln, rev, err := cl.Stat("/test", nil) 182 | assert.Equal(t, nil, err) 183 | assert.Equal(t, store.Dir, rev) 184 | assert.Equal(t, int(2), ln) 185 | 186 | ln, rev, err = cl.Stat("/test/fun", nil) 187 | assert.Equal(t, nil, err) 188 | assert.Equal(t, setRev, rev) 189 | assert.Equal(t, int(5), ln) 190 | } 191 | 192 | func TestDoozerGetdirOnDir(t *testing.T) { 193 | l := mustListen() 194 | defer l.Close() 195 | u := mustListenUDP(l.Addr().String()) 196 | defer u.Close() 197 | 198 | go Main("a", "X", "", "", "", nil, u, l, nil, 1e9, 2e9, 3e9, 101) 199 | 200 | cl := dial(l.Addr().String()) 201 | 202 | cl.Set("/test/a", store.Clobber, []byte("1")) 203 | cl.Set("/test/b", store.Clobber, []byte("2")) 204 | cl.Set("/test/c", store.Clobber, []byte("3")) 205 | 206 | rev, err := cl.Rev() 207 | if err != nil { 208 | panic(err) 209 | } 210 | 211 | got, err := cl.Getdir("/test", rev, 0, -1) 212 | assert.Equal(t, nil, err) 213 | assert.Equal(t, []string{"a", "b", "c"}, got) 214 | } 215 | 216 | func TestDoozerGetdirOnFile(t *testing.T) { 217 | l := mustListen() 218 | defer l.Close() 219 | u := mustListenUDP(l.Addr().String()) 220 | defer u.Close() 221 | 222 | go Main("a", "X", "", "", "", nil, u, l, nil, 1e9, 2e9, 3e9, 101) 223 | 224 | cl := dial(l.Addr().String()) 225 | 226 | cl.Set("/test/a", store.Clobber, []byte("1")) 227 | 228 | rev, err := cl.Rev() 229 | if err != nil { 230 | panic(err) 231 | } 232 | 233 | names, err := cl.Getdir("/test/a", rev, 0, -1) 234 | assert.Equal(t, &doozer.Error{doozer.ErrNotDir, ""}, err) 235 | assert.Equal(t, []string(nil), names) 236 | } 237 | 238 | func TestDoozerGetdirMissing(t *testing.T) { 239 | l := mustListen() 240 | defer l.Close() 241 | u := mustListenUDP(l.Addr().String()) 242 | defer u.Close() 243 | 244 | go Main("a", "X", "", "", "", nil, u, l, nil, 1e9, 2e9, 3e9, 101) 245 | 246 | cl := dial(l.Addr().String()) 247 | 248 | rev, err := cl.Rev() 249 | if err != nil { 250 | panic(err) 251 | } 252 | 253 | names, err := cl.Getdir("/not/here", rev, 0, -1) 254 | assert.Equal(t, &doozer.Error{doozer.ErrNoEnt, ""}, err) 255 | assert.Equal(t, []string(nil), names) 256 | } 257 | 258 | func TestDoozerGetdirOffsetLimit(t *testing.T) { 259 | l := mustListen() 260 | defer l.Close() 261 | u := mustListenUDP(l.Addr().String()) 262 | defer u.Close() 263 | 264 | go Main("a", "X", "", "", "", nil, u, l, nil, 1e9, 2e9, 3e9, 101) 265 | 266 | cl := dial(l.Addr().String()) 267 | cl.Set("/test/a", store.Clobber, []byte("1")) 268 | cl.Set("/test/b", store.Clobber, []byte("2")) 269 | cl.Set("/test/c", store.Clobber, []byte("3")) 270 | cl.Set("/test/d", store.Clobber, []byte("4")) 271 | 272 | rev, err := cl.Rev() 273 | if err != nil { 274 | panic(err) 275 | } 276 | 277 | names, err := cl.Getdir("/test", rev, 1, 2) 278 | assert.Equal(t, nil, err) 279 | assert.Equal(t, []string{"b", "c"}, names) 280 | } 281 | 282 | func TestPeerShun(t *testing.T) { 283 | l0 := mustListen() 284 | defer l0.Close() 285 | a0 := l0.Addr().String() 286 | u0 := mustListenUDP(a0) 287 | defer u0.Close() 288 | 289 | l1 := mustListen() 290 | defer l1.Close() 291 | u1 := mustListenUDP(l1.Addr().String()) 292 | defer u1.Close() 293 | l2 := mustListen() 294 | defer l2.Close() 295 | u2 := mustListenUDP(l2.Addr().String()) 296 | defer u2.Close() 297 | 298 | go Main("a", "X", "", "", "", nil, u0, l0, nil, 1e8, 1e7, 1e9, 1e9) 299 | go Main("a", "Y", "", "", "", dial(a0), u1, l1, nil, 1e8, 1e7, 1e9, 1e9) 300 | go Main("a", "Z", "", "", "", dial(a0), u2, l2, nil, 1e8, 1e7, 1e9, 1e9) 301 | 302 | cl := dial(l0.Addr().String()) 303 | cl.Set("/ctl/cal/1", store.Missing, nil) 304 | cl.Set("/ctl/cal/2", store.Missing, nil) 305 | 306 | waitFor(cl, "/ctl/node/X/writable") 307 | waitFor(cl, "/ctl/node/Y/writable") 308 | waitFor(cl, "/ctl/node/Z/writable") 309 | 310 | rev, err := cl.Set("/test", store.Clobber, nil) 311 | if e, ok := err.(*doozer.Error); ok && e.Err == doozer.ErrReadonly { 312 | } else if err != nil { 313 | panic(err) 314 | } 315 | 316 | u1.Close() 317 | for { 318 | ev, err := cl.Wait("/ctl/cal/*", rev) 319 | if err != nil { 320 | panic(err) 321 | } 322 | if ev.IsSet() && len(ev.Body) == 0 { 323 | break 324 | } 325 | rev = ev.Rev + 1 326 | } 327 | } 328 | 329 | func TestPeerLateJoin(t *testing.T) { 330 | l0 := mustListen() 331 | defer l0.Close() 332 | a0 := l0.Addr().String() 333 | u0 := mustListenUDP(a0) 334 | defer u0.Close() 335 | 336 | l1 := mustListen() 337 | defer l1.Close() 338 | u1 := mustListenUDP(l1.Addr().String()) 339 | defer u1.Close() 340 | 341 | go Main("a", "X", "", "", "", nil, u0, l0, nil, 1e8, 1e7, 1e9, 60) 342 | 343 | cl := dial(l0.Addr().String()) 344 | waitFor(cl, "/ctl/node/X/writable") 345 | 346 | // TODO: this is set slightly higher than the hardcoded interval 347 | // at which a store is cleaned. Refactor that to be configurable 348 | // so we can drop this down to something reasonable 349 | time.Sleep(1100 * time.Millisecond) 350 | 351 | go Main("a", "Y", "", "", "", dial(a0), u1, l1, nil, 1e8, 1e7, 1e9, 60) 352 | rev, _ := cl.Set("/ctl/cal/1", store.Missing, nil) 353 | for { 354 | ev, err := cl.Wait("/ctl/node/Y/writable", rev) 355 | if err != nil { 356 | panic(err) 357 | } 358 | if ev.IsSet() && len(ev.Body) == 4 { 359 | break 360 | } 361 | rev = ev.Rev + 1 362 | } 363 | } 364 | 365 | func assertDenied(t *testing.T, err error) { 366 | assert.NotEqual(t, nil, err) 367 | assert.Equal(t, doozer.ErrOther, err.(*doozer.Error).Err) 368 | assert.Equal(t, "permission denied", err.(*doozer.Error).Detail) 369 | } 370 | 371 | func runDoozer(a ...string) *exec.Cmd { 372 | path := "/home/kr/src/go/bin/doozerd" 373 | args := append([]string{path}, a...) 374 | c := exec.Command(path, args...) 375 | if err := c.Run(); err != nil { 376 | panic(err) 377 | } 378 | return c 379 | } 380 | -------------------------------------------------------------------------------- /consensus/manager_test.go: -------------------------------------------------------------------------------- 1 | package consensus 2 | 3 | import ( 4 | "code.google.com/p/goprotobuf/proto" 5 | "container/heap" 6 | "github.com/bmizerany/assert" 7 | "github.com/ha/doozerd/store" 8 | "net" 9 | "sort" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | // The first element in a protobuf stream is always a varint. 15 | // The high bit of a varint byte indicates continuation; 16 | // This is a continuation bit without a subsequent byte. 17 | // http://code.google.com/apis/protocolbuffers/docs/encoding.html#varints. 18 | var invalidProtobuf = []byte{0x80} 19 | 20 | func mustMarshal(p proto.Message) []byte { 21 | buf, err := proto.Marshal(p) 22 | if err != nil { 23 | panic(err) 24 | } 25 | return buf 26 | } 27 | 28 | func mustWait(s *store.Store, n int64) <-chan store.Event { 29 | c, err := s.Wait(store.Any, n) 30 | if err != nil { 31 | panic(err) 32 | } 33 | return c 34 | } 35 | 36 | func TestManagerPumpDropsOldPackets(t *testing.T) { 37 | st := store.New() 38 | defer close(st.Ops) 39 | x, _ := net.ResolveUDPAddr("udp", "1.2.3.4:5") 40 | st.Ops <- store.Op{1, store.MustEncodeSet(node+"/a/addr", "1.2.3.4:5", 0)} 41 | st.Ops <- store.Op{2, store.MustEncodeSet("/ctl/cal/0", "a", 0)} 42 | 43 | var m Manager 44 | m.run = make(map[int64]*run) 45 | m.event(<-mustWait(st, 2)) 46 | m.pump() 47 | recvPacket(&m.packet, Packet{x, mustMarshal(&msg{Seqn: proto.Int64(1)})}) 48 | m.pump() 49 | assert.Equal(t, 0, m.Stats.WaitPackets) 50 | } 51 | 52 | func TestRecvPacket(t *testing.T) { 53 | q := new(packets) 54 | x, _ := net.ResolveUDPAddr("udp", "1.2.3.4:5") 55 | 56 | p := recvPacket(q, Packet{x, mustMarshal(&msg{ 57 | Seqn: proto.Int64(1), 58 | Cmd: invite, 59 | })}) 60 | assert.Equal(t, &packet{x, msg{Seqn: proto.Int64(1), Cmd: invite}}, p) 61 | p = recvPacket(q, Packet{x, mustMarshal(&msg{ 62 | Seqn: proto.Int64(2), 63 | Cmd: invite, 64 | })}) 65 | assert.Equal(t, &packet{x, msg{Seqn: proto.Int64(2), Cmd: invite}}, p) 66 | p = recvPacket(q, Packet{x, mustMarshal(&msg{ 67 | Seqn: proto.Int64(3), 68 | Cmd: invite, 69 | })}) 70 | assert.Equal(t, &packet{x, msg{Seqn: proto.Int64(3), Cmd: invite}}, p) 71 | assert.Equal(t, 3, q.Len()) 72 | } 73 | 74 | func TestRecvEmptyPacket(t *testing.T) { 75 | q := new(packets) 76 | x, _ := net.ResolveUDPAddr("udp", "1.2.3.4:5") 77 | 78 | p := recvPacket(q, Packet{x, []byte{}}) 79 | assert.Equal(t, (*packet)(nil), p) 80 | assert.Equal(t, 0, q.Len()) 81 | } 82 | 83 | func TestRecvInvalidPacket(t *testing.T) { 84 | q := new(packets) 85 | x, _ := net.ResolveUDPAddr("udp", "1.2.3.4:5") 86 | p := recvPacket(q, Packet{x, invalidProtobuf}) 87 | assert.Equal(t, (*packet)(nil), p) 88 | assert.Equal(t, 0, q.Len()) 89 | } 90 | 91 | func TestSchedTrigger(t *testing.T) { 92 | var q triggers 93 | d := int64(15e8) 94 | 95 | t0 := time.Now().UnixNano() 96 | ts := t0 + d 97 | schedTrigger(&q, 1, t0, d) 98 | 99 | assert.Equal(t, 1, q.Len()) 100 | f := q[0] 101 | assert.Equal(t, int64(1), f.n) 102 | assert.T(t, f.t == ts) 103 | } 104 | 105 | func TestManagerPacketProcessing(t *testing.T) { 106 | st := store.New() 107 | defer close(st.Ops) 108 | in := make(chan Packet) 109 | out := make(chan Packet, 100) 110 | var m Manager 111 | m.run = make(map[int64]*run) 112 | m.Alpha = 1 113 | m.Store = st 114 | m.In = in 115 | m.Out = out 116 | m.Ops = st.Ops 117 | 118 | st.Ops <- store.Op{1, store.MustEncodeSet(node+"/a/addr", "1.2.3.4:5", 0)} 119 | st.Ops <- store.Op{2, store.MustEncodeSet("/ctl/cal/0", "a", 0)} 120 | m.event(<-mustWait(st, 2)) 121 | 122 | addr, _ := net.ResolveUDPAddr("udp", "127.0.0.1:9999") 123 | recvPacket(&m.packet, Packet{ 124 | Data: mustMarshal(&msg{Seqn: proto.Int64(2), Cmd: learn, Value: []byte("foo")}), 125 | Addr: addr, 126 | }) 127 | m.pump() 128 | assert.Equal(t, 0, m.packet.Len()) 129 | } 130 | 131 | func TestManagerTickQueue(t *testing.T) { 132 | st := store.New() 133 | defer close(st.Ops) 134 | st.Ops <- store.Op{1, store.MustEncodeSet(node+"/a/addr", "1.2.3.4:5", 0)} 135 | st.Ops <- store.Op{2, store.MustEncodeSet("/ctl/cal/0", "a", 0)} 136 | 137 | var m Manager 138 | m.run = make(map[int64]*run) 139 | m.Alpha = 1 140 | m.Store = st 141 | m.Out = make(chan Packet, 100) 142 | m.event(<-mustWait(st, 2)) 143 | 144 | // get it to tick for seqn 3 145 | recvPacket(&m.packet, Packet{Data: mustMarshal(&msg{Seqn: proto.Int64(3), Cmd: propose})}) 146 | m.pump() 147 | assert.Equal(t, 1, m.tick.Len()) 148 | 149 | m.doTick(time.Now().UnixNano() + initialWaitBound*2) 150 | assert.Equal(t, int64(1), m.Stats.TotalTicks) 151 | } 152 | 153 | func TestManagerFilterPropSeqn(t *testing.T) { 154 | ps := make(chan int64, 100) 155 | st := store.New() 156 | defer close(st.Ops) 157 | 158 | m := &Manager{ 159 | DefRev: 2, 160 | Alpha: 1, 161 | Self: "b", 162 | PSeqn: ps, 163 | Store: st, 164 | } 165 | go m.Run() 166 | 167 | st.Ops <- store.Op{1, store.MustEncodeSet("/ctl/cal/0", "a", 0)} 168 | st.Ops <- store.Op{2, store.MustEncodeSet("/ctl/cal/1", "b", 0)} 169 | st.Ops <- store.Op{3, store.Nop} 170 | st.Ops <- store.Op{4, store.Nop} 171 | assert.Equal(t, int64(3), <-ps) 172 | assert.Equal(t, int64(5), <-ps) 173 | 174 | st.Ops <- store.Op{5, store.Nop} 175 | st.Ops <- store.Op{6, store.Nop} 176 | assert.Equal(t, int64(7), <-ps) 177 | } 178 | 179 | func TestManagerProposalQueue(t *testing.T) { 180 | var m Manager 181 | m.run = make(map[int64]*run) 182 | m.propose(&m.packet, &Prop{Seqn: 1, Mut: []byte("foo")}, time.Now().UnixNano()) 183 | assert.Equal(t, 1, m.packet.Len()) 184 | } 185 | 186 | func TestManagerProposeFill(t *testing.T) { 187 | q := new(packets) 188 | var m Manager 189 | m.Self = "a" 190 | m.run = map[int64]*run{ 191 | 6: &run{seqn: 6, cals: []string{"a", "b", "c"}}, 192 | 7: &run{seqn: 7, cals: []string{"a", "b", "c"}}, 193 | 8: &run{seqn: 8, cals: []string{"a", "b", "c"}}, 194 | } 195 | exp := triggers{ 196 | {123, 7}, 197 | {123, 8}, 198 | } 199 | m.propose(q, &Prop{Seqn: 9, Mut: []byte("foo")}, 123) 200 | assert.Equal(t, exp, m.fill) 201 | } 202 | 203 | func TestApplyTriggers(t *testing.T) { 204 | pkts := new(packets) 205 | tgrs := new(triggers) 206 | 207 | heap.Push(tgrs, trigger{t: 1, n: 1}) 208 | heap.Push(tgrs, trigger{t: 2, n: 2}) 209 | heap.Push(tgrs, trigger{t: 3, n: 3}) 210 | heap.Push(tgrs, trigger{t: 4, n: 4}) 211 | heap.Push(tgrs, trigger{t: 5, n: 5}) 212 | heap.Push(tgrs, trigger{t: 6, n: 6}) 213 | heap.Push(tgrs, trigger{t: 7, n: 7}) 214 | heap.Push(tgrs, trigger{t: 8, n: 8}) 215 | heap.Push(tgrs, trigger{t: 9, n: 9}) 216 | 217 | n := applyTriggers(pkts, tgrs, 5, &msg{Cmd: tick}) 218 | assert.Equal(t, 5, n) 219 | 220 | expTriggers := new(triggers) 221 | expPackets := new(packets) 222 | heap.Push(expPackets, &packet{msg: msg{Cmd: tick, Seqn: proto.Int64(1)}}) 223 | heap.Push(expPackets, &packet{msg: msg{Cmd: tick, Seqn: proto.Int64(2)}}) 224 | heap.Push(expPackets, &packet{msg: msg{Cmd: tick, Seqn: proto.Int64(3)}}) 225 | heap.Push(expPackets, &packet{msg: msg{Cmd: tick, Seqn: proto.Int64(4)}}) 226 | heap.Push(expPackets, &packet{msg: msg{Cmd: tick, Seqn: proto.Int64(5)}}) 227 | heap.Push(expTriggers, trigger{t: 6, n: 6}) 228 | heap.Push(expTriggers, trigger{t: 7, n: 7}) 229 | heap.Push(expTriggers, trigger{t: 8, n: 8}) 230 | heap.Push(expTriggers, trigger{t: 9, n: 9}) 231 | 232 | sort.Sort(pkts) 233 | sort.Sort(tgrs) 234 | sort.Sort(expPackets) 235 | sort.Sort(expTriggers) 236 | 237 | assert.Equal(t, expTriggers, tgrs) 238 | assert.Equal(t, expPackets, pkts) 239 | } 240 | 241 | func TestManagerEvent(t *testing.T) { 242 | const alpha = 2 243 | runs := make(map[int64]*run) 244 | st := store.New() 245 | defer close(st.Ops) 246 | 247 | st.Ops <- store.Op{ 248 | Seqn: 1, 249 | Mut: store.MustEncodeSet(node+"/a/addr", "1.2.3.4:5", 0), 250 | } 251 | 252 | st.Ops <- store.Op{ 253 | Seqn: 2, 254 | Mut: store.MustEncodeSet(cal+"/1", "a", 0), 255 | } 256 | 257 | ch, err := st.Wait(store.Any, 2) 258 | if err != nil { 259 | panic(err) 260 | } 261 | 262 | x, _ := net.ResolveUDPAddr("udp", "1.2.3.4:5") 263 | pseqn := make(chan int64, 1) 264 | m := &Manager{ 265 | Alpha: alpha, 266 | Self: "a", 267 | PSeqn: pseqn, 268 | Ops: st.Ops, 269 | Out: make(chan Packet), 270 | run: runs, 271 | } 272 | m.event(<-ch) 273 | 274 | exp := &run{ 275 | self: "a", 276 | seqn: 2 + alpha, 277 | cals: []string{"a"}, 278 | addr: []*net.UDPAddr{x}, 279 | ops: st.Ops, 280 | out: m.Out, 281 | bound: initialWaitBound, 282 | } 283 | exp.c = coordinator{ 284 | crnd: 1, 285 | size: 1, 286 | quor: exp.quorum(), 287 | } 288 | exp.l = learner{ 289 | round: 1, 290 | size: 1, 291 | quorum: int64(exp.quorum()), 292 | votes: map[string]int64{}, 293 | voted: []bool{false}, 294 | } 295 | 296 | assert.Equal(t, 1, len(runs)) 297 | assert.Equal(t, exp, runs[exp.seqn]) 298 | assert.Equal(t, exp.seqn, <-pseqn) 299 | assert.Equal(t, exp.seqn+1, m.next) 300 | } 301 | 302 | func TestManagerRemoveLastCal(t *testing.T) { 303 | const alpha = 2 304 | runs := make(map[int64]*run) 305 | st := store.New() 306 | defer close(st.Ops) 307 | 308 | st.Ops <- store.Op{1, store.MustEncodeSet(node+"/a/addr", "1.2.3.4:5", 0)} 309 | st.Ops <- store.Op{2, store.MustEncodeSet(cal+"/1", "a", 0)} 310 | st.Ops <- store.Op{3, store.MustEncodeSet(cal+"/1", "", -1)} 311 | 312 | x, _ := net.ResolveUDPAddr("udp", "1.2.3.4:5") 313 | pseqn := make(chan int64, 100) 314 | m := &Manager{ 315 | Alpha: alpha, 316 | Self: "a", 317 | PSeqn: pseqn, 318 | Ops: st.Ops, 319 | Out: make(chan Packet), 320 | run: runs, 321 | } 322 | m.event(<-mustWait(st, 2)) 323 | m.event(<-mustWait(st, 3)) 324 | 325 | exp := &run{ 326 | self: "a", 327 | seqn: 3 + alpha, 328 | cals: []string{"a"}, 329 | addr: []*net.UDPAddr{x}, 330 | ops: st.Ops, 331 | out: m.Out, 332 | bound: initialWaitBound, 333 | } 334 | exp.c = coordinator{ 335 | crnd: 1, 336 | size: 1, 337 | quor: exp.quorum(), 338 | } 339 | exp.l = learner{ 340 | round: 1, 341 | size: 1, 342 | quorum: int64(exp.quorum()), 343 | votes: map[string]int64{}, 344 | voted: []bool{false}, 345 | } 346 | 347 | assert.Equal(t, 2, len(runs)) 348 | assert.Equal(t, exp, runs[exp.seqn]) 349 | assert.Equal(t, exp.seqn+1, m.next) 350 | } 351 | 352 | func TestDelRun(t *testing.T) { 353 | const alpha = 2 354 | runs := make(map[int64]*run) 355 | st := store.New() 356 | defer close(st.Ops) 357 | 358 | st.Ops <- store.Op{1, store.MustEncodeSet(node+"/a/addr", "x", 0)} 359 | st.Ops <- store.Op{2, store.MustEncodeSet(cal+"/1", "a", 0)} 360 | st.Ops <- store.Op{3, store.Nop} 361 | st.Ops <- store.Op{4, store.Nop} 362 | 363 | c2, err := st.Wait(store.Any, 2) 364 | if err != nil { 365 | panic(err) 366 | } 367 | 368 | c3, err := st.Wait(store.Any, 3) 369 | if err != nil { 370 | panic(err) 371 | } 372 | 373 | c4, err := st.Wait(store.Any, 4) 374 | if err != nil { 375 | panic(err) 376 | } 377 | 378 | pseqn := make(chan int64, 100) 379 | m := &Manager{ 380 | Alpha: alpha, 381 | Self: "a", 382 | PSeqn: pseqn, 383 | Ops: st.Ops, 384 | Out: make(chan Packet), 385 | run: runs, 386 | } 387 | m.event(<-c2) 388 | assert.Equal(t, 1, len(m.run)) 389 | m.event(<-c3) 390 | assert.Equal(t, 2, len(m.run)) 391 | m.event(<-c4) 392 | assert.Equal(t, 2, len(m.run)) 393 | } 394 | 395 | func TestGetCalsFull(t *testing.T) { 396 | st := store.New() 397 | defer close(st.Ops) 398 | 399 | st.Ops <- store.Op{Seqn: 1, Mut: store.MustEncodeSet(cal+"/1", "a", 0)} 400 | st.Ops <- store.Op{Seqn: 2, Mut: store.MustEncodeSet(cal+"/2", "c", 0)} 401 | st.Ops <- store.Op{Seqn: 3, Mut: store.MustEncodeSet(cal+"/3", "b", 0)} 402 | <-st.Seqns 403 | 404 | assert.Equal(t, []string{"a", "b", "c"}, getCals(st)) 405 | } 406 | 407 | func TestGetCalsPartial(t *testing.T) { 408 | st := store.New() 409 | defer close(st.Ops) 410 | 411 | st.Ops <- store.Op{Seqn: 1, Mut: store.MustEncodeSet(cal+"/1", "a", 0)} 412 | st.Ops <- store.Op{Seqn: 2, Mut: store.MustEncodeSet(cal+"/2", "", 0)} 413 | st.Ops <- store.Op{Seqn: 3, Mut: store.MustEncodeSet(cal+"/3", "", 0)} 414 | <-st.Seqns 415 | 416 | assert.Equal(t, []string{"a"}, getCals(st)) 417 | } 418 | 419 | func TestGetAddrs(t *testing.T) { 420 | st := store.New() 421 | defer close(st.Ops) 422 | 423 | st.Ops <- store.Op{1, store.MustEncodeSet(node+"/1/addr", "1.2.3.4:5", 0)} 424 | st.Ops <- store.Op{2, store.MustEncodeSet(node+"/2/addr", "2.3.4.5:6", 0)} 425 | st.Ops <- store.Op{3, store.MustEncodeSet(node+"/3/addr", "3.4.5.6:7", 0)} 426 | <-st.Seqns 427 | 428 | x, _ := net.ResolveUDPAddr("udp", "1.2.3.4:5") 429 | y, _ := net.ResolveUDPAddr("udp", "2.3.4.5:6") 430 | z, _ := net.ResolveUDPAddr("udp", "3.4.5.6:7") 431 | addrs := getAddrs(st, []string{"1", "2", "3"}) 432 | assert.Equal(t, []*net.UDPAddr{x, y, z}, addrs) 433 | } 434 | --------------------------------------------------------------------------------