package activator import ( "context" "errors" "fmt" "strings" "sync" "testing" "github.com/carlosframework/platform/internal/cgroup" "github.com/carlosframework/platform/internal/registry" ) // prepHookBackend is fakeSwapBackend with hooks in the pinned wake's // preparation: Restore (under the wake's lease) and GenTier (the last step // before StartStaged). type prepHookBackend struct { *fakeSwapBackend onRestore func() onTier func() } func (b *prepHookBackend) Restore(ctx context.Context, rt registry.Route) (bool, error) { if b.onRestore != nil { b.onRestore() } return b.fakeSwapBackend.Restore(ctx, rt) } func (b *prepHookBackend) GenTier(rt registry.Route, build string) cgroup.Limit { if b.onTier != nil { b.onTier() } return b.fakeSwapBackend.GenTier(rt, build) } // Final review F7: a hold's pinned wake checks the lease it actually holds, // and the attempt's checks, at the exec itself (immediately before // StartStaged), not before its preparation. A renewal that fails during // Restore or the rest of the preparation, or a fence set there, execs // nothing, and the attempt is abandoned (NoOwnership). func TestSwapHoldChecksItsLeaseAndAttemptAtTheExec(t *testing.T) { t.Parallel() lose := func(f *swapFixture) func() { var once sync.Once return func() { once.Do(func() { f.b.lastRep().setErr(errors.New("lease renewal failed")) }) } } fence := func(f *swapFixture) func() { var once sync.Once return func() { once.Do(func() { f.a.mu.Lock() if f.a.fences == nil { f.a.fences = map[string]uint64{} } f.a.fences[f.host] = 9 f.a.mu.Unlock() }) } } for _, tc := range []struct { name string arrange func(f *swapFixture, b *prepHookBackend) want error }{ {"the lease is lost during Restore", func(f *swapFixture, b *prepHookBackend) { b.onRestore = lose(f) }, errSwapLeaseLost}, {"the lease is lost during the preparation", func(f *swapFixture, b *prepHookBackend) { b.onTier = lose(f) }, errSwapLeaseLost}, {"a fence lands during the preparation", func(f *swapFixture, b *prepHookBackend) { b.onTier = fence(f) }, errSwapFenced}, } { t.Run(tc.name, func(t *testing.T) { f := newSwapFixture(t) b := &prepHookBackend{fakeSwapBackend: f.b} tc.arrange(f, b) f.a.backend = b res := f.a.Swap(context.Background(), f.attempt(ModeHold), f.newest) if res.Outcome != SwapAbandoned || !strings.Contains(res.Reason, tc.want.Error()) { t.Fatalf("Swap = %+v, want abandoned: %v", res, tc.want) } if gens, _, _ := f.b.calls(); gens != 0 { t.Fatalf("StartStaged calls = %d: the pinned build was exec'd without its ownership", gens) } }) } } // Final review F10: a pinned wake's execution evidence is the spawn's // result, a returned process or an unproven-start sentinel, never having // entered StartStaged. A StartStaged that fails before its exec (a socket // directory, a token, a placement, cmd.Start) returns no process: nothing // of the build ran, so the hold is abandoned, not down. One that returns an // unproven-start sentinel, or a process that then fails its health check, // executed: the hold is down. func TestSwapHoldExecEvidenceIsTheSpawnResult(t *testing.T) { t.Parallel() for _, tc := range []struct { name string arrange func(f *swapFixture) want SwapOutcome }{ {"StartStaged fails before its exec", func(f *swapFixture) { f.b.gmu.Lock() f.b.genErr = errors.New("socket dir: permission denied") f.b.gmu.Unlock() }, SwapAbandoned}, {"StartStaged answers an unproven start", func(f *swapFixture) { f.b.gmu.Lock() f.b.genErr = fmt.Errorf("start g: %w", errGenUnproven) f.b.gmu.Unlock() }, SwapDown}, {"the build ran and failed its health check", func(f *swapFixture) { f.a.genHealth = func(context.Context, Gen) error { return errors.New("unhealthy: HTTP 500") } }, SwapDown}, } { t.Run(tc.name, func(t *testing.T) { f := newSwapFixture(t) tc.arrange(f) res := f.a.Swap(context.Background(), f.attempt(ModeHold), f.newest) if res.Outcome != tc.want { t.Fatalf("Swap = %+v, want %s", res, tc.want) } if gens, _, _ := f.b.calls(); gens != 1 { t.Fatalf("StartStaged calls = %d, want 1", gens) } }) } }