Files
mesh-controller/cmd/mesh-controller/merge_unsaved_integration_test.go
T
jschoubben 275ddd76e8 Keep rollouts and the merges they answer in the rollouts bucket (issue 1092)
Once chain_moved records the kind, a plan is a key of the rollouts bucket under its id and the open batch is the same key in state assembling. A save refuses a copy another save moved since (the plan's own Revision, kept in its value, counts saves) and keeps the merges the record holds, so a copy read before a merge moved cannot lay it back. The merges a batch or walk answers are the record's held field, with the columns the table had; a merge no plan answers stays, flagged, in the record that held it last, or in the record kept for those. A merge moved to a plan is written to the receiving record first and then taken from the giving one; one found in both is read from the one it entered last and repaired at the next cut. Plans and merges are read from a view kept by one watch of the bucket. A bus that does not answer, a lease lost and a key moved are now not-now like the store being away, so a merge whose plan could not be written is held and handed over again, never acknowledged. The start moves release_plan and batched_merge into the bucket, one kind, counted against both tables.
2026-10-18 03:06:50 +02:00

72 lines
2.4 KiB
Go

//go:build integration
package main
import (
"context"
"errors"
"sync/atomic"
"testing"
"time"
"github.com/nats-io/nats.go"
"github.com/novox/mesh-controller/internal/chain"
"github.com/novox/mesh-controller/internal/link"
)
// refusing is a bucket that refuses to write while it is told to.
type refusing struct {
chain.Backend
refuse atomic.Bool
}
func (r *refusing) Create(ctx context.Context, key string, value []byte) (uint64, error) {
if r.refuse.Load() {
return 0, nats.ErrTimeout
}
return r.Backend.Create(ctx, key, value)
}
func (r *refusing) Update(ctx context.Context, key string, value []byte, revision uint64) (uint64, error) {
if r.refuse.Load() {
return 0, nats.ErrTimeout
}
return r.Backend.Update(ctx, key, value, revision)
}
// **A merge whose plan cannot be saved is held, recorded nowhere, and recorded when it is handed over again** (ADR 0407
// decision 3, issue 1092): hearMerge answers ErrTryAgain — the bus leaves the announcement unacknowledged and hands it
// over again — and the merge is in no record until the plan holding it is written.
func TestAMergeWhosePlanCannotBeSavedIsHeldAndRecordedWhenHandedOverAgain(t *testing.T) {
open := windowed(t)
everyHeadWaits(t)
asksWithPaths(t)
ctx := t.Context()
rollouts := &refusing{Backend: chain.NewMemory()}
open.inventory.UseChain(chain.New(chain.NewMemory(), rollouts, chain.NewMemory(), func(context.Context) error { return nil }))
merge := catalogueMerge("cat1aaaaaaaa", "app", t0)
rollouts.refuse.Store(true)
err := (following{open: open}).hearMerge(ctx, merge, t0.Add(time.Second))
if !errors.Is(err, link.ErrTryAgain) {
t.Fatalf("a merge whose plan could not be saved answered %v, which the bus takes as done", err)
}
if _, known, err := open.inventory.MergeOf(ctx, "novox/mesh-catalog", merge.Commit); err != nil || known {
t.Fatalf("the merge is kept though its plan was not: %v %v", known, err)
}
if _, bs := walks(t, open); len(bs) != 0 {
t.Fatalf("a batch was kept though its write failed: %+v", bs)
}
rollouts.refuse.Store(false)
hear(t, open, merge, t0.Add(10*time.Second))
_, bs := walks(t, open)
if len(bs) != 1 {
t.Fatalf("the merge handed over again made %d batch(es)", len(bs))
}
if m, known, _ := open.inventory.MergeOf(ctx, "novox/mesh-catalog", merge.Commit); !known || m.Plan != bs[0].ID {
t.Fatalf("the merge is %+v %v, and the batch %s", m, known, bs[0].ID)
}
}