package hostbox import ( "context" "errors" "fmt" "testing" "time" ) type renewableFake struct { lost chan struct{} released int } func (h *renewableFake) Err() error { select { case <-h.lost: return ErrChanged default: return nil } } func (h *renewableFake) Lost() <-chan struct{} { return h.lost } func (h *renewableFake) Release(context.Context) error { h.released++; return nil } func TestRenewingLeaseCancelsRequestsAndReleasesOnce(t *testing.T) { h := &renewableFake{lost: make(chan struct{})} l := RenewingLeases{Region: "eu-west-1", Open: func(context.Context, string, string, string) (Renewable, error) { return h, nil }} held, err := l.Acquire(t.Context(), "bucket", "instances/x.example/inc") if err != nil { t.Fatal(err) } ctx := held.(interface { Context(context.Context) context.Context }).Context(t.Context()) close(h.lost) select { case <-ctx.Done(): case <-time.After(time.Second): t.Fatal("lease loss did not cancel request") } if !errors.Is(held.Check(ctx), context.Canceled) { t.Fatal("lost handle accepted work") } held.Close() held.Close() if h.released != 1 { t.Fatalf("released %d times", h.released) } } func TestAggregateContextCancelsOnAnyConstituent(t *testing.T) { a, b := &renewableFake{lost: make(chan struct{})}, &renewableFake{lost: make(chan struct{})} leases := RenewingLeases{Region: "r", Open: func(_ context.Context, _, prefix, _ string) (Renewable, error) { if prefix == "a" { return a, nil } return b, nil }} held, err := (AggregateLeases{Leases: leases, Resolve: func(context.Context, string, string) ([]string, error) { return []string{"b", "a"}, nil }}).Acquire(t.Context(), "bucket", "a") if err != nil { t.Fatal(err) } defer held.Close() ctx := held.(interface { Context(context.Context) context.Context }).Context(t.Context()) close(b.lost) select { case <-ctx.Done(): case <-time.After(time.Second): t.Fatal("aggregate loss not propagated") } } type releaseContextLease struct { renewableFake check func(context.Context) error } func (h *releaseContextLease) Release(ctx context.Context) error { h.released++; return h.check(ctx) } func TestRenewingReleaseSurvivesItsOwnCancellation(t *testing.T) { for _, cancelled := range []bool{false, true} { t.Run(fmt.Sprint(cancelled), func(t *testing.T) { h := &releaseContextLease{renewableFake: renewableFake{lost: make(chan struct{})}} l := RenewingLeases{Region: "eu-west-1", Open: func(context.Context, string, string, string) (Renewable, error) { return h, nil }} held, err := l.Acquire(t.Context(), "bucket", "instances/x.example/inc") if err != nil { t.Fatal(err) } ctx, cancel := context.WithCancel(held.(interface { Context(context.Context) context.Context }).Context(t.Context())) defer cancel() if cancelled { cancel() } h.check = func(cleanup context.Context) error { if err := cleanup.Err(); err != nil { return err } deadline, ok := cleanup.Deadline() if !ok || time.Until(deadline) > RequestLimit || time.Until(deadline) <= 0 { t.Fatal("release is not request bounded") } select { case <-ctx.Done(): case <-time.After(time.Second): t.Fatal("work context still active during release") } return cleanup.Err() } if err := held.Release(ctx); err != nil { t.Fatal("release cancelled itself", err) } held.Close() if h.released != 1 { t.Fatalf("released %d times", h.released) } if err := held.Check(t.Context()); err == nil { t.Fatal("released lease still grants work") } }) } }