Keep a condition's looks in the store, so a look that changes nothing writes no bucket (issue 420)
Every look rewrote the condition's value in the conditions bucket, because the value carried the count of looks, the last look and the newest evidence: 131 647 writes in four days for twelve open conditions, against ADR 0289's rule that a bucket is written only when what it describes changed. The looks now live in the controller's store (migration 0095, condition_looks), the bucket holds what a condition says and is written only when that changes, and the conditions verb and the condition events put the looks back.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package conditions
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -53,6 +54,7 @@ const Seat = "mesh-controller"
|
||||
// Keeper raises, observes, silences and clears conditions, and says each transition.
|
||||
type Keeper struct {
|
||||
store Backend
|
||||
looks LookStore
|
||||
history History
|
||||
teller Teller
|
||||
now func() time.Time
|
||||
@@ -91,7 +93,10 @@ type clearing struct {
|
||||
|
||||
// Options are what a Keeper is made with.
|
||||
type Options struct {
|
||||
Store Backend
|
||||
Store Backend
|
||||
// Looks is where the open conditions' looks are kept (looks.go): the controller's store. Nil takes the
|
||||
// Store's own when it keeps them (the in-memory store does), and otherwise keeps them in this process.
|
||||
Looks LookStore
|
||||
History History
|
||||
// Teller says the transitions; nil says nothing (a test, or a command run with no bus to say on).
|
||||
Teller Teller
|
||||
@@ -111,7 +116,7 @@ var TellFor = 10 * time.Minute
|
||||
// NewKeeper is a keeper over a store. It reads what cleared lately from the history, so a condition
|
||||
// that cleared just before this controller started and is raised again now is a reopening.
|
||||
func NewKeeper(ctx context.Context, o Options) *Keeper {
|
||||
k := &Keeper{store: o.Store, history: o.History, teller: o.Teller, now: o.Now, say: o.Say, changed: o.Changed,
|
||||
k := &Keeper{store: o.Store, looks: o.Looks, history: o.History, teller: o.Teller, now: o.Now, say: o.Say, changed: o.Changed,
|
||||
epoch: o.Epoch,
|
||||
cleared: map[string]clearing{}, reworded: map[string]bool{}, out: make(chan Event, 1024), drained: make(chan struct{})}
|
||||
if k.now == nil {
|
||||
@@ -120,6 +125,13 @@ func NewKeeper(ctx context.Context, o Options) *Keeper {
|
||||
if k.say == nil {
|
||||
k.say = func(string, ...any) {}
|
||||
}
|
||||
if k.looks == nil {
|
||||
if own, ok := o.Store.(LookStore); ok {
|
||||
k.looks = own
|
||||
} else {
|
||||
k.looks = NewMemoryLooks()
|
||||
}
|
||||
}
|
||||
if k.history != nil {
|
||||
if recent, err := k.history.Since(ctx, k.now().Add(-ReopenWithin)); err == nil {
|
||||
for _, e := range recent {
|
||||
@@ -218,7 +230,7 @@ func (k *Keeper) Observe(ctx context.Context, o Observation) (Condition, error)
|
||||
if err := k.stamp(&c); err != nil {
|
||||
return Condition{}, err
|
||||
}
|
||||
body, err := json.Marshal(c)
|
||||
body, err := BucketValue(c)
|
||||
if err != nil {
|
||||
return Condition{}, err
|
||||
}
|
||||
@@ -227,6 +239,7 @@ func (k *Keeper) Observe(ctx context.Context, o Observation) (Condition, error)
|
||||
} else if err != nil {
|
||||
return Condition{}, fmt.Errorf("raising the condition %s: %w", key, err)
|
||||
}
|
||||
k.keepLooks(ctx, &c)
|
||||
k.mu.Lock()
|
||||
delete(k.cleared, key)
|
||||
k.mu.Unlock()
|
||||
@@ -237,6 +250,7 @@ func (k *Keeper) Observe(ctx context.Context, o Observation) (Condition, error)
|
||||
if err := json.Unmarshal(entry.Value, &c); err != nil {
|
||||
return Condition{}, fmt.Errorf("the condition %s on the bus cannot be read: %w", key, err)
|
||||
}
|
||||
looked := k.attach(ctx, &c)
|
||||
var changes []Event
|
||||
// A healer's budget spent made it urgent; the watchdog seeing it again does not undo that.
|
||||
if o.Severity != c.Severity && !(c.Escalated() && o.Severity != Urgent) {
|
||||
@@ -278,14 +292,22 @@ func (k *Keeper) Observe(ctx context.Context, o Observation) (Condition, error)
|
||||
if err := k.stamp(&c); err != nil {
|
||||
return Condition{}, err
|
||||
}
|
||||
body, err := json.Marshal(c)
|
||||
body, err := BucketValue(c)
|
||||
if err != nil {
|
||||
return Condition{}, err
|
||||
}
|
||||
if err := k.store.Update(ctx, key, body, entry.Revision); errors.Is(err, ErrMoved) {
|
||||
continue
|
||||
} else if err != nil {
|
||||
return Condition{}, fmt.Errorf("observing the condition %s: %w", key, err)
|
||||
// Written only when what the condition says changed (novox/hq ADR 0289 §1, issue 420): a look
|
||||
// that saw it as it was changes only its looks, which the controller's store keeps.
|
||||
if !bytes.Equal(body, entry.Value) {
|
||||
if err := k.store.Update(ctx, key, body, entry.Revision); errors.Is(err, ErrMoved) {
|
||||
continue
|
||||
} else if err != nil {
|
||||
return Condition{}, fmt.Errorf("observing the condition %s: %w", key, err)
|
||||
}
|
||||
}
|
||||
// Looks read as unreadable are not written over: a count of one would replace the count kept.
|
||||
if fresh && looked {
|
||||
k.keepLooks(ctx, &c)
|
||||
}
|
||||
for _, e := range changes {
|
||||
e.At, e.Condition = now, c
|
||||
@@ -318,6 +340,7 @@ func (k *Keeper) ClearSaying(ctx context.Context, key, why, resolved string) (bo
|
||||
// Unreadable is not resolved: kept, and said, rather than removed unread.
|
||||
return false, fmt.Errorf("the condition %s on the bus cannot be read, so it is not cleared: %w", key, err)
|
||||
}
|
||||
k.attach(ctx, &c)
|
||||
if err := k.stamp(&c); err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -329,6 +352,10 @@ func (k *Keeper) ClearSaying(ctx context.Context, key, why, resolved string) (bo
|
||||
} else if err != nil {
|
||||
return false, fmt.Errorf("clearing the condition %s: %w", key, err)
|
||||
}
|
||||
if err := k.looks.ForgetLooks(ctx, key); err != nil {
|
||||
k.say("the observations of the condition %s, which cleared, could not be forgotten in the controller's "+
|
||||
"store; a raising of it counts from one all the same: %v", key, err)
|
||||
}
|
||||
now := k.now().UTC()
|
||||
k.mu.Lock()
|
||||
k.cleared[key] = clearing{at: now, count: c.Count, began: c.Began(), gaps: c.Gaps, silenced: c.Silenced, tried: c.Tried}
|
||||
@@ -402,12 +429,13 @@ func (k *Keeper) amend(ctx context.Context, key string, change func(*Condition,
|
||||
if err := json.Unmarshal(entry.Value, &c); err != nil {
|
||||
return Condition{}, false, fmt.Errorf("the condition %s on the bus cannot be read: %w", key, err)
|
||||
}
|
||||
k.attach(ctx, &c)
|
||||
now := k.now().UTC()
|
||||
changes := change(&c, now)
|
||||
if err := k.stamp(&c); err != nil {
|
||||
return Condition{}, false, err
|
||||
}
|
||||
body, err := json.Marshal(c)
|
||||
body, err := BucketValue(c)
|
||||
if err != nil {
|
||||
return Condition{}, false, err
|
||||
}
|
||||
@@ -479,12 +507,13 @@ func (k *Keeper) Silence(ctx context.Context, key string, d time.Duration, by, w
|
||||
if err := json.Unmarshal(entry.Value, &c); err != nil {
|
||||
return Condition{}, fmt.Errorf("the condition %s on the bus cannot be read: %w", key, err)
|
||||
}
|
||||
k.attach(ctx, &c)
|
||||
now := k.now().UTC()
|
||||
c.Silenced = &Silence{Until: now.Add(d), By: by, Why: strings.TrimSpace(why), Since: now}
|
||||
if err := k.stamp(&c); err != nil {
|
||||
return Condition{}, err
|
||||
}
|
||||
body, err := json.Marshal(c)
|
||||
body, err := BucketValue(c)
|
||||
if err != nil {
|
||||
return Condition{}, err
|
||||
}
|
||||
@@ -526,12 +555,13 @@ func (k *Keeper) EndSilences(ctx context.Context) error {
|
||||
if held.Silenced == nil || now.Before(held.Silenced.Until) {
|
||||
break
|
||||
}
|
||||
k.attach(ctx, &held)
|
||||
was := held.Silenced.Why
|
||||
held.Silenced = nil
|
||||
if err := k.stamp(&held); err != nil {
|
||||
return err
|
||||
}
|
||||
body, err := json.Marshal(held)
|
||||
body, err := BucketValue(held)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -549,12 +579,12 @@ func (k *Keeper) EndSilences(ctx context.Context) error {
|
||||
|
||||
// Open is every open condition, urgent first and then oldest first.
|
||||
func (k *Keeper) Open(ctx context.Context) ([]Condition, error) {
|
||||
return Read(ctx, k.store)
|
||||
return ReadWith(ctx, k.store, k.looks)
|
||||
}
|
||||
|
||||
// Get is one open condition.
|
||||
func (k *Keeper) Get(ctx context.Context, key string) (Condition, bool, error) {
|
||||
return ReadOne(ctx, k.store, key)
|
||||
return ReadOneWith(ctx, k.store, k.looks, key)
|
||||
}
|
||||
|
||||
// HistorySince is every transition from a moment, oldest first.
|
||||
@@ -565,9 +595,16 @@ func (k *Keeper) HistorySince(ctx context.Context, since time.Time) ([]Event, er
|
||||
return k.history.Since(ctx, since)
|
||||
}
|
||||
|
||||
// Read is every open condition in a store, in the order status says them. A value that cannot be
|
||||
// read is an error naming its key, never a condition left out (ADR 0227 rule 4).
|
||||
// Read is every open condition in a store, in the order status says them, with its looks when the
|
||||
// store keeps them. A value that cannot be read is an error naming its key, never a condition left out
|
||||
// (ADR 0227 rule 4).
|
||||
func Read(ctx context.Context, store Backend) ([]Condition, error) {
|
||||
looks, _ := store.(LookStore)
|
||||
return ReadWith(ctx, store, looks)
|
||||
}
|
||||
|
||||
// ReadWith is Read with each condition's looks from where they are kept (looks.go); nil reads none.
|
||||
func ReadWith(ctx context.Context, store Backend, looks LookStore) ([]Condition, error) {
|
||||
all, err := store.All(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the open conditions cannot be read: %w", err)
|
||||
@@ -580,12 +617,19 @@ func Read(ctx context.Context, store Backend) ([]Condition, error) {
|
||||
}
|
||||
out = append(out, c)
|
||||
}
|
||||
attachLooks(ctx, looks, out)
|
||||
Order(out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ReadOne is one open condition from a store.
|
||||
// ReadOne is one open condition from a store, with its looks when the store keeps them.
|
||||
func ReadOne(ctx context.Context, store Backend, key string) (Condition, bool, error) {
|
||||
looks, _ := store.(LookStore)
|
||||
return ReadOneWith(ctx, store, looks, key)
|
||||
}
|
||||
|
||||
// ReadOneWith is ReadOne with its looks from where they are kept; nil reads none.
|
||||
func ReadOneWith(ctx context.Context, store Backend, looks LookStore, key string) (Condition, bool, error) {
|
||||
e, found, err := store.Get(ctx, key)
|
||||
if err != nil || !found {
|
||||
return Condition{}, found, err
|
||||
@@ -594,9 +638,43 @@ func ReadOne(ctx context.Context, store Backend, key string) (Condition, bool, e
|
||||
if err := json.Unmarshal(e.Value, &c); err != nil {
|
||||
return Condition{}, false, fmt.Errorf("the condition %s cannot be read: %w", key, err)
|
||||
}
|
||||
if looks != nil {
|
||||
l, kept, err := looks.Looked(ctx, key)
|
||||
if err != nil {
|
||||
c.LooksUnread = "its observations could not be read from the controller's store: " + err.Error()
|
||||
} else {
|
||||
attachOne(&c, l, kept)
|
||||
}
|
||||
}
|
||||
return c, true, nil
|
||||
}
|
||||
|
||||
// attach puts a condition's kept looks on it, as read from the bucket. False when they could not be
|
||||
// read, which is said: the condition then carries why, and its looks are not written over.
|
||||
func (k *Keeper) attach(ctx context.Context, c *Condition) bool {
|
||||
l, kept, err := k.looks.Looked(ctx, c.Key)
|
||||
if err != nil {
|
||||
c.LooksUnread = "its observations could not be read from the controller's store: " + err.Error()
|
||||
k.say("the observations of the condition %s could not be read from the controller's store, so this look "+
|
||||
"is not counted: %v", c.Key, err)
|
||||
return false
|
||||
}
|
||||
attachOne(c, l, kept)
|
||||
return true
|
||||
}
|
||||
|
||||
// keepLooks writes a condition's looks, saying a write that failed: the condition is raised or kept in the
|
||||
// bucket all the same, and this look is not counted.
|
||||
func (k *Keeper) keepLooks(ctx context.Context, c *Condition) {
|
||||
if err := k.looks.KeepLooks(ctx, c.Key, c.looks()); err == nil {
|
||||
c.LooksUnread = ""
|
||||
} else {
|
||||
c.LooksUnread = "its observations could not be kept in the controller's store: " + err.Error()
|
||||
k.say("the observations of the condition %s could not be kept in the controller's store, so this look "+
|
||||
"is not counted: %v", c.Key, err)
|
||||
}
|
||||
}
|
||||
|
||||
// tell queues a transition to be kept and said. Never blocks the caller for long: a queue that is
|
||||
// full is a bus away for a long time, and the transition is said lost rather than holding a watchdog.
|
||||
func (k *Keeper) tell(e Event) {
|
||||
|
||||
Reference in New Issue
Block a user