diff --git a/etcdutil/README.md b/etcdutil/README.md index fc71816b..297f7c58 100644 --- a/etcdutil/README.md +++ b/etcdutil/README.md @@ -3,8 +3,8 @@ Use etcd for leader election if you have several instances of a service running and you only want one of the service instances to preform a task. `LeaderElection` starts a goroutine which performs an election and maintains a leader -while services join and leave the election. Calling `Stop()` will `Concede()` leadership if -the service currently has it. +while candidates join and leave the election. Calling `Close()` will concede leadership if +the service currently has it and will withdraw the candidate from the election. ```go @@ -16,33 +16,29 @@ import ( func main() { var wg holster.WaitGroup - hostname, err := os.Hostname() - if err != nil { - fmt.Fprintf(os.Stderr, "while obtaining hostname: %s\n", err) - return - } - client, err := etcdutil.NewClient(nil) if err != nil { fmt.Fprintf(os.Stderr, "while creating etcd client: %s\n", err) return } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() - // Preform an election called 'my-service' with hostname as the candidate name - e := etcdutil.NewElection(client, etcdutil.ElectionConfig{ + // Start a leader election and attempt to become leader, only returns after + // determining the current leader. + election := etcdutil.NewElection(ctx, client, etcdutil.ElectionConfig{ Election: "my-service", - Candidate: hostname, - LeaderChannelSize: 10, - ResumeLeaderOnReconnect: true, + Candidate: "my-candidate", + EventObserver: func(e etcdutil.Event) { + leaderChan <- e + if e.IsDone { + close(leaderChan) + } + }, TTL: 10, }) - // Start the election, will block until a leader is elected - if err = e.Start(); err != nil { - fmt.Printf("during election start: %s\n", err) - os.Exit(1) - } - // Handle graceful shutdown signalChan := make(chan os.Signal, 1) signal.Notify(signalChan, os.Interrupt, os.Kill) @@ -56,7 +52,8 @@ func main() { if election.IsLeader() { err := DoThing() if err != nil { - // Have another instance DoThing(), we can't for some reason + // Have another instance run DoThing() + // since we can't for some reason. election.Concede() } } @@ -68,9 +65,9 @@ func main() { }) wg.Wait() - // Or you can listen on a channel for leadership updates - for leader := range e.LeaderChan() { - fmt.Printf("Leader: %t\n", leader) + // Or you can pipe events to a channel + for leader := range leaderChan { + fmt.Printf("Leader: %v\n", leader) } } ``` diff --git a/etcdutil/election.go b/etcdutil/election.go index 8adb71c6..8b1951a2 100644 --- a/etcdutil/election.go +++ b/etcdutil/election.go @@ -24,10 +24,18 @@ type LeaderElector interface { } type Event struct { - IsLeader bool - LeaderKey string + // True if our candidate is leader + IsLeader bool + // True if the election is shutdown and + // no further events will follow. + IsDone bool + // Holds the current leader key + LeaderKey string + // Hold the current leaders data LeaderData string - Err error + // If not nil, contains an error encountered + // while participating in the election. + Err error } type EventObserver func(Event) @@ -360,6 +368,8 @@ func (e *Election) onLeaderChange(kv *mvccpb.KeyValue) { } event.LeaderKey = string(kv.Key) event.LeaderData = string(kv.Value) + } else { + event.IsDone = true } for _, v := range e.observers { diff --git a/etcdutil/election_test.go b/etcdutil/election_test.go index 450a35c1..2e234666 100644 --- a/etcdutil/election_test.go +++ b/etcdutil/election_test.go @@ -48,13 +48,13 @@ func TestTwoCampaigns(t *testing.T) { }) require.Nil(t, err) - c2Chan := make(chan bool, 5) + c2Chan := make(chan etcdutil.Event, 5) c2, err := etcdutil.NewElection(ctx, client, etcdutil.ElectionConfig{ EventObserver: func(e etcdutil.Event) { if err != nil { t.Fatal(err.Error()) } - c2Chan <- e.IsLeader + c2Chan <- e }, Election: "/my-election", Candidate: "c2", @@ -69,9 +69,14 @@ func TestTwoCampaigns(t *testing.T) { assert.Equal(t, false, c1.IsLeader()) // Second campaign should become leader - assert.Equal(t, false, <-c2Chan) - assert.Equal(t, true, <-c2Chan) + e := <-c2Chan + assert.Equal(t, false, e.IsLeader) + e = <-c2Chan + assert.Equal(t, true, e.IsLeader) + assert.Equal(t, false, e.IsDone) c2.Close() - assert.Equal(t, false, <-c2Chan) + e = <-c2Chan + assert.Equal(t, false, e.IsLeader) + assert.Equal(t, true, e.IsDone) }