M-4: leases and offered grants kept in the bus's own storage
A lease is a key created with a two-minute lifetime, which fails while another holds it: the bus's own compare-and-set keeps one rotation source (ADR 0183 §6). An offered grant's outcome is kept under its fingerprint in the key form for 90 days, every outcome kept. Builds: design 39 bus-and-switch §4, §5, B13 Claude-Session: https://claude.ai/code/session_01NcbgfHb9d4T9nq5Q6WqdiJ
This commit is contained in:
@@ -41,6 +41,8 @@ type BindingState struct {
|
||||
type BusStore struct {
|
||||
LicencesState keptState
|
||||
BindingsState keptState
|
||||
LeasesState keptState
|
||||
OfferedState keptState
|
||||
Now func() time.Time
|
||||
rest
|
||||
}
|
||||
@@ -178,3 +180,72 @@ func (s *BusStore) Advance(ctx context.Context, licence string, m Move) ([]Bindi
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ---- leases and offers (§4, §5) ---------------------------------------------------------------------------------
|
||||
|
||||
// leaseValue is a lease's key in `leases`: its holder and until when.
|
||||
type leaseValue struct {
|
||||
Holder string `json:"holder"`
|
||||
Until string `json:"until"`
|
||||
}
|
||||
|
||||
// Lease takes a lease by creating its key with a lifetime of d (§4): a create fails while another holds it, the bus's
|
||||
// own compare-and-set. A key its own holder holds is taken again.
|
||||
func (s *BusStore) Lease(_ context.Context, key, holder string, d time.Duration) (bool, error) {
|
||||
v := leaseValue{Holder: holder, Until: s.Now().Add(d).UTC().Format(time.RFC3339)}
|
||||
_, err := s.LeasesState.CreateTTL(key, v, d)
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
if !errors.Is(err, stdio.ErrChanged) {
|
||||
return false, err
|
||||
}
|
||||
var held leaseValue
|
||||
if ok, gerr := s.get(s.LeasesState, key, &held); gerr != nil || !ok || held.Holder != holder {
|
||||
return false, gerr
|
||||
}
|
||||
_, err = s.LeasesState.PutTTL(key, v, d)
|
||||
return err == nil, err
|
||||
}
|
||||
|
||||
// Unlease deletes a lease its holder holds; another's is left to it or its lifetime.
|
||||
func (s *BusStore) Unlease(_ context.Context, key, holder string) error {
|
||||
var held leaseValue
|
||||
if ok, err := s.get(s.LeasesState, key, &held); err != nil || !ok || held.Holder != holder {
|
||||
return err
|
||||
}
|
||||
return s.LeasesState.Delete(key)
|
||||
}
|
||||
|
||||
// offeredFor is how long the outcome of an offered grant is kept (§5): longer than a refresh token lives.
|
||||
const offeredFor = 90 * 24 * time.Hour
|
||||
|
||||
// offeredValue is a grant's key in `offered`: what became of it, where and when (§5, B13: every outcome).
|
||||
type offeredValue struct {
|
||||
Node string `json:"node"`
|
||||
Account string `json:"account"`
|
||||
Outcome Outcome `json:"outcome"`
|
||||
Why string `json:"why"`
|
||||
At string `json:"at"`
|
||||
}
|
||||
|
||||
// OfferedKey is a fingerprint's key in `offered`: through the licence key form (B13).
|
||||
func OfferedKey(fingerprint string) string { return KeyFor(fingerprint) }
|
||||
|
||||
func (s *BusStore) Outcome(_ context.Context, fingerprint string) (Outcome, error) {
|
||||
var v offeredValue
|
||||
if ok, err := s.get(s.OfferedState, OfferedKey(fingerprint), &v); err != nil || !ok {
|
||||
return "", err
|
||||
}
|
||||
return v.Outcome, nil
|
||||
}
|
||||
|
||||
func (s *BusStore) RecordOutcome(_ context.Context, fingerprint, node, account string, o Outcome, why string) error {
|
||||
return s.putOffered(fingerprint, offeredValue{Node: node, Account: account, Outcome: o, Why: why,
|
||||
At: s.Now().UTC().Format(time.RFC3339)}, offeredFor)
|
||||
}
|
||||
|
||||
func (s *BusStore) putOffered(fingerprint string, v offeredValue, ttl time.Duration) error {
|
||||
_, err := s.OfferedState.PutTTL(OfferedKey(fingerprint), v, ttl)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -51,7 +50,7 @@ func (s *memState) write(key string, value any, ttl time.Duration, create bool)
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if create && s.live(key) {
|
||||
return 0, errors.New("the key has a value")
|
||||
return 0, stdio.ErrChanged
|
||||
}
|
||||
raw, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
@@ -176,3 +175,41 @@ func TestEachMoveWritesTheBindingWithWhyAndBy(t *testing.T) {
|
||||
t.Fatalf("server's history %v", h)
|
||||
}
|
||||
}
|
||||
|
||||
// design 39 bus-and-switch §4, §5, B13: a lease is a key created with a two-minute lifetime that another holder cannot
|
||||
// create; an offered grant's outcome is kept under its fingerprint in the key form for 90 days.
|
||||
func TestALeaseAndAnOfferAreKeptByTheirKeysAndLifetimes(t *testing.T) {
|
||||
mm := newMesh(t)
|
||||
ctx := context.Background()
|
||||
key := "licence." + KeyFor(licence1)
|
||||
if ok, err := mm.store.Lease(ctx, key, "one", leaseFor); !ok || err != nil {
|
||||
t.Fatalf("the first lease: %v %v", ok, err)
|
||||
}
|
||||
if ok, _ := mm.store.Lease(ctx, key, "two", leaseFor); ok {
|
||||
t.Fatal("a second holder took a live lease")
|
||||
}
|
||||
if u := mm.leases.until[key]; !u.Equal(mm.now.Add(2 * time.Minute)) {
|
||||
t.Fatalf("the lease lives until %v", u)
|
||||
}
|
||||
mm.now = mm.now.Add(2 * time.Minute)
|
||||
if ok, _ := mm.store.Lease(ctx, key, "two", leaseFor); !ok {
|
||||
t.Fatal("a lease past its lifetime was not taken")
|
||||
}
|
||||
_ = mm.store.Unlease(ctx, key, "one")
|
||||
if _, held := mm.leases.values[key]; !held {
|
||||
t.Fatal("another holder's lease was ended")
|
||||
}
|
||||
_, _ = mm.m.Consider(ctx, []Holdings{mm.login("laptop", "rt-dead", false, t0)})
|
||||
fp := OfferedKey(Fingerprint("rt-dead"))
|
||||
if strings.ContainsAny(fp, ":") || !strings.HasPrefix(fp, "sha256_") {
|
||||
t.Fatalf("the offered key %q is not in the key form", fp)
|
||||
}
|
||||
var v offeredValue
|
||||
_ = json.Unmarshal(mm.offered.values[fp], &v)
|
||||
if v.Outcome != Dead || v.Node != "laptop" || v.Account != "acct-1" || v.Why == "" || v.At == "" {
|
||||
t.Fatalf("offered %+v", v)
|
||||
}
|
||||
if u := mm.offered.until[fp]; !u.Equal(mm.now.Add(90 * 24 * time.Hour)) {
|
||||
t.Fatalf("the outcome lives until %v", u)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,8 +72,8 @@ func manager() (*Manager, error) {
|
||||
node = n
|
||||
}
|
||||
built = &Manager{
|
||||
Store: &BusStore{LicencesState: stdio.State("licences"), BindingsState: stdio.State("bindings"), Now: time.Now,
|
||||
rest: store},
|
||||
Store: &BusStore{LicencesState: stdio.State("licences"), BindingsState: stdio.State("bindings"),
|
||||
LeasesState: stdio.State("leases"), OfferedState: stdio.State("offered"), Now: time.Now, rest: store},
|
||||
Vendor: LiveVendor(),
|
||||
Crypt: crypt,
|
||||
Grants: VaultGrants{Keys: keys},
|
||||
|
||||
@@ -251,7 +251,7 @@ func (m *Manager) Consider(ctx context.Context, reports []Holdings) ([]string, e
|
||||
var adopted []string
|
||||
for _, account := range accounts {
|
||||
list := byAccount[account]
|
||||
key := "account:" + account
|
||||
key := "account." + account
|
||||
ok, err := m.Store.Lease(ctx, key, m.Holder, leaseFor)
|
||||
if err != nil || !ok {
|
||||
continue
|
||||
@@ -521,7 +521,7 @@ func Due(l Licence, now time.Time, s Settings) bool {
|
||||
// lease live and does nothing; one started just after finds a fresh grant and is not due. force refreshes
|
||||
// whatever the age — the seat's `refresh` verb.
|
||||
func (m *Manager) Rotate(ctx context.Context, name string, force bool) (map[string]any, error) {
|
||||
key := "licence:" + name
|
||||
key := "licence." + KeyFor(name)
|
||||
ok, err := m.Store.Lease(ctx, key, m.Holder, leaseFor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -88,6 +88,8 @@ type miniMesh struct {
|
||||
mem *MemoryStore
|
||||
licences *memState
|
||||
bindings *memState
|
||||
leases *memState
|
||||
offered *memState
|
||||
vendor *stubVendor
|
||||
logins map[string]FullGrant // node → what its credentials file holds
|
||||
events []string
|
||||
@@ -103,7 +105,9 @@ func newMesh(t *testing.T) *miniMesh {
|
||||
now := func() time.Time { return mm.now }
|
||||
mm.mem = NewMemoryStore(now)
|
||||
mm.licences, mm.bindings = newMemState(now), newMemState(now)
|
||||
mm.store = &BusStore{LicencesState: mm.licences, BindingsState: mm.bindings, Now: now, rest: mm.mem}
|
||||
mm.leases, mm.offered = newMemState(now), newMemState(now)
|
||||
mm.store = &BusStore{LicencesState: mm.licences, BindingsState: mm.bindings, LeasesState: mm.leases,
|
||||
OfferedState: mm.offered, Now: now, rest: mm.mem}
|
||||
mm.vendor = &stubVendor{live: map[string]bool{}, now: now}
|
||||
crypt, _ := NewCrypt("a key the vault made")
|
||||
mm.keys, _ = GenerateKeyPair()
|
||||
|
||||
@@ -91,16 +91,16 @@ type Store interface {
|
||||
Unbind(ctx context.Context, consumer string) (bool, error)
|
||||
// Advance gives every consumer of a licence its next generation: what a rotation is to them.
|
||||
Advance(ctx context.Context, licence string, m Move) ([]Binding, error)
|
||||
rest
|
||||
}
|
||||
|
||||
// rest is what the store keeps beside its licences and bindings.
|
||||
type rest interface {
|
||||
// Lease takes a lease for d, or answers false while another holder's is live.
|
||||
Lease(ctx context.Context, key, holder string, d time.Duration) (bool, error)
|
||||
Unlease(ctx context.Context, key, holder string) error
|
||||
Outcome(ctx context.Context, fingerprint string) (Outcome, error)
|
||||
RecordOutcome(ctx context.Context, fingerprint, node, account string, o Outcome, why string) error
|
||||
rest
|
||||
}
|
||||
|
||||
// rest is what the store keeps beside its licences and bindings.
|
||||
type rest interface {
|
||||
RecordUsage(ctx context.Context, licence string, at int64, r UsageReading, raw map[string]any) error
|
||||
Usage(ctx context.Context, licence string, limit int) ([]UsageRow, error)
|
||||
Audit(ctx context.Context, what string, detail map[string]any) error
|
||||
@@ -122,46 +122,7 @@ type MemoryStore struct {
|
||||
|
||||
// NewMemoryStore is an empty store whose leases age by now.
|
||||
func NewMemoryStore(now func() time.Time) *MemoryStore {
|
||||
return &MemoryStore{now: now,
|
||||
leases: map[string]struct {
|
||||
holder string
|
||||
until time.Time
|
||||
}{}, outcomes: map[string]Outcome{}}
|
||||
}
|
||||
|
||||
func (m *MemoryStore) Lease(_ context.Context, key, holder string, d time.Duration) (bool, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if held, ok := m.leases[key]; ok && held.until.After(m.now()) && held.holder != holder {
|
||||
return false, nil
|
||||
}
|
||||
m.leases[key] = struct {
|
||||
holder string
|
||||
until time.Time
|
||||
}{holder, m.now().Add(d)}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (m *MemoryStore) Unlease(_ context.Context, key, holder string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.leases[key].holder == holder {
|
||||
delete(m.leases, key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MemoryStore) Outcome(_ context.Context, fp string) (Outcome, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.outcomes[fp], nil
|
||||
}
|
||||
|
||||
func (m *MemoryStore) RecordOutcome(_ context.Context, fp, _, _ string, o Outcome, _ string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.outcomes[fp] = o
|
||||
return nil
|
||||
return &MemoryStore{now: now}
|
||||
}
|
||||
|
||||
func (m *MemoryStore) RecordUsage(_ context.Context, licence string, at int64, r UsageReading, _ map[string]any) error {
|
||||
|
||||
Reference in New Issue
Block a user