mesh/merge-gate head 98e856874913 merged into trunk c975dbf7b053 — pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus…
mesh/repo-check head 98e856874913 merged into trunk c975dbf7b053 — pass: its merge-check.sh passed
mesh/delivery delivered
A bus stall of a few seconds made a renewal time out, and the controller called that a lost lease, exited and raised the urgent lease-lost alert, though no other controller could take a key that still lived. Now only another holder seen, or the key's TTL passed unrenewed, loses the lease; new acts stop at half the TTL unrenewed and resume on the next renewal (novox/hq issue 477).
626 lines
24 KiB
Go
626 lines
24 KiB
Go
// Package lease is the controller's lease (novox/hq to-be 45 §6, ADR 0227 rule 1): the one key that
|
|
// says which controller instance may act, and the epoch every act of it carries.
|
|
//
|
|
// **A controller instance acts only while it holds the key `holder`** in its lease bucket — sends a
|
|
// declaration, writes a plan, a condition or a call. The key is written by compare-and-set, lives a
|
|
// bucket's age (fifteen seconds) unless renewed, and is renewed every five. The **epoch** is the
|
|
// bucket's revision at which the instance took it: a later holder's is higher, so a machine that has
|
|
// heard from epoch 57 can refuse anything still arriving from 41 (issue 204).
|
|
//
|
|
// controller A (epoch 41) ──renew──renew──╳ (renewal refused)──► stops acting, exits
|
|
// controller B ──wait──────────────take (epoch 57)──► acts
|
|
//
|
|
// **A renewal that fails is not a lost lease** (novox/hq issue 477, the operator's standing rule). A
|
|
// renewal the bus did not answer in time — a stall of its JetStream API — says nothing about another
|
|
// holder: none can take the key while it lives. So the instance keeps the lease it holds until the key's
|
|
// own age has passed without a successful renewal, and tries again every renewal interval:
|
|
//
|
|
// renewed ──╳ (no answer)──╳──────────── renewed ──► acts on, as before
|
|
// 0 ½ age: no new act starts (the gate opens again)
|
|
// renewed ──╳──────╳──────╳──────╳ age ──► lost, exits
|
|
//
|
|
// **Two ways an instance loses the lease, said and final**: Lost closes and the process exits, so its
|
|
// service manager restarts it as a candidate. Another holder seen — the renewal refused because the
|
|
// key moved, and the key naming somebody else or gone — is a loss at once. The key's age passed with no
|
|
// successful renewal is a loss too, because past it the key may be somebody else's. And Epoch, the gate
|
|
// every act passes, answers an error from the moment the last renewal is older than half the key's age
|
|
// (or its age less a margin, if that is sooner) — **long before** another instance could have taken it,
|
|
// whatever the renewing goroutine is doing — so a send in flight while the bus stalls is refused by the
|
|
// clock, not by a goroutine that may be stuck, and the gate opens again with the next renewal made.
|
|
package lease
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go/jetstream"
|
|
)
|
|
|
|
// Key is the one key of the lease bucket.
|
|
const Key = "holder"
|
|
|
|
// Holder is who holds the lease, as the key says it.
|
|
type Holder struct {
|
|
// Instance names one controller process: its machine, its process and when it started. Two
|
|
// instances on one machine — the handover of issue 213 — are two names.
|
|
Instance string `json:"instance"`
|
|
Host string `json:"host,omitempty"`
|
|
Build string `json:"build,omitempty"`
|
|
// Epoch is the revision the key was taken at. Zero in the value written to take it, because the
|
|
// revision is known only once written; every renewal says it, and Current reads the revision of a
|
|
// key never renewed.
|
|
Epoch uint64 `json:"epoch,omitempty"`
|
|
Taken time.Time `json:"taken"`
|
|
Renewed time.Time `json:"renewed"`
|
|
// Health is the holder's word about itself, written with every renewal (witness.go): nil from a
|
|
// process that says none, which the controller's gate reads as not ready. The host ignores it.
|
|
Health *Health `json:"health,omitempty"`
|
|
}
|
|
|
|
// Defaults, as to-be 45 §6 sets them. The age is the bucket's, read from it (Open), so the bucket
|
|
// and the lease cannot disagree about it.
|
|
const (
|
|
RenewEvery = 5 * time.Second
|
|
// Margin is how long before the key could expire this instance stops acting on it: the gap
|
|
// between its own clock and the bus's, and a send already on its way.
|
|
Margin = 3 * time.Second
|
|
// Poll is how often a candidate looks again at a key somebody else holds.
|
|
Poll = time.Second
|
|
)
|
|
|
|
// ErrNotHeld is an act asked of an instance that does not hold the lease.
|
|
var ErrNotHeld = errors.New("this controller does not hold the lease")
|
|
|
|
// ErrUnwritable is a lease nobody holds that the bus would not let this instance write: the one case a
|
|
// caller may serve without it, since nothing else holds it either. Every other failure to take it —
|
|
// the holder unreadable, the floor unreadable — says nothing about whether another instance acts.
|
|
var ErrUnwritable = errors.New("nobody holds the lease, and the bus would not let this controller write it")
|
|
|
|
// Options are what a Lease is made with.
|
|
type Options struct {
|
|
Holder Holder
|
|
// RenewEvery, Margin and Poll default to the package's.
|
|
RenewEvery, Margin, Poll time.Duration
|
|
// RenewWithin is how long one renewal may take before it counts as failed: RenewEvery when zero. Apart
|
|
// from it so a test that renews often still gives a slow bus a serving controller's time to answer
|
|
// (novox/hq issue 591).
|
|
RenewWithin time.Duration
|
|
// Floor is the highest epoch this mesh has issued, from the controller's own store. **An epoch is
|
|
// never issued twice**: a lease bucket raised again from nothing — a bus whose data was replaced —
|
|
// starts its revisions over, and every machine that heard epoch 57 would refuse the next controller
|
|
// for ever. Below the floor, the bucket's revisions are moved past it before the key is taken.
|
|
// Nil is no floor.
|
|
Floor func(ctx context.Context) (uint64, error)
|
|
// Say is where what the lease does is said: waiting, taking, losing.
|
|
Say func(format string, args ...any)
|
|
// Moved is told when the bucket's revisions were moved past the floor: the bucket was raised again
|
|
// from nothing, which the caller says as a condition. Nil tells nobody.
|
|
Moved func(was, floor uint64)
|
|
// Now is the clock; nil is time.Now.
|
|
Now func() time.Time
|
|
// Health is asked at every take and renewal for the holder's word about itself (witness.go); nil
|
|
// writes none.
|
|
Health func() *Health
|
|
// Delayed is told of every renewal that failed without losing the lease — the bus did not answer
|
|
// in time — with why, when the lease was last renewed, and when it is lost unless a renewal is made
|
|
// first. It is where a bus stall is said as one (novox/hq issue 478); nil tells nobody. It is called
|
|
// on the renewing goroutine and must not block.
|
|
Delayed func(why error, renewed, lostBy time.Time)
|
|
}
|
|
|
|
// Lease is one instance's hold, or its wait for one.
|
|
type Lease struct {
|
|
kv jetstream.KeyValue
|
|
stream jetstream.Stream
|
|
ttl time.Duration
|
|
o Options
|
|
|
|
mu sync.Mutex
|
|
// held is whether this instance holds the key; epoch the revision it took it at; revision the
|
|
// revision of its last write, which the next renewal must find.
|
|
held bool
|
|
epoch uint64
|
|
revision uint64
|
|
// validUntil is when this instance stops starting acts unless it renews: the moment before its last
|
|
// renewal was sent, plus half the key's age (or the age less the margin, if sooner).
|
|
validUntil time.Time
|
|
// keptUntil is when this instance loses the lease unless it renews: the moment before its last
|
|
// renewal was sent, plus the key's age (novox/hq issue 477).
|
|
keptUntil time.Time
|
|
// renewed is when this instance last took or renewed the key.
|
|
renewed time.Time
|
|
// failing is how many renewals failed since the last one made, and failingSince when the first did.
|
|
failing int
|
|
failingSince time.Time
|
|
lostWhy error
|
|
lost chan struct{}
|
|
taken time.Time
|
|
// update writes the key at a revision: the bucket's own, or a test's bus that does not answer.
|
|
update func(ctx context.Context, key string, value []byte, last uint64) (uint64, error)
|
|
}
|
|
|
|
// Open is a lease over the bucket, read for its age. The bucket is the caller's to assert.
|
|
func Open(ctx context.Context, js jetstream.JetStream, bucket string, o Options) (*Lease, error) {
|
|
kv, err := js.KeyValue(ctx, bucket)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the lease bucket %s is not on the bus: %w", bucket, err)
|
|
}
|
|
status, err := kv.Status(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the lease bucket %s cannot be read: %w", bucket, err)
|
|
}
|
|
if status.TTL() <= 0 {
|
|
// A key that never expires is a lease a dead controller holds for ever.
|
|
return nil, fmt.Errorf("the lease bucket %s keeps its keys for ever, so a controller that died "+
|
|
"holding it would hold it for ever: it must have an age", bucket)
|
|
}
|
|
stream, err := js.Stream(ctx, "KV_"+bucket)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the stream under the lease bucket %s cannot be read: %w", bucket, err)
|
|
}
|
|
if o.RenewEvery <= 0 {
|
|
o.RenewEvery = RenewEvery
|
|
}
|
|
if o.RenewWithin <= 0 {
|
|
o.RenewWithin = o.RenewEvery
|
|
}
|
|
if o.Margin <= 0 {
|
|
o.Margin = Margin
|
|
}
|
|
if o.Poll <= 0 {
|
|
o.Poll = Poll
|
|
}
|
|
if o.Say == nil {
|
|
o.Say = func(string, ...any) {}
|
|
}
|
|
if o.Now == nil {
|
|
o.Now = time.Now
|
|
}
|
|
if o.RenewEvery+o.Margin >= status.TTL() {
|
|
return nil, fmt.Errorf("a lease renewed every %s with a margin of %s does not fit a key that lives %s",
|
|
o.RenewEvery, o.Margin, status.TTL())
|
|
}
|
|
return &Lease{kv: kv, stream: stream, ttl: status.TTL(), o: o, lost: make(chan struct{}), update: kv.Update}, nil
|
|
}
|
|
|
|
// TTL is how long the key lives unrenewed.
|
|
func (l *Lease) TTL() time.Duration { return l.ttl }
|
|
|
|
// Current is who holds the lease now, and false when nobody does. An error is an error: never "nobody".
|
|
func Current(ctx context.Context, kv jetstream.KeyValue) (Holder, bool, error) {
|
|
entry, err := kv.Get(ctx, Key)
|
|
if errors.Is(err, jetstream.ErrKeyNotFound) {
|
|
return Holder{}, false, nil
|
|
}
|
|
if err != nil {
|
|
return Holder{}, false, err
|
|
}
|
|
var h Holder
|
|
if err := json.Unmarshal(entry.Value(), &h); err != nil {
|
|
return Holder{}, false, fmt.Errorf("the lease's key holds something that is not a holder: %w", err)
|
|
}
|
|
if h.Epoch == 0 {
|
|
// Taken and not yet renewed: the revision it was taken at is the one it has.
|
|
h.Epoch = entry.Revision()
|
|
}
|
|
return h, true, nil
|
|
}
|
|
|
|
// Current is who holds this lease now.
|
|
func (l *Lease) Current(ctx context.Context) (Holder, bool, error) { return Current(ctx, l.kv) }
|
|
|
|
// ErrTaken is a take that found the key held by somebody else; Take waits on it, TryTake answers it.
|
|
var ErrTaken = errors.New("another controller holds the lease")
|
|
|
|
// Take waits until the key is absent or expired and takes it, answering the epoch. A key somebody
|
|
// else holds is waited on and said once; an error reading or writing the bucket is answered, because
|
|
// a candidate that cannot see the key cannot tell whether it may act.
|
|
func (l *Lease) Take(ctx context.Context) (uint64, error) {
|
|
said := ""
|
|
for {
|
|
epoch, err := l.TryTake(ctx)
|
|
if err == nil {
|
|
return epoch, nil
|
|
}
|
|
var held *heldBy
|
|
if !errors.As(err, &held) {
|
|
return 0, err
|
|
}
|
|
if held.h.Instance != said {
|
|
l.o.Say("another controller holds the lease (%s, epoch %d, taken %s); waiting until it lets go "+
|
|
"or stops renewing", held.h.Instance, held.h.Epoch, held.h.Taken.UTC().Format(time.RFC3339))
|
|
said = held.h.Instance
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return 0, ctx.Err()
|
|
case <-time.After(l.o.Poll):
|
|
}
|
|
}
|
|
}
|
|
|
|
// heldBy is ErrTaken naming the holder.
|
|
type heldBy struct{ h Holder }
|
|
|
|
func (e *heldBy) Error() string {
|
|
return fmt.Sprintf("%v: %s, epoch %d", ErrTaken, e.h.Instance, e.h.Epoch)
|
|
}
|
|
func (e *heldBy) Unwrap() error { return ErrTaken }
|
|
|
|
// TryTake takes the key if nobody holds it, once, without waiting: the epoch, or ErrTaken naming the
|
|
// holder, or what the bus said.
|
|
func (l *Lease) TryTake(ctx context.Context) (uint64, error) {
|
|
l.mu.Lock()
|
|
switch {
|
|
case l.lostWhy != nil:
|
|
// A lease lost is not taken again by the instance that lost it: that instance exits, and its
|
|
// successor is a new candidate with a new name.
|
|
defer l.mu.Unlock()
|
|
return 0, fmt.Errorf("%w: it lost it — %v", ErrNotHeld, l.lostWhy)
|
|
case l.held:
|
|
defer l.mu.Unlock()
|
|
return l.epoch, nil
|
|
}
|
|
l.mu.Unlock()
|
|
if h, found, err := l.Current(ctx); err != nil {
|
|
return 0, fmt.Errorf("who holds the lease cannot be read: %w", err)
|
|
} else if found {
|
|
return 0, &heldBy{h}
|
|
}
|
|
if err := l.aboveTheFloor(ctx); err != nil {
|
|
return 0, err
|
|
}
|
|
now := l.o.Now()
|
|
h := l.o.Holder
|
|
h.Taken, h.Renewed, h.Epoch = now, now, 0
|
|
if l.o.Health != nil {
|
|
h.Health = l.o.Health()
|
|
}
|
|
value, err := json.Marshal(h)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
anchor := l.o.Now()
|
|
revision, err := l.kv.Create(ctx, Key, value)
|
|
if errors.Is(err, jetstream.ErrKeyExists) {
|
|
// Somebody took it between the read and the write: theirs.
|
|
if h, found, rerr := l.Current(ctx); rerr == nil && found {
|
|
return 0, &heldBy{h}
|
|
}
|
|
return 0, ErrTaken
|
|
}
|
|
if err != nil {
|
|
return 0, fmt.Errorf("%w: %w", ErrUnwritable, err)
|
|
}
|
|
if floor, err := l.floor(ctx); err != nil {
|
|
_ = l.kv.Delete(ctx, Key, jetstream.LastRevision(revision))
|
|
return 0, err
|
|
} else if revision <= floor {
|
|
// The bucket moved under the floor between the check and the write (another candidate raising
|
|
// it again): never an epoch already issued. Given back, and taken again on the next try.
|
|
_ = l.kv.Delete(ctx, Key, jetstream.LastRevision(revision))
|
|
return 0, fmt.Errorf("the lease was taken at revision %d, not above the highest epoch issued (%d); given "+
|
|
"back to be taken again", revision, floor)
|
|
}
|
|
l.mu.Lock()
|
|
l.held, l.epoch, l.revision, l.taken = true, revision, revision, now
|
|
l.renewedAt(anchor)
|
|
l.mu.Unlock()
|
|
l.o.Say("took the controller lease at epoch %d (%s)", revision, h.Instance)
|
|
return revision, nil
|
|
}
|
|
|
|
// floor is the highest epoch issued, zero without a floor.
|
|
func (l *Lease) floor(ctx context.Context) (uint64, error) {
|
|
if l.o.Floor == nil {
|
|
return 0, nil
|
|
}
|
|
floor, err := l.o.Floor(ctx)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("the highest epoch this mesh has issued cannot be read, so no epoch can be "+
|
|
"issued that is surely higher: %w", err)
|
|
}
|
|
return floor, nil
|
|
}
|
|
|
|
// aboveTheFloor moves the bucket's revisions past the highest epoch issued, when a bucket raised again
|
|
// from nothing is behind it. Only while nobody holds the key: the stream is compacted to the floor, which
|
|
// sets the next revision above it.
|
|
func (l *Lease) aboveTheFloor(ctx context.Context) error {
|
|
floor, err := l.floor(ctx)
|
|
if err != nil || floor == 0 {
|
|
return err
|
|
}
|
|
info, err := l.stream.Info(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("the lease bucket's revisions cannot be read: %w", err)
|
|
}
|
|
if info.State.LastSeq > floor {
|
|
return nil
|
|
}
|
|
l.o.Say("the lease bucket is at revision %d and this mesh has issued epoch %d: the bucket was raised "+
|
|
"again from nothing, so its revisions are moved past %d before the lease is taken", info.State.LastSeq,
|
|
floor, floor)
|
|
if err := l.stream.Purge(ctx, jetstream.WithPurgeSequence(floor+1)); err != nil {
|
|
return fmt.Errorf("the lease bucket's revisions could not be moved past epoch %d: %w", floor, err)
|
|
}
|
|
if l.o.Moved != nil {
|
|
l.o.Moved(info.State.LastSeq, floor)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// renewedAt records a take or renewal sent at anchor: the gate open until half the key's age (or its age
|
|
// less the margin, if sooner), the lease kept until its age. Called with mu held.
|
|
func (l *Lease) renewedAt(anchor time.Time) {
|
|
open := l.ttl / 2
|
|
if l.ttl-l.o.Margin < open {
|
|
open = l.ttl - l.o.Margin
|
|
}
|
|
l.renewed, l.validUntil, l.keptUntil = anchor, anchor.Add(open), anchor.Add(l.ttl)
|
|
l.failing, l.failingSince = 0, time.Time{}
|
|
}
|
|
|
|
// Epoch is the gate every act passes: this instance's epoch while it holds the lease and its last
|
|
// renewal is in time, and an error otherwise — not held, lost, or a renewal overdue by half the key's
|
|
// age, which a renewal made opens again (novox/hq issue 477).
|
|
func (l *Lease) Epoch() (uint64, error) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
switch {
|
|
case l.lostWhy != nil:
|
|
return 0, fmt.Errorf("%w: it lost it — %v", ErrNotHeld, l.lostWhy)
|
|
case !l.held:
|
|
return 0, ErrNotHeld
|
|
case l.o.Now().After(l.validUntil):
|
|
return 0, fmt.Errorf("%w in time: no renewal was made since %s, so no new act starts until one is "+
|
|
"(the lease is kept until %s)", ErrNotHeld, l.renewed.UTC().Format(time.RFC3339),
|
|
l.keptUntil.UTC().Format(time.RFC3339))
|
|
}
|
|
return l.epoch, nil
|
|
}
|
|
|
|
// Renewed is when this instance last took or renewed the key; zero before it took it.
|
|
func (l *Lease) Renewed() time.Time {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
return l.renewed
|
|
}
|
|
|
|
// Held says this instance holds the lease now, as Epoch's gate does.
|
|
func (l *Lease) Held() bool {
|
|
_, err := l.Epoch()
|
|
return err == nil
|
|
}
|
|
|
|
// Lost is closed when this instance loses the lease it held. A caller exits on it.
|
|
func (l *Lease) Lost() <-chan struct{} { return l.lost }
|
|
|
|
// LostWhy is why it was lost, nil while it was not.
|
|
func (l *Lease) LostWhy() error {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
return l.lostWhy
|
|
}
|
|
|
|
// Keep renews the lease until ctx ends or it is lost. A renewal that fails without losing it is tried
|
|
// again at the next interval, and the lease is lost when its key's age passes with none made, even while
|
|
// a renewal hangs (novox/hq issue 477).
|
|
func (l *Lease) Keep(ctx context.Context) {
|
|
tick := time.NewTicker(l.o.RenewEvery)
|
|
defer tick.Stop()
|
|
for {
|
|
var expire <-chan time.Time
|
|
l.mu.Lock()
|
|
if l.failing > 0 {
|
|
expire = time.After(l.keptUntil.Sub(l.o.Now()))
|
|
}
|
|
l.mu.Unlock()
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-tick.C:
|
|
case <-expire:
|
|
}
|
|
_ = l.Renew(ctx)
|
|
if ctx.Err() != nil || l.LostWhy() != nil {
|
|
return // lost, said by lose; or stopping
|
|
}
|
|
}
|
|
}
|
|
|
|
// ErrDelayed is a renewal that failed without losing the lease: the bus did not answer in time, and the
|
|
// key's age has not passed since the last renewal made (novox/hq issue 477).
|
|
var ErrDelayed = errors.New("the lease's renewal is delayed")
|
|
|
|
// Renew writes the key once more at the revision this instance last wrote. Another holder seen — the
|
|
// write refused and the key naming somebody else, or gone — is the lease lost at once. A renewal that
|
|
// cannot be made — the bus does not answer in time — is not (novox/hq issue 477): it answers ErrDelayed,
|
|
// the lease is kept, and the gate closes only once half the key's age has passed without a renewal made.
|
|
// The lease is lost when the whole age has: past it the key may be somebody else's.
|
|
func (l *Lease) Renew(ctx context.Context) error {
|
|
l.mu.Lock()
|
|
if !l.held || l.lostWhy != nil {
|
|
defer l.mu.Unlock()
|
|
if l.lostWhy != nil {
|
|
return l.lostWhy
|
|
}
|
|
return ErrNotHeld
|
|
}
|
|
revision, epoch, taken, keptUntil := l.revision, l.epoch, l.taken, l.keptUntil
|
|
l.mu.Unlock()
|
|
|
|
anchor := l.o.Now()
|
|
if !anchor.Before(keptUntil) {
|
|
return l.lose(l.overdue(nil))
|
|
}
|
|
h := l.o.Holder
|
|
h.Epoch, h.Taken = epoch, taken
|
|
h.Renewed = anchor
|
|
if l.o.Health != nil {
|
|
h.Health = l.o.Health()
|
|
}
|
|
value, err := json.Marshal(h)
|
|
if err != nil {
|
|
return l.lose(err)
|
|
}
|
|
// One renewal waits no longer than its own bound, nor past the moment the lease is lost without it.
|
|
within := l.o.RenewWithin
|
|
if left := keptUntil.Sub(anchor); left < within {
|
|
within = left
|
|
}
|
|
renewing, cancel := context.WithTimeout(ctx, within)
|
|
defer cancel()
|
|
next, err := l.update(renewing, Key, value, revision)
|
|
if err != nil && ctx.Err() == nil && (errors.Is(err, jetstream.ErrKeyExists) || isWrongLast(err)) {
|
|
// The key moved. A renewal whose answer the stall swallowed may have been written all the same:
|
|
// the key still naming this instance at this epoch is that one, and is renewed at the revision it
|
|
// has. Anything else is another holder — or the key gone, free for one — and the lease lost.
|
|
var why error
|
|
next, why, err = l.renewMoved(renewing, epoch, value, revision, err)
|
|
if why != nil {
|
|
return l.lose(why)
|
|
}
|
|
}
|
|
if err != nil {
|
|
if ctx.Err() != nil {
|
|
// Stopping, not losing: Release gives it back.
|
|
return ctx.Err()
|
|
}
|
|
return l.delayed(fmt.Errorf("the renewal could not be made: %w", err))
|
|
}
|
|
l.mu.Lock()
|
|
failed, since := l.failing, l.failingSince
|
|
l.revision = next
|
|
l.renewedAt(anchor)
|
|
l.mu.Unlock()
|
|
if failed > 0 {
|
|
l.o.Say("renewed the controller lease (epoch %d) after %d renewal(s) that could not be made since %s: "+
|
|
"this controller acts again", epoch, failed, since.UTC().Format(time.RFC3339))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// renewMoved is a renewal the bus refused because the key moved: the renewal made at the key's revision
|
|
// when the key is still this instance's at this epoch, the lease lost (why) when it is somebody else's or
|
|
// gone, and an error when the key cannot be read — a renewal not made, which loses nothing yet.
|
|
func (l *Lease) renewMoved(ctx context.Context, epoch uint64, value []byte, revision uint64,
|
|
refused error) (next uint64, lost error, err error) {
|
|
entry, err := l.kv.Get(ctx, Key)
|
|
if errors.Is(err, jetstream.ErrKeyNotFound) {
|
|
return 0, fmt.Errorf("the renewal at revision %d was refused and the key is gone, free for another "+
|
|
"controller to take: %w", revision, refused), nil
|
|
}
|
|
if err != nil {
|
|
return 0, nil, fmt.Errorf("the renewal at revision %d was refused (%v) and the key could not be read: %w",
|
|
revision, refused, err)
|
|
}
|
|
var h Holder
|
|
mine := json.Unmarshal(entry.Value(), &h) == nil && h.Instance == l.o.Holder.Instance &&
|
|
(h.Epoch == epoch || h.Epoch == 0 && entry.Revision() == epoch)
|
|
if !mine {
|
|
return 0, fmt.Errorf("the renewal at revision %d was refused: another holder wrote the key (%s, epoch %d, "+
|
|
"revision %d): %w", revision, h.Instance, h.Epoch, entry.Revision(), refused), nil
|
|
}
|
|
next, err = l.update(ctx, Key, value, entry.Revision())
|
|
return next, nil, err
|
|
}
|
|
|
|
// delayed is a renewal that could not be made: said at the first of a run, told to Delayed, and the lease
|
|
// kept — or lost, when its key's age has passed meanwhile.
|
|
func (l *Lease) delayed(why error) error {
|
|
l.mu.Lock()
|
|
now := l.o.Now()
|
|
if !now.Before(l.keptUntil) {
|
|
l.mu.Unlock()
|
|
return l.lose(l.overdue(why))
|
|
}
|
|
l.failing++
|
|
if l.failing == 1 {
|
|
l.failingSince = now
|
|
}
|
|
first, epoch, renewed, validUntil, keptUntil := l.failing == 1, l.epoch, l.renewed, l.validUntil, l.keptUntil
|
|
l.mu.Unlock()
|
|
if first {
|
|
l.o.Say("the controller lease (epoch %d) could not be renewed: %v — it is kept: no new act starts after "+
|
|
"%s and it is lost at %s unless a renewal is made first", epoch, why,
|
|
validUntil.UTC().Format(time.RFC3339), keptUntil.UTC().Format(time.RFC3339))
|
|
}
|
|
if l.o.Delayed != nil {
|
|
l.o.Delayed(why, renewed, keptUntil)
|
|
}
|
|
return fmt.Errorf("%w: %w", ErrDelayed, why)
|
|
}
|
|
|
|
// overdue is why a lease whose key's age passed unrenewed is lost, with the last renewal's failure.
|
|
func (l *Lease) overdue(last error) error {
|
|
l.mu.Lock()
|
|
renewed, failing := l.renewed, l.failing
|
|
l.mu.Unlock()
|
|
why := fmt.Errorf("no renewal was made within the key's age of %s (the last at %s, %d failed since)",
|
|
l.ttl, renewed.UTC().Format(time.RFC3339), failing)
|
|
if last != nil {
|
|
why = fmt.Errorf("%w: %w", why, last)
|
|
}
|
|
return why
|
|
}
|
|
|
|
// isWrongLast is the bus refusing a compare-and-set because the key moved.
|
|
func isWrongLast(err error) bool {
|
|
var apiErr *jetstream.APIError
|
|
return errors.As(err, &apiErr) && apiErr.ErrorCode == jetstream.JSErrCodeStreamWrongLastSequence
|
|
}
|
|
|
|
// lose marks the lease lost, once, and says why.
|
|
func (l *Lease) lose(why error) error {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
if l.lostWhy == nil {
|
|
l.lostWhy = why
|
|
l.held = false
|
|
close(l.lost)
|
|
l.o.Say("LOST the controller lease (epoch %d): %v — this controller stops acting at once", l.epoch, why)
|
|
}
|
|
return l.lostWhy
|
|
}
|
|
|
|
// Release gives the lease back, so the next candidate takes it at once rather than after its age.
|
|
// Only the key this instance last wrote is deleted: one that moved is somebody else's.
|
|
func (l *Lease) Release(ctx context.Context) {
|
|
l.mu.Lock()
|
|
held, revision, epoch := l.held && l.lostWhy == nil, l.revision, l.epoch
|
|
l.held = false
|
|
if l.lostWhy == nil {
|
|
l.lostWhy = errors.New("given back")
|
|
close(l.lost)
|
|
}
|
|
l.mu.Unlock()
|
|
if !held {
|
|
return
|
|
}
|
|
releasing, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
|
|
defer cancel()
|
|
err := l.kv.Delete(releasing, Key, jetstream.LastRevision(revision))
|
|
if err != nil && isWrongLast(err) {
|
|
// A renewal was in flight as this began and landed first: the key moved, and is still this
|
|
// instance's if it still names it at this epoch. Deleted at the revision it has now.
|
|
if entry, gerr := l.kv.Get(releasing, Key); gerr == nil {
|
|
var h Holder
|
|
if json.Unmarshal(entry.Value(), &h) == nil && h.Instance == l.o.Holder.Instance &&
|
|
(h.Epoch == epoch || h.Epoch == 0 && entry.Revision() == epoch) {
|
|
err = l.kv.Delete(releasing, Key, jetstream.LastRevision(entry.Revision()))
|
|
}
|
|
}
|
|
}
|
|
if err != nil {
|
|
l.o.Say("the controller lease (epoch %d) could not be given back, so the next controller waits for "+
|
|
"it to expire: %v", epoch, err)
|
|
return
|
|
}
|
|
l.o.Say("gave the controller lease back (epoch %d)", epoch)
|
|
}
|