| 1 | //go:build linux |
| 2 | |
| 3 | package hostexec |
| 4 | |
| 5 | import ( |
| 6 | "bytes" |
| 7 | "context" |
| 8 | "encoding/json" |
| 9 | "strings" |
| 10 | "testing" |
| 11 | "time" |
| 12 | |
| 13 | "github.com/carlosframework/platform/internal/hostbox" |
| 14 | "github.com/carlosframework/platform/internal/moves" |
| 15 | "github.com/carlosframework/platform/internal/park" |
| 16 | ) |
| 17 | |
| 18 | type scopeLease struct { |
| 19 | bucket, scope string |
| 20 | releases, closes int |
| 21 | } |
| 22 | |
| 23 | func (l *scopeLease) Check(context.Context) error { return nil } |
| 24 | func (l *scopeLease) Release(context.Context) error { l.releases++; return nil } |
| 25 | func (l *scopeLease) Close() { l.closes++ } |
| 26 | func (l *scopeLease) LeaseScope() (string, string) { return l.bucket, l.scope } |
| 27 | |
| 28 | type scopePool struct{ acquired []string } |
| 29 | |
| 30 | func (p *scopePool) Acquire(_ context.Context, bucket, scope string) (park.HeldLease, error) { |
| 31 | p.acquired = append(p.acquired, bucket+":"+scope) |
| 32 | return &scopeLease{bucket: bucket, scope: scope}, nil |
| 33 | } |
| 34 | |
| 35 | func TestPlan14ExecutionKeepsOtherLeaseScopes(t *testing.T) { |
| 36 | t.Run("staging", func(t *testing.T) { |
| 37 | b, _, remote, clock, q, peer := brokerFixture(t) |
| 38 | if moves.StagingLeaseTTL != 45*time.Minute { |
| 39 | t.Fatal("staging horizon changed") |
| 40 | } |
| 41 | wall, _, _ := clock.Now() |
| 42 | key := moves.StagingLeasePrefix("ie", q.Host) + "/lock.json" |
| 43 | raw, _ := json.Marshal(Record{Op: q.Op, Expires: wall.Add(moves.StagingLeaseTTL)}) |
| 44 | tag, err := remote.Put(t.Context(), key, raw, "") |
| 45 | if err != nil { |
| 46 | t.Fatal(err) |
| 47 | } |
| 48 | permit, err := b.Execute(t.Context(), peer, q) |
| 49 | if err != nil { |
| 50 | t.Fatal(err) |
| 51 | } |
| 52 | q.Operation, q.ID, q.Token = "executionSettle", "settle", permit.Token |
| 53 | if _, err := b.Execute(t.Context(), peer, q); err != nil { |
| 54 | t.Fatal(err) |
| 55 | } |
| 56 | got, next, err := remote.Get(t.Context(), key) |
| 57 | if err != nil || next != tag || !bytes.Equal(got, raw) { |
| 58 | t.Fatal("execution settlement touched staging", err) |
| 59 | } |
| 60 | }) |
| 61 | t.Run("borrowed", func(t *testing.T) { |
| 62 | own := &scopeLease{bucket: "data", scope: "moves/shop.example/op"} |
| 63 | pool := &scopePool{} |
| 64 | borrower, err := (hostbox.AggregateLeases{Leases: pool, Resolve: hostbox.ReplicaParkLeaseSet}).AcquireHeld(t.Context(), own.bucket, own.scope, own) |
| 65 | if err != nil { |
| 66 | t.Fatal(err) |
| 67 | } |
| 68 | if err := borrower.Release(t.Context()); err != nil { |
| 69 | t.Fatal(err) |
| 70 | } |
| 71 | borrower.Close() |
| 72 | if own.releases != 0 || own.closes != 0 || len(pool.acquired) != 0 { |
| 73 | t.Fatal("borrower released or reacquired lender's lease") |
| 74 | } |
| 75 | if err := own.Release(t.Context()); err != nil { |
| 76 | t.Fatal(err) |
| 77 | } |
| 78 | own.Close() |
| 79 | if own.releases != 1 || own.closes != 1 { |
| 80 | t.Fatal("lender could not release") |
| 81 | } |
| 82 | }) |
| 83 | t.Run("replica-data-bucket", func(t *testing.T) { |
| 84 | for _, scope := range []string{"moves/shop.example/op", "instances/shop.example", "instances/shop.example/" + strings.Repeat("a", 64)} { |
| 85 | pool := &scopePool{} |
| 86 | held, err := (hostbox.AggregateLeases{Leases: pool, Resolve: hostbox.ReplicaParkLeaseSet}).Acquire(t.Context(), "data", scope) |
| 87 | if err != nil { |
| 88 | t.Fatal(err) |
| 89 | } |
| 90 | held.Close() |
| 91 | if len(pool.acquired) != 1 || pool.acquired[0] != "data:"+scope { |
| 92 | t.Fatalf("broadened scope: %v", pool.acquired) |
| 93 | } |
| 94 | } |
| 95 | if _, err := hostbox.ReplicaParkLeaseSet(t.Context(), "data", moves.StagingLeasePrefix("ie", "shop.example")); err == nil { |
| 96 | t.Fatal("deployment staging became data-bucket scope") |
| 97 | } |
| 98 | }) |
| 99 | } |
| 100 | |