lifecycle

package
v0.0.1-alpha.44 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 24, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Overview

Package lifecycle provides a shared, testable scheduler for async state transitions. EC2 instances, ECS tasks, and RDS instances all need delayed state changes (e.g. pending → running, creating → available). This package provides a single abstraction instead of each service hand-rolling time.AfterFunc/goroutine/cancel patterns.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AdvanceAndSettleAll

func AdvanceAndSettleAll(mock *clock.Mock, d time.Duration)

AdvanceAndSettleAll advances mock by d and returns only once every transition that came due on every Scheduler built with that clock has run to completion. It is Scheduler.AdvanceAndSettle for a caller that holds the clock rather than the Scheduler, and carries the same definition of settled: transitions a callback schedules while this runs are not waited for, even if they fall due within d.

A zero d settles what is already due without moving time. A Scheduler created after this call begins is not covered — build the server first, then advance its clock.

func Forget

func Forget(mock *clock.Mock)

Forget drops the Schedulers indexed against mock. A test server calls it when it shuts down, so a package that builds thousands of servers does not retain every Scheduler any of them made until the test binary exits.

Calling it while the clock is still in use is safe but pointless: a Scheduler built afterwards indexes itself again.

func ScopedKey

func ScopedKey(region, id, transition string) string

ScopedKey builds the region-scoped scheduler key AfterScoped and CancelScoped use. Exported for callers that need to inspect or test pending transitions.

Types

type Scheduler

type Scheduler struct {
	// contains filtered or unexported fields
}

Scheduler manages keyed delayed callbacks. Each service creates its own Scheduler instance (no global state, DI-friendly).

func NewScheduler

func NewScheduler(clk clock.Clock) *Scheduler

NewScheduler creates a Scheduler using the given clock. Production: clock.New(). Tests: clock.NewMock() for instant time skips.

A Scheduler built on a mock clock records itself against that clock, so a test that holds only the clock can settle it — see mockclock.go. One built on a real clock records nothing.

func (*Scheduler) AdvanceAndSettle

func (s *Scheduler) AdvanceAndSettle(mock *clock.Mock, d time.Duration)

AdvanceAndSettle advances a mock clock by d and returns only once every transition that came due has run to completion. mock must be the clock this Scheduler was built with.

It exists because a bare mock.Add is not enough to observe the effects of a transition. The mock clock runs an AfterFunc callback on a goroutine of its own and then yields for a single millisecond, so a test that advances the clock and reads the store on the next line is racing the callback: it wins on an idle machine and loses on a loaded one, which is a flake that only ever reproduces in CI. Waiting on the callback itself is deterministic regardless of load.

Transitions a callback schedules while this runs are not waited for, even if they fall due within d — settling is defined over the transitions that were outstanding when the call began.

func (*Scheduler) After

func (s *Scheduler) After(key string, delay time.Duration, fn func())

After schedules fn to run after delay. Key identifies the transition (e.g. "i-abc123:terminate"). If a transition with the same key is already pending, it is cancelled before the new one is scheduled.

When delay is 0 and the clock is a real clock (not a mock), fn is executed synchronously within this call. This ensures that subsequent API calls immediately see the updated state instead of racing with a goroutine. With a mock clock, 0-delay timers remain pending until clock.Add is called, preserving test-time control.

After after Stop is a defined no-op: it neither runs fn nor schedules it, including the 0-delay inline fast path. That is deliberate — a service's shutdown must be able to rely on nothing it manages through this Scheduler starting new work once Stop has been called, and a callback that reschedules itself (as readiness.Watch's health-check loop does) must stop doing so rather than run forever underneath a stopped service.

func (*Scheduler) AfterScoped

func (s *Scheduler) AfterScoped(region, id, transition string, delay time.Duration, fn func(ctx context.Context))

AfterScoped schedules fn to run after delay for a resource that lives in a region-keyed store. It exists because the plain After callbacks run outside any request context: a bare context.Background() resolves to the DEFAULT region, so for a resource created in any other region the callback reads or writes the wrong store key and silently no-ops (statuses stuck at "creating"/"stopping", deferred deletes that never land).

AfterScoped closes both halves of that bug at once:

  • fn receives a background context carrying region, so region-keyed store lookups inside the callback resolve the same record the request wrote;
  • the scheduler key is scoped by region, so same-named resources in different regions cannot cancel or replace each other's pending transitions.

Callers pass the region resolved from the request context at schedule time (e.g. store.region(ctx)).

func (*Scheduler) Cancel

func (s *Scheduler) Cancel(key string) bool

Cancel cancels a pending transition by key. Returns true if a pending transition was found and cancelled.

True means fn will not run. A transition whose timer has fired but whose callback has not yet claimed the entry is still pending, so it is cancelled too and its callback stands down. One already running fn is not pending: Cancel reports false and does not interrupt it.

func (*Scheduler) CancelScoped

func (s *Scheduler) CancelScoped(region, id, transition string) bool

CancelScoped cancels a pending transition scheduled with AfterScoped.

func (*Scheduler) PendingCount

func (s *Scheduler) PendingCount() int

PendingCount returns the number of currently scheduled transitions.

func (*Scheduler) Settle

func (s *Scheduler) Settle()

Settle blocks until every transition outstanding when it was called has run to completion or been cancelled. It cancels nothing — on a real clock it waits the delays out — so a test can let the real transitions happen and then read what they produced, rather than polling until they show up.

Outstanding covers both the transitions still pending and the callbacks that have already fired and are still running, so a test whose setup ran long enough for some of its transitions to come due does not silently get a partial wait. Transitions scheduled after the call are not waited for.

func (*Scheduler) Stop

func (s *Scheduler) Stop(ctx context.Context)

Stop marks the scheduler stopped, cancels all pending transitions, and waits for any in-flight callbacks to complete. Respects ctx for timeout on the wait.

stopped is set inside this same critical section, before any pending transition is cancelled and before the wg.Wait below. That ordering is what makes wg.Add and wg.Wait race-free without either taking mu: every After call that has not yet reached its own critical section when Stop reaches this one will, once it does, see stopped and return without calling wg.Add — so by the time this critical section ends, no more Adds are coming, and it is safe to wait.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL
⌂ 换网址