//go:build linux package hostexec import ( "bytes" "context" "encoding/json" "strings" "testing" "time" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/moves" "github.com/carlosframework/platform/internal/park" ) type scopeLease struct { bucket, scope string releases, closes int } func (l *scopeLease) Check(context.Context) error { return nil } func (l *scopeLease) Release(context.Context) error { l.releases++; return nil } func (l *scopeLease) Close() { l.closes++ } func (l *scopeLease) LeaseScope() (string, string) { return l.bucket, l.scope } type scopePool struct{ acquired []string } func (p *scopePool) Acquire(_ context.Context, bucket, scope string) (park.HeldLease, error) { p.acquired = append(p.acquired, bucket+":"+scope) return &scopeLease{bucket: bucket, scope: scope}, nil } func TestPlan14ExecutionKeepsOtherLeaseScopes(t *testing.T) { t.Run("staging", func(t *testing.T) { b, _, remote, clock, q, peer := brokerFixture(t) if moves.StagingLeaseTTL != 45*time.Minute { t.Fatal("staging horizon changed") } wall, _, _ := clock.Now() key := moves.StagingLeasePrefix("ie", q.Host) + "/lock.json" raw, _ := json.Marshal(Record{Op: q.Op, Expires: wall.Add(moves.StagingLeaseTTL)}) tag, err := remote.Put(t.Context(), key, raw, "") if err != nil { t.Fatal(err) } permit, err := b.Execute(t.Context(), peer, q) if err != nil { t.Fatal(err) } q.Operation, q.ID, q.Token = "executionSettle", "settle", permit.Token if _, err := b.Execute(t.Context(), peer, q); err != nil { t.Fatal(err) } got, next, err := remote.Get(t.Context(), key) if err != nil || next != tag || !bytes.Equal(got, raw) { t.Fatal("execution settlement touched staging", err) } }) t.Run("borrowed", func(t *testing.T) { own := &scopeLease{bucket: "data", scope: "moves/shop.example/op"} pool := &scopePool{} borrower, err := (hostbox.AggregateLeases{Leases: pool, Resolve: hostbox.ReplicaParkLeaseSet}).AcquireHeld(t.Context(), own.bucket, own.scope, own) if err != nil { t.Fatal(err) } if err := borrower.Release(t.Context()); err != nil { t.Fatal(err) } borrower.Close() if own.releases != 0 || own.closes != 0 || len(pool.acquired) != 0 { t.Fatal("borrower released or reacquired lender's lease") } if err := own.Release(t.Context()); err != nil { t.Fatal(err) } own.Close() if own.releases != 1 || own.closes != 1 { t.Fatal("lender could not release") } }) t.Run("replica-data-bucket", func(t *testing.T) { for _, scope := range []string{"moves/shop.example/op", "instances/shop.example", "instances/shop.example/" + strings.Repeat("a", 64)} { pool := &scopePool{} held, err := (hostbox.AggregateLeases{Leases: pool, Resolve: hostbox.ReplicaParkLeaseSet}).Acquire(t.Context(), "data", scope) if err != nil { t.Fatal(err) } held.Close() if len(pool.acquired) != 1 || pool.acquired[0] != "data:"+scope { t.Fatalf("broadened scope: %v", pool.acquired) } } if _, err := hostbox.ReplicaParkLeaseSet(t.Context(), "data", moves.StagingLeasePrefix("ie", "shop.example")); err == nil { t.Fatal("deployment staging became data-bucket scope") } }) }