How to do it...

The following steps cover writing and running your application:

  1. From your Terminal or console application, create a new directory called ~/projects/go-programming-cookbook/chapter11/consensus and navigate to it.
  2. 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    
  1. Copy the tests from ~/projects/go-programming-cookbook-original/chapter11/consensus, or use this as an opportunity to write some of your own code!
  2. 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
}
}
  1. 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
}
  1. 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
}
}
  1. 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
}
  1. 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
}
}
  1. Create a new directory named example and navigate to it.
  2. 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)
}
  1. 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}
  1. 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
  1. The go.mod file may be updated and the go.sum file should now be present in the top-level recipe directory.
  2. If you copied or wrote your own tests, go up one directory and run go test. Ensure that all the tests pass.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.134.81.206