commit
1f61988369
@ -0,0 +1,175 @@
|
|||||||
|
package cluster
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ConnectionTimeout = 10 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
var respOKMarshalled []byte
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
var err error
|
||||||
|
respOKMarshalled, err = json.Marshal(response{})
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("unable to JSON marshall OK response: %s", err.Error()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type response struct {
|
||||||
|
Code int `json:"code,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listener is the interface the network service must provide.
|
||||||
|
type Listener interface {
|
||||||
|
net.Listener
|
||||||
|
|
||||||
|
// Dial is used to create a new outgoing connection
|
||||||
|
Dial(address string, timeout time.Duration) (net.Conn, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store represents a store of information, managed via consensus.
|
||||||
|
type Store interface {
|
||||||
|
// Leader returns the leader of the consensus system.
|
||||||
|
Leader() string
|
||||||
|
|
||||||
|
// UpdateAPIPeers updates the API peers on the store.
|
||||||
|
UpdateAPIPeers(peers map[string]string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service allows access to the cluster and associated meta data,
|
||||||
|
// via consensus.
|
||||||
|
type Service struct {
|
||||||
|
ln Listener
|
||||||
|
store Store
|
||||||
|
addr net.Addr
|
||||||
|
|
||||||
|
wg sync.WaitGroup
|
||||||
|
|
||||||
|
logger *log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewService returns a new instance of the cluster service
|
||||||
|
func NewService(ln Listener, store Store) *Service {
|
||||||
|
return &Service{
|
||||||
|
ln: ln,
|
||||||
|
store: store,
|
||||||
|
addr: ln.Addr(),
|
||||||
|
logger: log.New(os.Stderr, "[cluster] ", log.LstdFlags),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open opens the Service.
|
||||||
|
func (s *Service) Open() error {
|
||||||
|
s.wg.Add(1)
|
||||||
|
go s.serve()
|
||||||
|
s.logger.Println("service listening on", s.ln.Addr())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the service.
|
||||||
|
func (s *Service) Close() error {
|
||||||
|
s.ln.Close()
|
||||||
|
s.wg.Wait()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Addr returns the address the service is listening on.
|
||||||
|
func (s *Service) Addr() string {
|
||||||
|
return s.addr.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPeer will set the mapping between raftAddr and apiAddr for the entire cluster.
|
||||||
|
func (s *Service) SetPeer(raftAddr, apiAddr string) error {
|
||||||
|
peer := map[string]string{
|
||||||
|
raftAddr: apiAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try the local store. It might be the leader.
|
||||||
|
err := s.store.UpdateAPIPeers(peer)
|
||||||
|
if err == nil {
|
||||||
|
// All done! Aren't we lucky?
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try talking to the leader over the network.
|
||||||
|
conn, err := s.ln.Dial(s.store.Leader(), ConnectionTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
b, err := json.Marshal(peer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := conn.Write(b); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the response and verify the operation went through.
|
||||||
|
resp := response{}
|
||||||
|
d := json.NewDecoder(conn)
|
||||||
|
err = d.Decode(&resp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Code != 0 {
|
||||||
|
return fmt.Errorf(resp.Message)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) serve() error {
|
||||||
|
defer s.wg.Done()
|
||||||
|
|
||||||
|
for {
|
||||||
|
conn, err := s.ln.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
go s.handleConn(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) handleConn(conn net.Conn) {
|
||||||
|
// Only handles peers updates for now.
|
||||||
|
peers := make(map[string]string)
|
||||||
|
d := json.NewDecoder(conn)
|
||||||
|
err := d.Decode(&peers)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the peers.
|
||||||
|
if err := s.store.UpdateAPIPeers(peers); err != nil {
|
||||||
|
resp := response{1, err.Error()}
|
||||||
|
b, err := json.Marshal(resp)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close() // Only way left to signal.
|
||||||
|
} else {
|
||||||
|
if _, err := conn.Write(b); err != nil {
|
||||||
|
conn.Close() // Only way left to signal.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let the remote node know everything went OK.
|
||||||
|
if _, err := conn.Write(respOKMarshalled); err != nil {
|
||||||
|
conn.Close() // Only way left to signal.
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
@ -0,0 +1,128 @@
|
|||||||
|
package cluster
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_NewServiceOpenClose(t *testing.T) {
|
||||||
|
ml := mustNewMockListener()
|
||||||
|
ms := &mockStore{}
|
||||||
|
s := NewService(ml, ms)
|
||||||
|
if s == nil {
|
||||||
|
t.Fatalf("failed to create cluster service")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Open(); err != nil {
|
||||||
|
t.Fatalf("failed to open cluster service")
|
||||||
|
}
|
||||||
|
if err := s.Close(); err != nil {
|
||||||
|
t.Fatalf("failed to close cluster service")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_SetAPIPeer(t *testing.T) {
|
||||||
|
raftAddr, apiAddr := "localhost:4002", "localhost:4001"
|
||||||
|
|
||||||
|
s, _, ms := mustNewOpenService()
|
||||||
|
defer s.Close()
|
||||||
|
if err := s.SetPeer(raftAddr, apiAddr); err != nil {
|
||||||
|
t.Fatalf("failed to set peer: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if ms.peers[raftAddr] != apiAddr {
|
||||||
|
t.Fatalf("peer not set correctly, exp %s, got %s", apiAddr, ms.peers[raftAddr])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_SerAPIPeerNetwork(t *testing.T) {
|
||||||
|
t.Skip("remote service not responding correctly")
|
||||||
|
|
||||||
|
raftAddr, apiAddr := "localhost:4002", "localhost:4001"
|
||||||
|
|
||||||
|
s, _, ms := mustNewOpenService()
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
raddr, err := net.ResolveTCPAddr("tcp", s.Addr())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to resolve remote uster ervice address: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := net.DialTCP("tcp4", nil, raddr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to connect to remote cluster service: %s", err.Error())
|
||||||
|
}
|
||||||
|
conn.Write([]byte(fmt.Sprintf(`{"%s": "%s"}`, raftAddr, apiAddr)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to write to remote cluster service: %s", err.Error())
|
||||||
|
}
|
||||||
|
// XXX Check response
|
||||||
|
|
||||||
|
if ms.peers[raftAddr] != apiAddr {
|
||||||
|
t.Fatalf("peer not set correctly, exp %s, got %s", apiAddr, ms.peers[raftAddr])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustNewOpenService() (*Service, *mockListener, *mockStore) {
|
||||||
|
ml := mustNewMockListener()
|
||||||
|
ms := newMockStore()
|
||||||
|
s := NewService(ml, ms)
|
||||||
|
if err := s.Open(); err != nil {
|
||||||
|
panic("failed to open new service")
|
||||||
|
}
|
||||||
|
return s, ml, ms
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockListener struct {
|
||||||
|
ln net.Listener
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustNewMockListener() *mockListener {
|
||||||
|
ln, err := net.Listen("tcp", "localhost:0")
|
||||||
|
if err != nil {
|
||||||
|
panic("failed to create mock listener")
|
||||||
|
}
|
||||||
|
return &mockListener{
|
||||||
|
ln: ln,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ml *mockListener) Accept() (c net.Conn, err error) {
|
||||||
|
return ml.ln.Accept()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ml *mockListener) Addr() net.Addr {
|
||||||
|
return ml.ln.Addr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ml *mockListener) Close() (err error) {
|
||||||
|
return ml.ln.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ml *mockListener) Dial(addr string, t time.Duration) (net.Conn, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockStore struct {
|
||||||
|
leader string
|
||||||
|
peers map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockStore() *mockStore {
|
||||||
|
return &mockStore{
|
||||||
|
peers: make(map[string]string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms *mockStore) Leader() string {
|
||||||
|
return ms.leader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms *mockStore) UpdateAPIPeers(peers map[string]string) error {
|
||||||
|
for k, v := range peers {
|
||||||
|
ms.peers[k] = v
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in New Issue