The following steps cover writing and running your application:
- From your Terminal or console application, create a new directory called ~/projects/go-programming-cookbook/chapter11/consensus and navigate to it.
- Run the following command:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/consensus
You should see a file called go.mod that contains the following content:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter11/consensus
- Copy the tests from ~/projects/go-programming-cookbook-original/chapter11/consensus, or use this as an opportunity to write some of your own code!
- Create a file called state.go with the following content:
package consensus
type state string
const (
first state = "first"
second = "second"
third = "third"
)
var allowedState map[state][]state
func init() {
// setup valid states
allowedState = make(map[state][]state)
allowedState[first] = []state{second, third}
allowedState[second] = []state{third}
allowedState[third] = []state{first}
}
// CanTransition checks if a new state is valid
func (s *state) CanTransition(next state) bool {
for _, n := range allowedState[*s] {
if n == next {
return true
}
}
return false
}
// Transition will move a state to the next
// state if able
func (s *state) Transition(next state) {
if s.CanTransition(next) {
*s = next
}
}
- Create a file called raftset.go with the following content:
package consensus
import (
"fmt"
"github.com/hashicorp/raft"
)
// keep a map of rafts for later
var rafts map[raft.ServerAddress]*raft.Raft
func init() {
rafts = make(map[raft.ServerAddress]*raft.Raft)
}
// raftSet stores all the setup material we need
type raftSet struct {
Config *raft.Config
Store *raft.InmemStore
SnapShotStore raft.SnapshotStore
FSM *FSM
Transport raft.LoopbackTransport
Configuration raft.Configuration
}
// generate n raft sets to bootstrap the raft cluster
func getRaftSet(num int) []*raftSet {
rs := make([]*raftSet, num)
servers := make([]raft.Server, num)
for i := 0; i < num; i++ {
addr := raft.ServerAddress(fmt.Sprint(i))
_, transport := raft.NewInmemTransport(addr)
servers[i] = raft.Server{
Suffrage: raft.Voter,
ID: raft.ServerID(addr),
Address: addr,
}
config := raft.DefaultConfig()
config.LocalID = raft.ServerID(addr)
rs[i] = &raftSet{
Config: config,
Store: raft.NewInmemStore(),
SnapShotStore: raft.NewInmemSnapshotStore(),
FSM: NewFSM(),
Transport: transport,
}
}
// configuration needs to be consistent between
// services and so we need the full serverlist in this
// case
for _, r := range rs {
r.Configuration = raft.Configuration{Servers: servers}
}
return rs
}
- Create a file called config.go with the following content:
package consensus
import (
"github.com/hashicorp/raft"
)
// Config creates num in-memory raft
// nodes and connects them
func Config(num int) {
// create n "raft-sets" consisting of
// everything needed to represent a node
rs := getRaftSet(num)
//connect all of the transports
for _, r1 := range rs {
for _, r2 := range rs {
r1.Transport.Connect(r2.Transport.LocalAddr(), r2.Transport)
}
}
// for each node, bootstrap then connect
for _, r := range rs {
if err := raft.BootstrapCluster(r.Config, r.Store, r.Store, r.SnapShotStore, r.Transport, r.Configuration); err != nil {
panic(err)
}
raft, err := raft.NewRaft(r.Config, r.FSM, r.Store, r.Store, r.SnapShotStore, r.Transport)
if err != nil {
panic(err)
}
rafts[r.Transport.LocalAddr()] = raft
}
}
- Create a file called fsm.go with the following content:
package consensus
import (
"io"
"github.com/hashicorp/raft"
)
// FSM implements the raft FSM interface
// and holds a state
type FSM struct {
state state
}
// NewFSM creates a new FSM with
// start state of "first"
func NewFSM() *FSM {
return &FSM{state: first}
}
// Apply updates our FSM
func (f *FSM) Apply(r *raft.Log) interface{} {
f.state.Transition(state(r.Data))
return string(f.state)
}
// Snapshot needed to satisfy the raft FSM interface
func (f *FSM) Snapshot() (raft.FSMSnapshot, error) {
return nil, nil
}
// Restore needed to satisfy the raft FSM interface
func (f *FSM) Restore(io.ReadCloser) error {
return nil
}
- Create a file called handler.go with the following content:
package consensus
import (
"net/http"
"time"
)
// Handler grabs the get param ?next= and tries
// to transition to the state contained there
func Handler(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
state := r.FormValue("next")
for address, raft := range rafts {
if address != raft.Leader() {
continue
}
result := raft.Apply([]byte(state), 1*time.Second)
if result.Error() != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
newState, ok := result.Response().(string)
if !ok {
w.WriteHeader(http.StatusInternalServerError)
return
}
if newState != state {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("invalid transition"))
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(newState))
return
}
}
- Create a new directory named example and navigate to it.
- Create a file named main.go with the following content:
package main
import (
"net/http"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter11/consensus"
)
func main() {
consensus.Config(3)
http.HandleFunc("/", consensus.Handler)
err := http.ListenAndServe(":3333", nil)
panic(err)
}
- Run the go run main.go command. Alternatively, you may also run the following commands:
$ go build
$ ./example
You should now see the following output:
$ go run main.go
2019/05/04 21:06:46 [INFO] raft: Initial configuration (index=1): [{Suffrage:Voter ID:0 Address:0} {Suffrage:Voter ID:1 Address:1} {Suffrage:Voter ID:2 Address:2}]
2019/05/04 21:06:46 [INFO] raft: Initial configuration (index=1): [{Suffrage:Voter ID:0 Address:0} {Suffrage:Voter ID:1 Address:1} {Suffrage:Voter ID:2 Address:2}]
2019/05/04 21:06:46 [INFO] raft: Node at 0 [Follower] entering Follower state (Leader: "")
2019/05/04 21:06:46 [INFO] raft: Node at 1 [Follower] entering Follower state (Leader: "")
2019/05/04 21:06:46 [INFO] raft: Initial configuration (index=1): [{Suffrage:Voter ID:0 Address:0} {Suffrage:Voter ID:1 Address:1} {Suffrage:Voter ID:2 Address:2}]
2019/05/04 21:06:46 [INFO] raft: Node at 2 [Follower] entering Follower state (Leader: "")
2019/05/04 21:06:47 [WARN] raft: Heartbeat timeout from "" reached, starting election
2019/05/04 21:06:47 [INFO] raft: Node at 0 [Candidate] entering Candidate state in term 2
2019/05/04 21:06:47 [DEBUG] raft: Votes needed: 2
2019/05/04 21:06:47 [DEBUG] raft: Vote granted from 0 in term 2. Tally: 1
2019/05/04 21:06:47 [DEBUG] raft: Vote granted from 1 in term 2. Tally: 2
2019/05/04 21:06:47 [INFO] raft: Election won. Tally: 2
2019/05/04 21:06:47 [INFO] raft: Node at 0 [Leader] entering Leader state
2019/05/04 21:06:47 [INFO] raft: Added peer 1, starting replication
2019/05/04 21:06:47 [INFO] raft: Added peer 2, starting replication
2019/05/04 21:06:47 [INFO] raft: pipelining replication to peer {Voter 1 1}
2019/05/04 21:06:47 [INFO] raft: pipelining replication to peer {Voter 2 2}
- In a separate Terminal, run the following command:
$ curl "http://localhost:3333/?next=second"
second
$ curl "http://localhost:3333/?next=third"
third
$ curl "http://localhost:3333/?next=second"
invalid transition
$ curl "http://localhost:3333/?next=first"
first
- The go.mod file may be updated and the go.sum file should now be present in the top-level recipe directory.
- If you copied or wrote your own tests, go up one directory and run go test. Ensure that all the tests pass.