package broker import ( "context" "fmt" "strings" "time" "github.com/nats-io/nats.go/jetstream" ) // A seat's cancelled set (novox/hq ADR 0219). // // **Cancelling an ask is deleting its message from the seat's work queue** — and that alone has a // race no ordering on one side closes: a holder may fetch the ask in the moment between the // controller reading the queue and deleting the message, and build what a person cancelled. So the // controller first writes the ask's id into the seat's cancelled set, and a holder looks its ask up // there after taking it and before building: listed, it terminates the ask and announces it failed // rather than starting it. // // **A key-value bucket, because that is the shape the bus already has for "a small set the // controller writes and many read cheaply"** (ADR 0201's state buckets): one direct read by key per // ask taken — no consumer, no subscription a holder must keep, nothing that grows a holder's grants // beyond one read subject. Kept beside the seat's own stream and worker and named like them, so the // three objects of one work queue read as one family; asserted by the controller with the queue, // and aged out with it — an id cancelled a week ago names an ask the queue no longer holds. // CancelledSetName is the bucket holding a seat's cancelled asks. func CancelledSetName(seat string) string { return "SEAT_" + upperSnake(seat) + "_cancelled" } // hasCancelledSet is whether a seat's queue can be cancelled from: the work queues the controller // asks, whose asks it alone shows and changes. A module's own seat's queue is that module's affair. func hasCancelledSet(seat string) bool { for _, s := range seatsTheControllerAsks { if s == seat { return true } } return false } // IsCancelledSet says a bucket is a seat's cancelled set rather than a module's state — the mesh's // own, and never one to report as state nothing declares. func IsCancelledSet(bucket string) bool { return strings.HasPrefix(bucket, "SEAT_") && strings.HasSuffix(bucket, "_cancelled") } // cancelledSetAge is how long a cancelled id is kept: as long as the seat's queue keeps an ask. const cancelledSetAge = 7 * 24 * time.Hour // A CancelledSetAsserter is what raising the cancelled sets needs of a connection. type CancelledSetAsserter interface { EnsureCancelledSet(seat string) error } // RaiseCancelledSets asserts the cancelled set of every work queue the controller asks. func RaiseCancelledSets(a CancelledSetAsserter, seats []DeclaredSeat) error { for _, s := range seats { if len(s.Accepts) == 0 || !hasCancelledSet(s.Name) { continue } if err := a.EnsureCancelledSet(s.Name); err != nil { return fmt.Errorf("asserting the cancelled set of %s: %w", s.Name, err) } } return nil } // EnsureCancelledSet creates a seat's cancelled set if absent and brings its options to match. // Direct reads on, which is how a holder looks an id up with one request. func (j *JetStream) EnsureCancelledSet(seat string) error { js, err := jetstream.New(j.conn) if err != nil { return err } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if _, err := js.CreateOrUpdateKeyValue(ctx, jetstream.KeyValueConfig{ Bucket: CancelledSetName(seat), Description: fmt.Sprintf("the asks of the %s seat cancelled by hand (novox/hq ADR 0219): written "+ "by the controller before it deletes an ask from the queue, read by a holder on taking one, "+ "so an ask fetched in that moment is ended rather than built", seat), History: 1, TTL: cancelledSetAge, MaxValueSize: 4 * 1024, MaxBytes: 4 * 1024 * 1024, Storage: jetstream.FileStorage, }); err != nil { return fmt.Errorf("asserting bucket %s: %w", CancelledSetName(seat), err) } return nil }