163 lines
5.7 KiB
Go
163 lines
5.7 KiB
Go
package link
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
"github.com/nats-io/nats.go/jetstream"
|
|
|
|
"github.com/novox/mesh-controller/internal/broker"
|
|
)
|
|
|
|
// Calls kept on the bus (novox/hq to-be 45 §6).
|
|
//
|
|
// **Two keys a call**: `<id>` holds the record — verb, arguments as kept, caller, state, times — and
|
|
// `<id>.answer` the answer, bounded. A listing watches the records alone, so reading the last thousand
|
|
// calls reads the last thousand small records and not a thousand answers; one call asked by id reads
|
|
// both. A call's id is one token, so the record's key never holds a dot and `*` matches records only.
|
|
|
|
// BusCalls keeps calls in the controller's calls bucket.
|
|
type BusCalls struct {
|
|
kv jetstream.KeyValue
|
|
}
|
|
|
|
// CallsOnTheBus opens the calls bucket the controller asserts at its start.
|
|
func CallsOnTheBus(ctx context.Context, conn *nats.Conn) (*BusCalls, error) {
|
|
api, err := jetstream.New(conn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
kv, err := api.KeyValue(ctx, broker.CallsBucket)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the calls bucket %s is not on the bus — the controller asserts it at its "+
|
|
"start, so one older than this has not: %w", broker.CallsBucket, err)
|
|
}
|
|
return &BusCalls{kv: kv}, nil
|
|
}
|
|
|
|
const answerKey = ".answer"
|
|
|
|
// Keep writes a call's record, and its answer when it has one.
|
|
func (b *BusCalls) Keep(ctx context.Context, c Call) error {
|
|
answer := c.Answer
|
|
c.Answer = nil
|
|
if len(answer) > 0 {
|
|
if len(answer) > broker.CallAnswerBytes {
|
|
answer, _ = json.Marshal(map[string]any{"cut": fmt.Sprintf("an answer of %d bytes; the first %d "+
|
|
"are kept", len(answer), broker.CallAnswerBytes), "start": string(answer[:broker.CallAnswerBytes])})
|
|
}
|
|
// The answer before the record, so a record saying a call finished never points at an answer
|
|
// not yet written.
|
|
if _, err := b.kv.Put(ctx, c.ID+answerKey, answer); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
record, err := json.Marshal(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = b.kv.Put(ctx, c.ID, record)
|
|
return err
|
|
}
|
|
|
|
// Kept is one call, with its answer.
|
|
func (b *BusCalls) Kept(ctx context.Context, id string) (Call, bool, error) {
|
|
entry, err := b.kv.Get(ctx, id)
|
|
if errors.Is(err, jetstream.ErrKeyNotFound) || errors.Is(err, jetstream.ErrInvalidKey) {
|
|
return Call{}, false, nil
|
|
}
|
|
if err != nil {
|
|
return Call{}, false, err
|
|
}
|
|
var c Call
|
|
if err := json.Unmarshal(entry.Value(), &c); err != nil {
|
|
return Call{}, false, fmt.Errorf("the record of %s on the bus is not a call: %w", id, err)
|
|
}
|
|
answer, err := b.kv.Get(ctx, id+answerKey)
|
|
switch {
|
|
case err == nil:
|
|
c.Answer = json.RawMessage(answer.Value())
|
|
case !errors.Is(err, jetstream.ErrKeyNotFound):
|
|
return Call{}, false, err
|
|
}
|
|
return c, true, nil
|
|
}
|
|
|
|
// markAbandonedTries bounds the compare-and-set loop below (novox/hq issue 711 review round 1): a
|
|
// record that keeps changing on every read is not this sweep's to keep retrying forever over.
|
|
const markAbandonedTries = 10
|
|
|
|
// MarkAbandoned marks id abandoned unless its record already says it finished (novox/hq issue 711):
|
|
// a controller starting finds a call running under a holder that stopped, but that holder may have
|
|
// written the finish — with its kept answer — around the same moment. The record's own
|
|
// compare-and-set decides the race rather than the order these two writers happen to run in: this
|
|
// reads the record, and if it still says running, writes abandoned at that revision; a finish landing
|
|
// in between fails the write with a revision mismatch, and this rereads and finds the finish, which
|
|
// then stands. A record already resolved, or gone, is returned as found, so the caller never writes
|
|
// abandoned over it. Past markAbandonedTries attempts, it gives up and returns an error naming the
|
|
// call instead of looping forever (novox/hq issue 711 review round 1); Durably reports that as it
|
|
// reports any other keeper error.
|
|
func (b *BusCalls) MarkAbandoned(ctx context.Context, id string) (Call, error) {
|
|
for try := 0; try < markAbandonedTries; try++ {
|
|
entry, err := b.kv.Get(ctx, id)
|
|
if errors.Is(err, jetstream.ErrKeyNotFound) || errors.Is(err, jetstream.ErrInvalidKey) {
|
|
return Call{}, nil
|
|
}
|
|
if err != nil {
|
|
return Call{}, err
|
|
}
|
|
var c Call
|
|
if err := json.Unmarshal(entry.Value(), &c); err != nil {
|
|
return Call{}, fmt.Errorf("the record of %s on the bus is not a call: %w", id, err)
|
|
}
|
|
if c.State != CallRunning {
|
|
// A finish (or an earlier mark) is already the record's word: it stands, untouched.
|
|
return c, nil
|
|
}
|
|
c.State = CallAbandoned
|
|
record, err := json.Marshal(c)
|
|
if err != nil {
|
|
return Call{}, err
|
|
}
|
|
if _, err := b.kv.Update(ctx, id, record, entry.Revision()); err != nil {
|
|
if errors.Is(err, jetstream.ErrKeyRevisionMismatch) {
|
|
continue // something — a finish, most likely — wrote since: reread and judge it again
|
|
}
|
|
return Call{}, err
|
|
}
|
|
return c, nil
|
|
}
|
|
return Call{}, fmt.Errorf("%s: its record kept changing; it is left as it stands", id)
|
|
}
|
|
|
|
// Recent is every call the bucket holds, newest first, without answers: read once through a watch
|
|
// of the records, which hands over the current value of each and then says it has.
|
|
func (b *BusCalls) Recent(ctx context.Context) ([]Call, error) {
|
|
w, err := b.kv.Watch(ctx, "*", jetstream.IgnoreDeletes())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = w.Stop() }()
|
|
var out []Call
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, fmt.Errorf("reading the calls bucket: %w", ctx.Err())
|
|
case entry := <-w.Updates():
|
|
if entry == nil {
|
|
// Every current value handed over.
|
|
sort.SliceStable(out, func(i, j int) bool { return out[i].Started.After(out[j].Started) })
|
|
return out, nil
|
|
}
|
|
var c Call
|
|
if json.Unmarshal(entry.Value(), &c) == nil && c.ID != "" {
|
|
out = append(out, c)
|
|
}
|
|
}
|
|
}
|
|
}
|