mirror of https://github.com/docker/docs.git
leadership: Distributed Leader Election
Signed-off-by: Andrea Luzzardi <aluzzardi@gmail.com>
This commit is contained in:
parent
78839ba4ea
commit
3f01413b75
|
@ -0,0 +1,58 @@
|
||||||
|
# Leadership: Distributed Leader Election for Clustered Environments.
|
||||||
|
|
||||||
|
Leadership is a library for a cluster leader election on top of a distributed
|
||||||
|
Key/Value store.
|
||||||
|
|
||||||
|
It's built using Swarm's `pkg/store` and is designed to work across multiple
|
||||||
|
storage backends.
|
||||||
|
|
||||||
|
Right now only `Consul` is supported but `etcd` and `Zookeeper` will be coming
|
||||||
|
soon.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Create a store using pkg/store.
|
||||||
|
client, err := store.NewStore("consul", []string{"127.0.0.1:8500"}, &store.Config{})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
underwood := leadership.NewCandidate(client, "service/swarm/leader", "underwood")
|
||||||
|
underwood.RunForElection()
|
||||||
|
|
||||||
|
for elected := range candidate.ElectedCh {
|
||||||
|
// This loop will run every time there is a change in our leadership
|
||||||
|
// status.
|
||||||
|
|
||||||
|
if elected {
|
||||||
|
// We won the election - we are now the leader.
|
||||||
|
// Let's do leader stuff, for example, sleep for a while.
|
||||||
|
log.Printf("I won the election! I'm now the leader")
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
|
||||||
|
// Tired of being a leader? You can resign anytime.
|
||||||
|
candidate.Resign()
|
||||||
|
} else {
|
||||||
|
// We lost the election but are still running for leadership.
|
||||||
|
// `elected == false` is the default state and is the first event
|
||||||
|
// we'll receive from the channel. After a successfull election,
|
||||||
|
// this event can get triggered if someone else steals the
|
||||||
|
// leadership or if we resign.
|
||||||
|
|
||||||
|
log.Printf("Lost the election, let's try another time")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
It is possible to follow an election in real-time and get notified whenever
|
||||||
|
there is a change in leadership:
|
||||||
|
```go
|
||||||
|
follower := leadership.NewFollower(client, "service/swarm/leader")
|
||||||
|
follower.FollowElection()
|
||||||
|
for leader := <-follower.LeaderCh {
|
||||||
|
// Leader is a string containing the value passed to `NewCandidate`.
|
||||||
|
log.Printf("%s is now the leader", leader)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
A typical usecase for this is to be able to always send requests to the current
|
||||||
|
leader.
|
|
@ -0,0 +1,108 @@
|
||||||
|
package leadership
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
log "github.com/Sirupsen/logrus"
|
||||||
|
"github.com/docker/swarm/pkg/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Candidate runs the leader election algorithm asynchronously
|
||||||
|
type Candidate struct {
|
||||||
|
ElectedCh chan bool
|
||||||
|
|
||||||
|
client store.Store
|
||||||
|
key string
|
||||||
|
node string
|
||||||
|
|
||||||
|
lock sync.Mutex
|
||||||
|
leader bool
|
||||||
|
stopCh chan struct{}
|
||||||
|
resignCh chan bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCandidate creates a new Candidate
|
||||||
|
func NewCandidate(client store.Store, key, node string) *Candidate {
|
||||||
|
return &Candidate{
|
||||||
|
client: client,
|
||||||
|
key: key,
|
||||||
|
node: node,
|
||||||
|
|
||||||
|
ElectedCh: make(chan bool),
|
||||||
|
leader: false,
|
||||||
|
resignCh: make(chan bool),
|
||||||
|
stopCh: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunForElection starts the leader election algorithm. Updates in status are
|
||||||
|
// pushed through the ElectedCh channel.
|
||||||
|
func (c *Candidate) RunForElection() error {
|
||||||
|
// Need a `SessionTTL` (keep-alive) and a stop channel.
|
||||||
|
lock, err := c.client.NewLock(c.key, &store.LockOptions{Value: []byte(c.node)})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
go c.campaign(lock)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop running for election.
|
||||||
|
func (c *Candidate) Stop() {
|
||||||
|
close(c.stopCh)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resign forces the candidate to step-down and try again.
|
||||||
|
// If the candidate is not a leader, it doesn't have any effect.
|
||||||
|
// Candidate will retry immediately to acquire the leadership. If no-one else
|
||||||
|
// took it, then the Candidate will end up being a leader again.
|
||||||
|
func (c *Candidate) Resign() {
|
||||||
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
|
if c.leader {
|
||||||
|
c.resignCh <- true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Candidate) update(status bool) {
|
||||||
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
|
c.ElectedCh <- status
|
||||||
|
c.leader = status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Candidate) campaign(lock store.Locker) {
|
||||||
|
defer close(c.ElectedCh)
|
||||||
|
|
||||||
|
for {
|
||||||
|
// Start as a follower.
|
||||||
|
c.update(false)
|
||||||
|
|
||||||
|
lostCh, err := lock.Lock()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hooray! We acquired the lock therefore we are the new leader.
|
||||||
|
c.update(true)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-c.resignCh:
|
||||||
|
// We were asked to resign, give up the lock and go back
|
||||||
|
// campaigning.
|
||||||
|
lock.Unlock()
|
||||||
|
case <-c.stopCh:
|
||||||
|
// Give up the leadership and quit.
|
||||||
|
if c.leader {
|
||||||
|
lock.Unlock()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case <-lostCh:
|
||||||
|
// We lost the lock. Someone else is the leader, try again.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
package leadership
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
kv "github.com/docker/swarm/pkg/store"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCandidate(t *testing.T) {
|
||||||
|
store, err := kv.NewStore("mock", []string{}, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
mockStore := store.(*kv.Mock)
|
||||||
|
mockLock := &kv.MockLock{}
|
||||||
|
mockStore.On("NewLock", "test_key", mock.Anything).Return(mockLock, nil)
|
||||||
|
|
||||||
|
// Lock and unlock always succeeds.
|
||||||
|
lostCh := make(chan struct{})
|
||||||
|
var mockLostCh <-chan struct{} = lostCh
|
||||||
|
mockLock.On("Lock").Return(mockLostCh, nil)
|
||||||
|
mockLock.On("Unlock").Return(nil)
|
||||||
|
|
||||||
|
candidate := NewCandidate(store, "test_key", "test_node")
|
||||||
|
candidate.RunForElection()
|
||||||
|
|
||||||
|
// Should issue a false upon start, no matter what.
|
||||||
|
assert.False(t, <-candidate.ElectedCh)
|
||||||
|
|
||||||
|
// Since the lock always succeeeds, we should get elected.
|
||||||
|
assert.True(t, <-candidate.ElectedCh)
|
||||||
|
|
||||||
|
// Signaling a lost lock should get us de-elected...
|
||||||
|
close(lostCh)
|
||||||
|
assert.False(t, <-candidate.ElectedCh)
|
||||||
|
|
||||||
|
// And we should attempt to get re-elected again.
|
||||||
|
assert.True(t, <-candidate.ElectedCh)
|
||||||
|
|
||||||
|
// When we resign, unlock will get called, we'll be notified of the
|
||||||
|
// de-election and we'll try to get the lock again.
|
||||||
|
go candidate.Resign()
|
||||||
|
assert.False(t, <-candidate.ElectedCh)
|
||||||
|
assert.True(t, <-candidate.ElectedCh)
|
||||||
|
|
||||||
|
// After stopping the candidate, the ElectedCh should be closed.
|
||||||
|
candidate.Stop()
|
||||||
|
select {
|
||||||
|
case <-candidate.ElectedCh:
|
||||||
|
assert.True(t, false) // we should not get here.
|
||||||
|
default:
|
||||||
|
assert.True(t, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
mockStore.AssertExpectations(t)
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
package leadership
|
||||||
|
|
||||||
|
import "github.com/docker/swarm/pkg/store"
|
||||||
|
|
||||||
|
// Follower can folow an election in real-time and push notifications whenever
|
||||||
|
// there is a change in leadership.
|
||||||
|
type Follower struct {
|
||||||
|
LeaderCh chan string
|
||||||
|
|
||||||
|
client store.Store
|
||||||
|
key string
|
||||||
|
|
||||||
|
stopCh chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFollower creates a new follower.
|
||||||
|
func NewFollower(client store.Store, key string) *Follower {
|
||||||
|
return &Follower{
|
||||||
|
LeaderCh: make(chan string),
|
||||||
|
client: client,
|
||||||
|
key: key,
|
||||||
|
stopCh: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FollowElection starts monitoring the election. The current leader is updated
|
||||||
|
// in real-time and pushed through `LeaderCh`.
|
||||||
|
func (f *Follower) FollowElection() error {
|
||||||
|
ch, err := f.client.Watch(f.key, f.stopCh)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
go f.follow(ch)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops monitoring an election.
|
||||||
|
func (f *Follower) Stop() {
|
||||||
|
close(f.stopCh)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Follower) follow(<-chan *store.KVPair) {
|
||||||
|
defer close(f.LeaderCh)
|
||||||
|
|
||||||
|
// FIXME: We should pass `RequireConsistent: true` to Consul.
|
||||||
|
ch, err := f.client.Watch(f.key, f.stopCh)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
prev := ""
|
||||||
|
for kv := range ch {
|
||||||
|
curr := string(kv.Value)
|
||||||
|
if curr == prev {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
prev = curr
|
||||||
|
f.LeaderCh <- string(curr)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package leadership
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
kv "github.com/docker/swarm/pkg/store"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFollower(t *testing.T) {
|
||||||
|
store, err := kv.NewStore("mock", []string{}, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
mockStore := store.(*kv.Mock)
|
||||||
|
|
||||||
|
kvCh := make(chan *kv.KVPair)
|
||||||
|
var mockKVCh <-chan *kv.KVPair = kvCh
|
||||||
|
mockStore.On("Watch", "test_key", mock.Anything).Return(mockKVCh, nil)
|
||||||
|
|
||||||
|
follower := NewFollower(store, "test_key")
|
||||||
|
follower.FollowElection()
|
||||||
|
|
||||||
|
// Simulate leader updates
|
||||||
|
go func() {
|
||||||
|
kvCh <- &kv.KVPair{Key: "test_key", Value: []byte("leader1")}
|
||||||
|
kvCh <- &kv.KVPair{Key: "test_key", Value: []byte("leader1")}
|
||||||
|
kvCh <- &kv.KVPair{Key: "test_key", Value: []byte("leader2")}
|
||||||
|
kvCh <- &kv.KVPair{Key: "test_key", Value: []byte("leader1")}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// We shouldn't see duplicate events.
|
||||||
|
assert.Equal(t, <-follower.LeaderCh, "leader1")
|
||||||
|
assert.Equal(t, <-follower.LeaderCh, "leader2")
|
||||||
|
assert.Equal(t, <-follower.LeaderCh, "leader1")
|
||||||
|
|
||||||
|
// Once stopped, iteration over the leader channel should stop.
|
||||||
|
follower.Stop()
|
||||||
|
close(kvCh)
|
||||||
|
assert.Equal(t, "", <-follower.LeaderCh)
|
||||||
|
|
||||||
|
mockStore.AssertExpectations(t)
|
||||||
|
}
|
Loading…
Reference in New Issue