// Copyright 2017 Google LLC. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package etcd provides an implementation of master election based on etcd.
package etcd
import (
"context"
"fmt"
"strings"
"github.com/google/trillian/util/election2"
clientv3 "go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/client/v3/concurrency"
"k8s.io/klog/v2"
)
const resignID = "<resign>"
// Election is an implementation of election2.Election based on etcd.
type Election struct {
resourceID string
instanceID string
lockFile string
client *clientv3.Client
session *concurrency.Session
election *concurrency.Election
}
// Await blocks until the instance captures mastership.
func (e *Election) Await(ctx context.Context) error {
return e.election.Campaign(ctx, e.instanceID)
}
// WithMastership returns a "mastership context" which remains active until the
// instance stops being the master, or the passed in context is canceled.
func (e *Election) WithMastership(ctx context.Context) (context.Context, error) {
// Get a channel for notifications of election status (using the cancelable
// context so that the monitoring goroutine below and the goroutine started
// by WithMastership will reliably terminate).
cctx, cancel := context.WithCancel(ctx)
etcdRev := e.election.Rev() // The revision at which e became the master.
if etcdRev == 0 {
// Not even tried to become the master. Return a canceled context.
cancel()
return cctx, nil
}
// Was the master once, so watch for latest mastership updates.
ch := e.election.Observe(cctx)
// Verify that we are still the master, before returning context.
select {
case <-ctx.Done():
cancel()
return nil, ctx.Err()
case rsp, ok := <-ch:
kv := rsp.Kvs[0]
if !ok || kv.CreateRevision != etcdRev || string(kv.Value) == resignID {
// Mastership has been overtaken, released, or not capturead at all.
cancel()
return cctx, nil
}
}
// At this point we have observed confirmation that we are the master; start
// a goroutine to monitor for anyone else overtaking us.
go func() {
defer func() {
cancel()
klog.Infof("%s: canceled mastership context", e.resourceID)
}()
for rsp := range ch {
kv := rsp.Kvs[0]
if kv.CreateRevision != etcdRev {
conquerorID := string(kv.Value)
// TODO(pavelkalinnikov): conquerorID can be resignID too. Serialize a
// protobuf with all mastership details instead of ID string.
klog.Warningf("%s: mastership overtaken by %s", e.resourceID, conquerorID)
break
} else if string(kv.Value) == resignID {
klog.Infof("%s: canceling context due to resignation", e.resourceID)
break
}
}
}()
return cctx, nil
}
// Resign releases mastership for this instance. The instance can be elected
// again using Await. Idempotent, might be useful to retry if fails.
func (e *Election) Resign(ctx context.Context) error {
// Trigger Observe callers to see the update, and cancel mastership contexts.
err := e.election.Proclaim(ctx, resignID)
if err == concurrency.ErrElectionNotLeader {
return nil // Resigning if not master is a no-op.
} else if err != nil {
return err
}
return e.election.Resign(ctx)
}
// Close resigns and permanently stops participating in election. No other
// method should be called after Close.
func (e *Election) Close(ctx context.Context) error {
if err := e.Resign(ctx); err != nil && err != concurrency.ErrElectionNotLeader {
klog.Errorf("%s: Resign(): %v", e.resourceID, err)
}
// Session's Close revokes the underlying lease, which results in removing
// the election-related keys. This achieves the effect of resignation even if
// the above Resign call failed (e.g. due to ctx cancelation).
return e.session.Close()
}
// Factory creates Election instances.
type Factory struct {
client *clientv3.Client
instanceID string
lockDir string
}
// NewFactory builds an election factory that uses the given parameters. The
// passed in etcd client should remain valid for the lifetime of the object.
func NewFactory(instanceID string, client *clientv3.Client, lockDir string) *Factory {
return &Factory{
client: client,
instanceID: instanceID,
lockDir: lockDir,
}
}
// NewElection creates a specific Election instance.
func (f *Factory) NewElection(ctx context.Context, resourceID string) (election2.Election, error) {
// TODO(pavelkalinnikov): Re-create the session if it expires.
// TODO(pavelkalinnikov): Share the same session between Election instances.
session, err := concurrency.NewSession(f.client)
if err != nil {
return nil, fmt.Errorf("failed to create etcd session: %v", err)
}
lockFile := fmt.Sprintf("%s/%s", strings.TrimRight(f.lockDir, "/"), resourceID)
election := concurrency.NewElection(session, lockFile)
el := Election{
resourceID: resourceID,
instanceID: f.instanceID,
lockFile: lockFile,
client: f.client,
session: session,
election: election,
}
klog.Infof("Election created: %+v", el)
return &el, nil
}