Skip to content

Commit

Permalink
Implement service discovery, agent
Browse files Browse the repository at this point in the history
Again inspired heavily by Distributed Services by Go, this adds service
discovery with Serf/SWIM with an agent which is repsonsible for
configuring the server, service discovery, Raft etc.

Server now takes a store interface, allowing us to swap in a
DistributedStore instead of a Store.

cmux is used to multiplex Raft and gRPC on the same port.
  • Loading branch information
antw committed Dec 4, 2021
1 parent a9b11b9 commit cc92609
Show file tree
Hide file tree
Showing 9 changed files with 364 additions and 114 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ require (
github.com/miekg/dns v1.1.41 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
github.com/soheilhy/cmux v0.1.5 // indirect
go.etcd.io/bbolt v1.3.5 // indirect
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1 // indirect
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=
github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
Expand Down Expand Up @@ -163,6 +165,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1 h1:4qWs8cYYH6PoEFy4dfhDFgoMGkwAcETd+MmPdCPMzUc=
Expand All @@ -187,6 +190,7 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand Down
179 changes: 179 additions & 0 deletions internal/agent/agent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package agent

import (
"bytes"
"fmt"
"io"
"net"
"sync"
"time"

"github.com/antw/violin/internal/server"
"github.com/hashicorp/raft"
"github.com/soheilhy/cmux"
"google.golang.org/grpc"

"github.com/antw/violin/internal/discovery"
"github.com/antw/violin/internal/storage"
)

type Agent struct {
Config

mux cmux.CMux
store *storage.DistributedStore
server *grpc.Server
membership *discovery.Membership

shutdown bool
shutdowns chan struct{}
shutdownLock sync.Mutex
}

type Config struct {
DataDir string
BindAddr string
RPCPort int
NodeName string
StartJoinAddrs []string
Bootstrap bool
}

func (c Config) RPCAddr() (string, error) {
host, _, err := net.SplitHostPort(c.BindAddr)
if err != nil {
return "", err
}

return fmt.Sprintf("%s:%d", host, c.RPCPort), nil
}

func New(config Config) (*Agent, error) {
a := &Agent{
Config: config,
shutdowns: make(chan struct{}),
}

setup := []func() error{
a.setupMux,
a.setupStore,
a.setupServer,
a.setupMembership,
}
for _, fn := range setup {
if err := fn(); err != nil {
return nil, err
}
}

go func() {
_ = a.serve()
}()

return a, nil
}

// setupStore configures cmux to allow multiplexing Raft and the gRPC server on the same port.
func (a *Agent) setupMux() error {
rpcAddr := fmt.Sprintf(":%d", a.Config.RPCPort)

listener, err := net.Listen("tcp", rpcAddr)
if err != nil {
return err
}

a.mux = cmux.New(listener)
return nil
}

// setupStore configures the distributed store.
func (a *Agent) setupStore() error {
raftListener := a.mux.Match(func(reader io.Reader) bool {
b := make([]byte, 1)
if _, err := reader.Read(b); err != nil {
return false
}
return bytes.Compare(b, []byte{storage.RaftRPC}) == 0
})

storeConfig := storage.Config{}
storeConfig.Raft.StreamLayer = storage.NewStreamLayer(raftListener)
storeConfig.Raft.LocalID = raft.ServerID(a.Config.NodeName)
storeConfig.Raft.Bootstrap = a.Config.Bootstrap

var err error

a.store, err = storage.NewDistributedStore(a.Config.DataDir, storeConfig)
if err != nil {
return err
}

if a.Config.Bootstrap {
err = a.store.WaitForLeader(3 * time.Second)
}

return nil
}

// setupServer configures the server and starts a listener.
func (a *Agent) setupServer() error {
a.server = server.New(a.store)

grpcListener := a.mux.Match(cmux.Any())

go func() {
if err := a.server.Serve(grpcListener); err != nil {
_ = a.Shutdown()
}
}()

return nil
}

func (a *Agent) setupMembership() error {
rpcAddr, err := a.RPCAddr()
if err != nil {
return err
}

a.membership, err = discovery.New(a.store, discovery.Config{
NodeName: a.Config.NodeName,
BindAddr: a.Config.BindAddr,
Tags: map[string]string{
"rpc_addr": rpcAddr,
},
StartJoinAddrs: a.Config.StartJoinAddrs,
})

return err
}

func (a *Agent) Shutdown() error {
a.shutdownLock.Lock()
defer a.shutdownLock.Unlock()

if a.shutdown {
return nil
}

a.shutdown = true
close(a.shutdowns)

if err := a.membership.Leave(); err != nil {
return err
}

a.server.GracefulStop()

return nil
}

// serve runs the server
func (a *Agent) serve() error {
if err := a.mux.Serve(); err != nil {
_ = a.Shutdown()
return err
}

return nil
}
104 changes: 104 additions & 0 deletions internal/agent/agent_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package agent

import (
"context"
"fmt"
"io/ioutil"
"os"
"testing"
"time"

"github.com/phayes/freeport"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"

"github.com/antw/violin/api"
)

func TestAgent(t *testing.T) {
var agents []*Agent
for i := 0; i < 3; i++ {
ports, err := freeport.GetFreePorts(2)
require.NoError(t, err)

bindAddr := fmt.Sprintf("%s:%d", "127.0.0.1", ports[0])

dataDir, err := ioutil.TempDir("", "agent-test")
require.NoError(t, err)

var startJoinAddrs []string
if i > 0 {
startJoinAddrs = append(startJoinAddrs, agents[0].Config.BindAddr)
}

fmt.Printf("!!! Server %d addr %s\n", i, bindAddr)

agent, err := New(Config{
NodeName: fmt.Sprintf("%d", i),
Bootstrap: i == 0,
StartJoinAddrs: startJoinAddrs,
BindAddr: bindAddr,
RPCPort: ports[1],
DataDir: dataDir,
})
require.NoError(t, err)

agents = append(agents, agent)
}

defer func() {
for _, agent := range agents {
err := agent.Shutdown()
require.NoError(t, err)
require.NoError(t, os.RemoveAll(agent.Config.DataDir))
}
}()

time.Sleep(3 * time.Second)

leaderClient := client(t, agents[0])
_, err := leaderClient.Set(
context.Background(),
&api.SetRequest{Register: &api.KV{Key: "foo", Value: []byte("bar")}},
)
require.NoError(t, err)

consumeResponse, err := leaderClient.Get(
context.Background(),
&api.GetRequest{Key: "foo"},
)
require.NoError(t, err)
require.Equal(t, "bar", string(consumeResponse.GetRegister().GetValue()))

// wait until replication has finished
time.Sleep(3 * time.Second)

followerClient := client(t, agents[1])
consumeResponse, err = followerClient.Get(
context.Background(),
&api.GetRequest{Key: "foo"},
)
require.NoError(t, err)
require.Equal(t, "bar", string(consumeResponse.GetRegister().GetValue()))

// Check accessing a register which does not exist.
consumeResponse, err = leaderClient.Get(
context.Background(),
&api.GetRequest{Key: "baz"},
)
require.Nil(t, consumeResponse)
require.Error(t, err)

// TODO: Check the gRPC error code?
}

// client creates a client for interacting with a server.
func client(t *testing.T, agent *Agent) api.RegisterClient {
rpcAddr, err := agent.Config.RPCAddr()
require.NoError(t, err)

conn, err := grpc.Dial(rpcAddr, grpc.WithInsecure())
require.NoError(t, err)

return api.NewRegisterClient(conn)
}
10 changes: 7 additions & 3 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,19 @@ import (
"google.golang.org/grpc"

"github.com/antw/violin/api"
"github.com/antw/violin/internal/storage"
)

type store interface {
Get(key string) (value []byte, ok bool)
Set(key string, value []byte) error
}

type server struct {
api.UnimplementedRegisterServer
store *storage.Store
store store
}

func New(store *storage.Store, grpcOpts ...grpc.ServerOption) *grpc.Server {
func New(store store, grpcOpts ...grpc.ServerOption) *grpc.Server {
srv := &server{store: store}

grpcSrv := grpc.NewServer(grpcOpts...)
Expand Down
4 changes: 2 additions & 2 deletions internal/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ func setupTest(t *testing.T) (*grpc.Server, api.RegisterClient, func()) {
require.NoError(t, err)

store := storage.NewStore()
server := New(&store)
server := New(store)

go func() {
server.Serve(listener)
_ = server.Serve(listener)
}()

clientConn, err := grpc.Dial(listener.Addr().String(), grpc.WithInsecure())
Expand Down
Loading

0 comments on commit cc92609

Please sign in to comment.