├── clustering-in-go.slide ├── crdt ├── handlers.go └── main.go ├── final ├── handlers.go └── main.go ├── g-counter-1.png ├── g-counter-2.png ├── g-counter-3.png ├── memberlist ├── handlers.go └── main.go └── simple_counter └── main.go /clustering-in-go.slide: -------------------------------------------------------------------------------- 1 | Clustering in Go 2 | May 2016 3 | Tags: go golang 4 | 5 | Wilfried Schobeiri 6 | MediaMath 7 | @nphase 8 | 9 | * Who am I? 10 | - Go enthusiast 11 | - These days, mostly codes for fun 12 | - Focused on Infrastruture & Platform @ [[http://careers.mediamath.com][MediaMath]] 13 | - We're hiring! 14 | 15 | * Why Go? 16 | 17 | - Easy to build services 18 | - Great stdlib 19 | - Lot's of community libraries & utilities 20 | - Great built-in tooling (like go fmt, test, vet, -race, etc) 21 | - Compiles as fast as a scripting language 22 | - Just "feels" productive 23 | - (This is not a language pitch talk) 24 | 25 | * Why "Clustering in Go"? 26 | 27 | * Why "Clustering in Go"? 28 | 29 | - Clustering is not batteries included in Golang 30 | - Lots of newer libraries, none very mature 31 | - More often not, services roll it themselves 32 | 33 | - So, here's one way of building a clustered, stateful service in Go. 34 | 35 | 36 | 37 | * And now, for a (fake-ish) scenario 38 | 39 | - Multiple datacenters 40 | - Separated by thousands of miles each (eg, ORD - HKG - AMS), 41 | - With many events happening concurrently at each one. 42 | 43 | - *We*want*to*count*them.* 44 | 45 | * With some constraints: 46 | 47 | - Counting should be *fast*, we can't afford to cross the ocean every time 48 | - Counts should be *correct* (please don't lose my events) 49 | 50 | Starting to look like an AP system, right? 51 | 52 | 53 | * Let's get started 54 | 55 | First, a basic counter service 56 | 57 | - One node 58 | - Counter = Atomic Int 59 | - Nothin Fancy 60 | 61 | $ curl http://localhost:4000/ 62 | 63 | 0 64 | 65 | $ curl http://localhost:4000/inc?amount=1 66 | 67 | 1 68 | 69 | 70 | * A basic Counter Service 71 | 72 | 73 | .play simple_counter/main.go /START OMIT/,/END OMIT/ 74 | 75 | 76 | * Ok, let's geodistribute it. 77 | 78 | * Ok, let's geodistribute it. 79 | 80 | - A node (or several) in each datacenter 81 | - Route increment requests to the closest node 82 | 83 | Let's stand one up in each. 84 | 85 | * Demo 86 | 87 | * Duh, we're not replicating state! 88 | 89 | - We need the counters to talk to each other 90 | - Which means we need the nodes to know about each other 91 | - Which means we need to to solve for cluster membership 92 | 93 | Enter, the [[https://github.com/hashicorp/memberlist][memberlist]] package 94 | 95 | * Memberlist 96 | 97 | - A Go library that manages cluster membership 98 | - Based on [[https://www.cs.cornell.edu/~asdas/research/dsn02-swim.pdf][SWIM]], a gossip-style membership protocol 99 | - Has baked in member failure detection 100 | - Used by Consul, Docker, libnetwork, many more 101 | 102 | * About SWIM 103 | 104 | "Scalable Weakly-consistent Infection-style Process Group Membership Protocol" 105 | 106 | Two goals: 107 | 108 | - Maintain a local membership list of non-faulty processes 109 | - Detect and eventually notify others of process failures 110 | 111 | * SWIM mechanics 112 | 113 | - Gossip-based 114 | - On join, a new node does a full state sync with an existing member, and begins gossipping its existence to the cluster 115 | - Gossip about memberlist state happens on a regular interval and against number of randomly selected members 116 | - If a node doesn't ack a probe message, it is marked "suspicious" 117 | - If a suspicious node doesn't dispute suspicion after a timeout, it's marked dead 118 | - Every so often, a full state sync is done between random members (expensive!) 119 | - Tradeoffs between bandwidth and convergence time are configurable 120 | 121 | More details about SWIM can be found [[https://www.cs.cornell.edu/~asdas/research/dsn02-swim.pdf][here]] and [[http://prakhar.me/articles/swim/][here]]. 122 | 123 | * Becoming Cluster-aware via Memberlist 124 | 125 | members = flag.String("members", "", "comma seperated list of members") 126 | 127 | ... 128 | 129 | c := memberlist.DefaultWANConfig() 130 | 131 | m, err := memberlist.Create(c) 132 | 133 | if err != nil { 134 | return err 135 | } 136 | 137 | //Join other members if specified, otherwise start a new cluster 138 | if len(*members) > 0 { 139 | members_each := strings.Split(*members, ",") 140 | _, err := m.Join(members_each) 141 | if err != nil { 142 | return err 143 | } 144 | } 145 | 146 | 147 | 148 | 149 | * Demo 150 | 151 | * CRDTs to the rescue! 152 | 153 | * CRDTs, simplified 154 | 155 | #semilattice algebra lets them merbe without conflict 156 | 157 | 158 | - CRDT = Conflict-Free Replicated Data Types 159 | - Counters, Sets, Maps, Flags, et al 160 | - Operations within the type must be associative, commutative, and idempotent 161 | - Order-free 162 | - Therefore, very easy to handle failure scenarios: just retry the merge! 163 | 164 | 165 | CRDTs are by nature eventually consistent, because there is no single source of truth. 166 | 167 | Some notes can be found [[http://hal.upmc.fr/inria-00555588/document][here]] and [[https://github.com/pfraze/crdt_notes][here]] (among many others!). 168 | 169 | * G-Counter 170 | 171 | Perhaps one of the most basic CRDTs: 172 | 173 | - A counter with only two ops: increment and merge (no decrement!) 174 | - Each node manages its own count 175 | - Nodes communicate their counter state with other nodes 176 | - Merges take the max() count for each node 177 | 178 | G-Counter's Value is the sum of all node count values 179 | 180 | 181 | #why go through all the trouble? when i can just query locally? 182 | #what if the node goes down permanently? waht if you lose the state? 183 | 184 | 185 | 186 | * G-counter 187 | 188 | .image g-counter-1.png 700 900 189 | 190 | * G-counter 191 | 192 | .image g-counter-2.png 700 900 193 | 194 | * G-counter 195 | 196 | .image g-counter-3.png 700 900 197 | 198 | 199 | * 200 | 201 | type GCounter struct { 202 | // ident provides a unique identity to each replica. 203 | ident string 204 | 205 | // counter maps identity of each replica to their counter values 206 | counter map[string]int 207 | } 208 | 209 | func (g *GCounter) IncVal(incr int) { 210 | g.counter[g.ident] += incr 211 | } 212 | 213 | func (g *GCounter) Count() (total int) { 214 | for _, val := range g.counter { 215 | total += val 216 | } 217 | return 218 | } 219 | 220 | func (g *GCounter) Merge(c *GCounter) { 221 | for ident, val := range c.counter { 222 | if v, ok := g.counter[ident]; !ok || v < val { 223 | g.counter[ident] = val 224 | } 225 | } 226 | } 227 | 228 | * Demo 229 | 230 | * Let's Merge State! 231 | 232 | * Merging State via Memberlist 233 | 234 | [DEBUG] memberlist: Initiating push/pull sync with: 127.0.0.1:61300 235 | 236 | - Memberlist does a "push/pull" to do a complete state exchange with another random member 237 | - We can piggyback this state exchange via the `Delegate` interface: `LocalState()` and `MergeRemoteState()` 238 | - Push/pull interval is configurable 239 | - Happens over TCP 240 | 241 | Let's use it to eventually merge state in the background. 242 | 243 | 244 | * Merging State via Memberlist 245 | 246 | // Share the local counter state via MemberList to another node 247 | func (d *delegate) LocalState(join bool) []byte { 248 | 249 | b, err := counter.MarshalJSON() 250 | 251 | if err != nil { 252 | panic(err) 253 | } 254 | 255 | return b 256 | } 257 | 258 | // Merge a received counter state 259 | func (d *delegate) MergeRemoteState(buf []byte, join bool) { 260 | if len(buf) == 0 { 261 | return 262 | } 263 | 264 | externalCRDT := crdt.NewGCounterFromJSON(buf) 265 | counter.Merge(externalCRDT) 266 | } 267 | 268 | * Demo 269 | 270 | * Success! (Eventually) 271 | 272 | * Want so sync faster? 273 | 274 | It's possible to broadcast to all member nodes, via Memberlist's `QueueBroadcast()` and `NotifyMsg()`. 275 | 276 | func BroadcastState() { 277 | ... 278 | broadcasts.QueueBroadcast(&broadcast{ 279 | msg: b, 280 | }) 281 | } 282 | 283 | // NotifyMsg is invoked upon receipt of message 284 | func (d *delegate) NotifyMsg(b []byte) { 285 | ... 286 | switch update.Action { 287 | case "merge": 288 | externalCRDT := crdt.NewGCounterFromJSONBytes(update.Data) 289 | counter.Merge(externalCRDT) 290 | } 291 | ... 292 | } 293 | 294 | Faster sync for more bandwidth. Still eventually consistent. 295 | 296 | * Demo 297 | 298 | * Next: 299 | 300 | - No tests? For shame! 301 | - Implement persistence and time windowing 302 | - We probably want more than one node per datacenter 303 | - Jepsen all the things 304 | - Implement a real RPC layer instead of Memberlist's `delegate` for finer performance and authn/z control 305 | - Run it as a unikernel within docker running inside a VM in the cloud 306 | - Sprinkle some devops magic dust on it 307 | - Achieve peak microservice 308 | 309 | * TL;DR: 310 | 311 | It's not (that) hard to build a clustered service in go. 312 | 313 | * Fin! 314 | 315 | - Questions? 316 | - Slides and code can be found at [[github.com/nphase/go-clustering-example][github.com/nphase/go-clustering-example]] 317 | - MediaMath is hiring! 318 | - Thanks! 319 | 320 | -------------------------------------------------------------------------------- /crdt/handlers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "strconv" 8 | ) 9 | 10 | // HTTP Handler for increment requests. Takes the form of /inc?amount=1 11 | func incHandler(w http.ResponseWriter, r *http.Request) { 12 | r.ParseForm() 13 | 14 | amount, parseErr := strconv.Atoi(r.FormValue("amount")) 15 | 16 | if parseErr != nil { 17 | http.Error(w, parseErr.Error(), 500) 18 | return 19 | } 20 | 21 | if amount < 1 { 22 | http.Error(w, "Only positive amounts are supported", 501) 23 | return 24 | } 25 | 26 | counter.IncVal(amount) 27 | 28 | fmt.Fprintln(w, counter) 29 | 30 | } 31 | 32 | // HTTP Handler to fetch the counter's count. Just / 33 | func getHandler(w http.ResponseWriter, r *http.Request) { 34 | 35 | fmt.Fprintln(w, counter) 36 | } 37 | 38 | // HTTP Handler to fetch the full local CRDT's counter state 39 | func verboseHandler(w http.ResponseWriter, r *http.Request) { 40 | 41 | counterJSON, marshalErr := counter.MarshalJSON() 42 | 43 | if marshalErr != nil { 44 | http.Error(w, marshalErr.Error(), 500) 45 | return 46 | } 47 | 48 | w.Write(counterJSON) 49 | } 50 | 51 | // HTTP Handler to fetch the cluster membership state 52 | func clusterHandler(w http.ResponseWriter, r *http.Request) { 53 | 54 | json.NewEncoder(w).Encode(m.Members()) 55 | 56 | } 57 | -------------------------------------------------------------------------------- /crdt/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "strings" 9 | "time" 10 | 11 | "github.com/hashicorp/memberlist" 12 | "github.com/nphase/crdt" 13 | 14 | uuid "github.com/satori/go.uuid" 15 | ) 16 | 17 | var ( 18 | counter = &crdt.GCounter{} 19 | 20 | members = flag.String("members", "", "comma seperated list of members") 21 | port = flag.Int("port", 4001, "http port") 22 | rpc_port = flag.Int("rpc_port", 0, "memberlist port (0 = auto select)") 23 | 24 | m *memberlist.Memberlist 25 | ) 26 | 27 | func start() error { 28 | flag.Parse() 29 | 30 | counter = crdt.NewGCounter() 31 | 32 | hostname, _ := os.Hostname() 33 | c := memberlist.DefaultWANConfig() 34 | c.BindPort = *rpc_port 35 | c.Name = hostname + "-" + uuid.NewV4().String() 36 | 37 | c.PushPullInterval = time.Second * 3 // to make sync demonstrable 38 | c.ProbeInterval = time.Second * 1 // to make failure demonstrable 39 | 40 | var err error 41 | 42 | m, err = memberlist.Create(c) 43 | 44 | if err != nil { 45 | return err 46 | } 47 | 48 | if len(*members) > 0 { 49 | parts := strings.Split(*members, ",") 50 | _, err := m.Join(parts) 51 | if err != nil { 52 | return err 53 | } 54 | } 55 | 56 | node := m.LocalNode() 57 | fmt.Printf("Local member %s:%d\n", node.Addr, node.Port) 58 | 59 | return nil 60 | } 61 | 62 | func main() { 63 | if err := start(); err != nil { 64 | fmt.Println(err) 65 | } 66 | 67 | http.HandleFunc("/cluster", clusterHandler) 68 | http.HandleFunc("/verbose", verboseHandler) 69 | http.HandleFunc("/inc", incHandler) 70 | http.HandleFunc("/", getHandler) 71 | 72 | fmt.Printf("Listening on :%d\n", *port) 73 | if err := http.ListenAndServe(fmt.Sprintf(":%d", *port), nil); err != nil { 74 | fmt.Println(err) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /final/handlers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "strconv" 8 | ) 9 | 10 | // HTTP Handler for increment requests. Takes the form of /inc?amount=1 11 | func incHandler(w http.ResponseWriter, r *http.Request) { 12 | r.ParseForm() 13 | amountForm := r.Form.Get("amount") 14 | 15 | //parse inc amount 16 | amount, parseErr := strconv.Atoi(amountForm) 17 | 18 | if parseErr != nil { 19 | http.Error(w, parseErr.Error(), 500) 20 | return 21 | } 22 | 23 | if amount < 1 { 24 | http.Error(w, "Only positive amounts are supported", 501) 25 | return 26 | } 27 | 28 | counter.IncVal(amount) 29 | 30 | fmt.Fprintln(w, counter) 31 | 32 | // broadcast the state? 33 | // go BroadcastState() 34 | } 35 | 36 | // HTTP Handler to fetch the counter's count. Just / 37 | func getHandler(w http.ResponseWriter, r *http.Request) { 38 | 39 | val := strconv.Itoa(counter.Count()) 40 | 41 | fmt.Fprintln(w, counter) 42 | } 43 | 44 | // HTTP Handler to fetch the full local CRDT's counter state 45 | func verboseHandler(w http.ResponseWriter, r *http.Request) { 46 | 47 | counterJSON, marshalErr := counter.MarshalJSON() 48 | 49 | if marshalErr != nil { 50 | http.Error(w, marshalErr.Error(), 500) 51 | return 52 | } 53 | 54 | w.Write(counterJSON) 55 | } 56 | 57 | // HTTP Handler to fetch the cluster membership state 58 | func clusterHandler(w http.ResponseWriter, r *http.Request) { 59 | 60 | json.NewEncoder(w).Encode(m.Members()) 61 | 62 | } 63 | -------------------------------------------------------------------------------- /final/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | "github.com/hashicorp/memberlist" 13 | "github.com/nphase/crdt" 14 | 15 | uuid "github.com/satori/go.uuid" 16 | ) 17 | 18 | var ( 19 | counter = &crdt.GCounter{} 20 | 21 | members = flag.String("members", "", "comma seperated list of members") 22 | port = flag.Int("port", 4001, "http port") 23 | rpc_port = flag.Int("rpc_port", 0, "memberlist port (0 = auto select)") 24 | 25 | broadcasts *memberlist.TransmitLimitedQueue 26 | 27 | m *memberlist.Memberlist 28 | ) 29 | 30 | type broadcast struct { 31 | msg []byte 32 | notify chan<- struct{} 33 | } 34 | 35 | type update struct { 36 | Action string // merge 37 | Data json.RawMessage // crdt.GCounterJSON 38 | } 39 | 40 | func (b *broadcast) Invalidates(other memberlist.Broadcast) bool { 41 | return false 42 | } 43 | 44 | func (b *broadcast) Message() []byte { 45 | return b.msg 46 | } 47 | 48 | func (b *broadcast) Finished() { 49 | if b.notify != nil { 50 | close(b.notify) 51 | } 52 | } 53 | 54 | type delegate struct{} 55 | 56 | func (d *delegate) NodeMeta(limit int) []byte { 57 | return []byte{} 58 | } 59 | 60 | //Handle merge events via gossip 61 | func (d *delegate) NotifyMsg(b []byte) { 62 | 63 | if len(b) == 0 { 64 | return 65 | } 66 | 67 | var update *update 68 | if err := json.Unmarshal(b, &update); err != nil { 69 | return 70 | } 71 | 72 | switch update.Action { 73 | case "merge": 74 | fmt.Println(" === Received Broadcast of Remote State === ") 75 | 76 | externalCRDT := crdt.NewGCounterFromJSONBytes([]byte(update.Data)) 77 | counter.Merge(externalCRDT) 78 | default: 79 | panic("unsupported update action") 80 | } 81 | 82 | } 83 | 84 | func (d *delegate) GetBroadcasts(overhead, limit int) [][]byte { 85 | return broadcasts.GetBroadcasts(overhead, limit) 86 | } 87 | 88 | // Share the local counter state via MemberList to another node 89 | func (d *delegate) LocalState(join bool) []byte { 90 | 91 | fmt.Println(" === Sharing Remote State for push/pull sync === ") 92 | 93 | b, err := counter.MarshalJSON() 94 | 95 | if err != nil { 96 | panic(err) 97 | } 98 | 99 | return b 100 | } 101 | 102 | // Merge in received counter state whenever 103 | // join = false means this was received after a push/pull sync 104 | // basically a full state refresh. 105 | func (d *delegate) MergeRemoteState(buf []byte, join bool) { 106 | if len(buf) == 0 { 107 | return 108 | } 109 | 110 | fmt.Println(" === Merging Remote State for push/pull sync === ") 111 | 112 | externalCRDT := crdt.NewGCounterFromJSONBytes(buf) 113 | counter.Merge(externalCRDT) 114 | } 115 | 116 | // BroadcastState broadcasts the local counter state to all cluster members 117 | func BroadcastState() { 118 | 119 | fmt.Println(" === Broadcasting Local State === ") 120 | 121 | counterJSON, marshalErr := counter.MarshalJSON() 122 | 123 | if marshalErr != nil { 124 | panic("Failed to marshal counter state in BroadcastState()") 125 | } 126 | 127 | b, err := json.Marshal(&update{ 128 | Action: "merge", 129 | Data: counterJSON, 130 | }) 131 | 132 | if err != nil { 133 | panic("Failed to marshal broadcast message in BroadcastState()") 134 | } 135 | 136 | broadcasts.QueueBroadcast(&broadcast{ 137 | msg: b, 138 | notify: nil, 139 | }) 140 | 141 | } 142 | 143 | func start() error { 144 | flag.Parse() 145 | 146 | counter = crdt.NewGCounter() 147 | 148 | hostname, _ := os.Hostname() 149 | c := memberlist.DefaultWANConfig() 150 | c.Delegate = &delegate{} 151 | c.BindPort = *rpc_port 152 | c.Name = hostname + "-" + uuid.NewV4().String() 153 | 154 | c.PushPullInterval = time.Second * 10 // to make it demonstrable 155 | c.ProbeInterval = time.Second * 1 // to make failure demonstrable 156 | 157 | var err error 158 | 159 | m, err = memberlist.Create(c) 160 | if err != nil { 161 | return err 162 | } 163 | 164 | if len(*members) > 0 { 165 | parts := strings.Split(*members, ",") 166 | _, err := m.Join(parts) 167 | if err != nil { 168 | return err 169 | } 170 | } 171 | 172 | broadcasts = &memberlist.TransmitLimitedQueue{ 173 | NumNodes: func() int { 174 | return m.NumMembers() 175 | }, 176 | RetransmitMult: 3, 177 | } 178 | 179 | node := m.LocalNode() 180 | fmt.Printf("Local member %s:%d\n", node.Addr, node.Port) 181 | 182 | return nil 183 | } 184 | 185 | func main() { 186 | if err := start(); err != nil { 187 | fmt.Println(err) 188 | } 189 | 190 | http.HandleFunc("/cluster", clusterHandler) 191 | http.HandleFunc("/verbose", verboseHandler) 192 | http.HandleFunc("/inc", incHandler) 193 | http.HandleFunc("/", getHandler) 194 | 195 | fmt.Printf("Listening on :%d\n", *port) 196 | if err := http.ListenAndServe(fmt.Sprintf(":%d", *port), nil); err != nil { 197 | fmt.Println(err) 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /g-counter-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphase/go-clustering-example/c181b156b4603a687d4defecdf02e7a26814928e/g-counter-1.png -------------------------------------------------------------------------------- /g-counter-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphase/go-clustering-example/c181b156b4603a687d4defecdf02e7a26814928e/g-counter-2.png -------------------------------------------------------------------------------- /g-counter-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nphase/go-clustering-example/c181b156b4603a687d4defecdf02e7a26814928e/g-counter-3.png -------------------------------------------------------------------------------- /memberlist/handlers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "strconv" 8 | ) 9 | 10 | // incHandler is a HTTP Handler for increment requests. Takes the form of /inc?amount=1 11 | func incHandler(w http.ResponseWriter, r *http.Request) { 12 | r.ParseForm() 13 | 14 | //parse inc amount 15 | amount, parseErr := strconv.Atoi(r.FormValue("amount")) 16 | 17 | if parseErr != nil { 18 | http.Error(w, parseErr.Error(), 500) 19 | return 20 | } 21 | 22 | if amount < 0 { 23 | http.Error(w, "Deprecation not supported", 501) 24 | return 25 | } 26 | 27 | counter.IncVal(amount) 28 | 29 | fmt.Printf("Incremented counter to %v\n", counter) 30 | fmt.Fprintln(w, counter) 31 | 32 | } 33 | 34 | // getHandler is a HTTP Handler to fetch the counter's count. Just / 35 | func getHandler(w http.ResponseWriter, r *http.Request) { 36 | val := strconv.Itoa(counter.Count()) 37 | fmt.Fprintln(w, counter) 38 | } 39 | 40 | // HTTP Handler to fetch the cluster membership state 41 | func clusterHandler(w http.ResponseWriter, r *http.Request) { 42 | 43 | json.NewEncoder(w).Encode(m.Members()) 44 | 45 | } 46 | -------------------------------------------------------------------------------- /memberlist/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "strconv" 9 | "strings" 10 | "sync/atomic" 11 | "time" 12 | 13 | "github.com/hashicorp/memberlist" 14 | 15 | uuid "github.com/satori/go.uuid" 16 | ) 17 | 18 | var ( 19 | counter *Counter 20 | 21 | members = flag.String("members", "", "comma seperated list of members") 22 | port = flag.Int("port", 4001, "http port") 23 | rpc_port = flag.Int("rpc_port", 0, "memberlist port (0 = auto select)") 24 | 25 | m *memberlist.Memberlist 26 | ) 27 | 28 | type Counter struct { 29 | val int32 30 | } 31 | 32 | // IncVal increments the counter's value by d 33 | func (c *Counter) IncVal(d int) { 34 | atomic.AddInt32(&c.val, int32(d)) 35 | } 36 | 37 | // Count fetches the counter value 38 | func (c *Counter) Count() int { 39 | return int(atomic.LoadInt32(&c.val)) 40 | } 41 | 42 | func (c *Counter) String() string { 43 | return strconv.Itoa(counter.Count()) 44 | } 45 | 46 | func start() error { 47 | flag.Parse() 48 | 49 | counter = &Counter{} 50 | 51 | hostname, _ := os.Hostname() 52 | c := memberlist.DefaultWANConfig() 53 | 54 | c.BindPort = *rpc_port 55 | c.Name = hostname + "-" + uuid.NewV4().String() 56 | 57 | c.PushPullInterval = time.Second * 5 // to make sync demonstrable 58 | c.ProbeInterval = time.Second * 1 // to make failure demonstrable 59 | 60 | var err error 61 | 62 | m, err = memberlist.Create(c) 63 | if err != nil { 64 | return err 65 | } 66 | if len(*members) > 0 { 67 | parts := strings.Split(*members, ",") 68 | _, err := m.Join(parts) 69 | if err != nil { 70 | return err 71 | } 72 | } 73 | 74 | node := m.LocalNode() 75 | fmt.Printf("Local member %s:%d\n", node.Addr, node.Port) 76 | return nil 77 | } 78 | 79 | func main() { 80 | if err := start(); err != nil { 81 | fmt.Println(err) 82 | } 83 | 84 | http.HandleFunc("/cluster", clusterHandler) 85 | 86 | http.HandleFunc("/inc", incHandler) 87 | http.HandleFunc("/", getHandler) 88 | 89 | fmt.Printf("Listening on :%d\n", *port) 90 | if err := http.ListenAndServe(fmt.Sprintf(":%d", *port), nil); err != nil { 91 | fmt.Println(err) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /simple_counter/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net/http" 7 | "strconv" 8 | "sync/atomic" 9 | ) 10 | 11 | var ( 12 | port = flag.Int("port", 4000, "http port") 13 | counter *Counter 14 | ) 15 | 16 | // START OMIT 17 | type Counter struct { 18 | val int32 19 | } 20 | 21 | // IncVal increments the counter's value by d 22 | func (c *Counter) IncVal(d int) { 23 | atomic.AddInt32(&c.val, int32(d)) 24 | } 25 | 26 | // Count fetches the counter value 27 | func (c *Counter) Count() int { 28 | return int(atomic.LoadInt32(&c.val)) 29 | } 30 | 31 | func (c *Counter) String() string { 32 | return strconv.Itoa(counter.Count()) 33 | } 34 | 35 | // END OMIT 36 | 37 | // incHandler is a HTTP Handler for increment requests. Takes the form of /inc?amount=1 38 | func incHandler(w http.ResponseWriter, r *http.Request) { 39 | r.ParseForm() 40 | 41 | //parse inc amount 42 | amount, parseErr := strconv.Atoi(r.FormValue("amount")) 43 | 44 | if parseErr != nil { 45 | http.Error(w, parseErr.Error(), 500) 46 | return 47 | } 48 | 49 | if amount < 0 { 50 | http.Error(w, "Deprecation not supported", 501) 51 | return 52 | } 53 | 54 | counter.IncVal(amount) 55 | 56 | fmt.Printf("Incremented counter to %v\n", counter) 57 | fmt.Fprintln(w, counter) 58 | 59 | } 60 | 61 | // getHandler is a HTTP Handler to fetch the counter's count. Just / 62 | func getHandler(w http.ResponseWriter, r *http.Request) { 63 | fmt.Fprintln(w, counter) 64 | } 65 | 66 | func main() { 67 | flag.Parse() 68 | 69 | counter = &Counter{} 70 | 71 | http.HandleFunc("/inc", incHandler) 72 | http.HandleFunc("/", getHandler) 73 | 74 | fmt.Printf("Listening on :%d\n", *port) 75 | if err := http.ListenAndServe(fmt.Sprintf(":%d", *port), nil); err != nil { 76 | fmt.Println(err) 77 | } 78 | } 79 | --------------------------------------------------------------------------------