diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e2afed..9bf4113 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,21 @@ below says plainly whether an upgrade can break a caller. ## [Unreleased] +`Binding.Worker` is now `Binding.Go`, after `errgroup.Group.Go` and +`sync.WaitGroup.Go`, whose contract it has always had: run a function in a +goroutine the group tracks, cancel it when the group winds down, wait for it, +and let its error take the group down. `go doc -all` against 0.15.1 removes +`Binding.Worker` and adds `Binding.Go` with the same signature. **An upgrade +breaks a caller that registers a worker**: rename the call from `Worker` to +`Go`, and nothing else changes. + +### Changed + +- `Binding.Worker` is renamed `Binding.Go`, with no alias left behind. The + word "worker" still names what the method registers, in the docs and in the + one error `Stop` reports about it, which now reads "worker did not return" + rather than "Worker hook did not return". + ## [0.15.1] - 2026-09-10 A code-organisation release: the library is six files rather than one, and diff --git a/CLAUDE.md b/CLAUDE.md index fadba77..dce86c2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -424,7 +424,7 @@ binding and cannot protect the inner scope. - Every user hook is called through `callHook`, which turns a panic into that hook's error, and every step is reported through `state.report`, so a hook that panicked is observed like one that failed. A cancelled - `Worker`'s return is dropped only when it says nothing beyond + worker's return is dropped only when it says nothing beyond `context.Canceled` (`onlyCancellation` walks the error tree); `errors.Is` matched `errors.Join(ctx.Err(), failure)` and dropped the failure with it (#35). A hook can panic by resolving something whose registration is rejected @@ -434,10 +434,10 @@ binding and cannot protect the inner scope. behind it. - Nothing in the teardown path may run a user hook against a value another hook still holds. That is one rule with three instances: `OnStop` after `OnDrain`, - `OnStop` after a `Worker` hook (deferred to `releaseAfterWorker` when `ctx` + `OnStop` after a `Go` worker (deferred to `releaseAfterWorker` when `ctx` expires rather than run alongside it), and a parent's hooks after a child's. - `Start`'s rollback goes through `Stop` with `context.WithoutCancel`, so it - stops child scopes and waits for `Worker` hooks. + stops child scopes and waits for workers. - Whichever `Stop` call queues a handoff owns that teardown's context; a later `Stop` must not clobber it. @@ -574,7 +574,7 @@ measured itself. **Where the defects came from.** Five reviews in September 2026, preceded by seven narrower passes. The first found eleven defects plus a gap it did not -count; the second six plus the `Worker`-hook overlap, and then two more the +count; the second six plus the worker overlap, and then two more the tightened driver found on its own; the third six that were all cross-phase or cross-branch -- a drain hook stopping a sibling scope, a release dropped with a missed deadline, a shutdown cause published after `Run` had read it, a false @@ -676,7 +676,7 @@ reverse is caught by the fuzzer in 0.06s and *not* by the 400 seeded sequences. this harness loses a backgrounded server's startup output when redirected, which once produced a false failure report. - **A teardown finishes after `Stop` returns only when `Stop`'s context - expired** -- waiting for a `Worker` hook, a start step or a drain hook -- plus + expired** -- waiting for a worker, a start step or a drain hook -- plus the one that undoes a build completing after the scope stopped, which no `Stop` issued. The deadline bounds how long `Stop` waits, never whether the release is owed, and `Stop` has already taken the instance off its scope's diff --git a/README.md b/README.md index f0f4c2f..dce8a68 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,8 @@ app.Wire[*Repo](NewRepo) // func NewRepo(*DB) *Repo repo := app.Get[*Repo]() // builds Config, then DB, then Repo, each once ``` -A registration takes `OnStart`, `OnStop` and `Worker` hooks typed on the -service, and `app.Run(ctx)` starts everything in dependency order, waits for +A registration takes `OnStart` and `OnStop` hooks and a `Go` worker, typed on +the service, and `app.Run(ctx)` starts everything in dependency order, waits for a signal, and stops it in reverse. Child scopes hold what belongs to one request or one test; a second registration of a key is rejected unless it says `Override()`; and `Explain`, `Graph` and `Modules` show what was built, @@ -132,7 +132,7 @@ registration and must be called before the scope is first resolved. | `.Override()` | Replace an earlier registration of `T` in this scope; a second one without it is rejected. | | `.OnStart(f)`, `.OnStop(f)` | Lifecycle hooks, `f` is `func(context.Context, T) error`. | | `.OnDrain(f)` | Runs before anything is stopped, while the scope still resolves. | -| `.Worker(f)` | A long-running function, cancelled on stop. | +| `.Go(f)` | A worker: a long-running function in a goroutine of its own, cancelled on stop. | To get a service back, call the scope, from a constructor or from outside: @@ -398,11 +398,11 @@ keeps the handlers' scopes alive until they return. #### Workers -`Worker` is for anything that loops until told to stop: consumers, pollers, -schedulers. +`Go` registers a worker, for anything that loops until told to stop: +consumers, pollers, schedulers. It is errgroup's `Go`, applied to a service. ```go -app.Wire[*Mailer](newMailer).Eager().Worker(func(ctx context.Context, m *Mailer) error { +app.Wire[*Mailer](newMailer).Eager().Go(func(ctx context.Context, m *Mailer) error { return m.Loop(ctx) // returns when ctx is cancelled }) ``` diff --git a/binding.go b/binding.go index 02eb351..9854808 100644 --- a/binding.go +++ b/binding.go @@ -378,19 +378,21 @@ func (b Binding[T]) OnStop(f func(context.Context, T) error) Binding[T] { return b.edit(func(b *binding) { b.onStop = func(ctx context.Context, v any) error { return f(ctx, as[T](v)) } }) } -// Worker registers a long-running function for T, such as a consumer loop. It is -// started in its own goroutine once the service starts and its context is -// cancelled when the service stops; Stop waits for it to return, bounded by -// its own context. A hook that outlasts that deadline is reported by Stop, -// and OnStop then waits for it rather than releasing the value underneath a +// Go registers a worker for T: a long-running function, such as a consumer +// loop, that runs in a goroutine of its own for as long as the service does. +// It is the contract of errgroup's Go, applied to a service. The worker +// starts once the service has started; its context is cancelled when the +// service stops, and Stop waits for it to return, bounded by Stop's own +// context. A worker that outlasts that deadline is reported by Stop, and +// OnStop then waits for it rather than releasing the value underneath a // worker still reading it. // // Returning a non-nil error calls Shutdown with it, stopping the application, // even if the scope was already stopping: a worker may fail, flush while the // scope winds down, and only then report. The exception is context.Canceled -// from a hook that was already cancelled, which is a worker reporting the -// cancellation and nothing else. A hook that wants to stay quiet during +// from a worker that was already cancelled, which is a worker reporting the +// cancellation and nothing else. A worker that wants to stay quiet during // shutdown should return nil. -func (b Binding[T]) Worker(f func(context.Context, T) error) Binding[T] { +func (b Binding[T]) Go(f func(context.Context, T) error) Binding[T] { return b.edit(func(b *binding) { b.worker = func(ctx context.Context, v any) error { return f(ctx, as[T](v)) } }) } diff --git a/concurrent_test.go b/concurrent_test.go index 0de8ef9..b7e0bb0 100644 --- a/concurrent_test.go +++ b/concurrent_test.go @@ -50,7 +50,7 @@ package di_test // // C9 and C10 exist because the third review found two more the same way. The // coverage the generators reached was 78% against the suite's 97%, and the -// whole gap was the lifecycle: no Worker hook, no Shutdown, no context expiring +// whole gap was the lifecycle: no Go hook, no Shutdown, no context expiring // mid-Stop, and starts kept strictly before stops. Everything the reviews // found lived in that gap. It is closed here. @@ -414,7 +414,7 @@ func (m *cmachine) register(s *di.Scope, o op) { } } -// errWorker is a Worker hook failing of its own accord rather than because it +// errWorker is a Go hook failing of its own accord rather than because it // was cancelled, which is what makes the container hand it to Shutdown. A // worker that only ever returns nil leaves that whole path unreached. var errWorker = errors.New("worker failed") @@ -519,16 +519,16 @@ func reg[T any](m *cmachine, s *di.Scope, o op, stop func(context.Context, any) switch o.reg % 6 { case 0: if o.wire { - b = s.Wire[T](wired).Worker(work).OnStop(down) + b = s.Wire[T](wired).Go(work).OnStop(down) } else { - b = s.Provide(build).Worker(work).OnStop(down) + b = s.Provide(build).Go(work).OnStop(down) } case 1: // Scoped through Wire puts a reflect.Call under every child build. if o.wire { - b = s.Wire[T](wired).Scoped().Worker(work).OnStop(down) + b = s.Wire[T](wired).Scoped().Go(work).OnStop(down) } else { - b = s.Provide(build).Scoped().Worker(work).OnStop(down) + b = s.Provide(build).Scoped().Go(work).OnStop(down) } case 2: build := func(sc *di.Scope) T { return own(sc, dep(sc)) } @@ -542,7 +542,7 @@ func reg[T any](m *cmachine, s *di.Scope, o op, stop func(context.Context, any) wrapped = s.Provide(build) } b = wrapped. - Worker(work). + Go(work). OnStop(func(ctx context.Context, v T) error { // Slow enough that an impatient Stop misses its deadline // here, which is the only way this driver reaches the @@ -561,11 +561,11 @@ func reg[T any](m *cmachine, s *di.Scope, o op, stop func(context.Context, any) // survived this driver. b = s.Provide(func(sc *di.Scope) T { return owe(sc, build(sc)) }). OnDrain(drainHook). - Worker(work). + Go(work). OnStop(down) case 4: // The one shape with an OnStart, so its stop step is owed only when - // the start step succeeded. Its Worker hook is what puts a worker under + // the start step succeeded. Its Go hook is what puts a worker under // a Stop that has to cancel it and wait. b = s.Provide(build). OnStart(func(_ context.Context, v T) error { @@ -574,7 +574,7 @@ func reg[T any](m *cmachine, s *di.Scope, o op, stop func(context.Context, any) m.owed.Store(any(v), m.holderOf(any(v))) return nil }). - Worker(work). + Go(work). OnStop(down) default: // Scoped *and* draining. Without this shape the driver could not put @@ -584,7 +584,7 @@ func reg[T any](m *cmachine, s *di.Scope, o op, stop func(context.Context, any) // drain defect was unreachable for want of one registration. b = s.Provide(func(sc *di.Scope) T { return owe(sc, build(sc)) }).Scoped(). OnDrain(drainHook). - Worker(work). + Go(work). OnStop(down) } if o.override { @@ -871,7 +871,7 @@ func (m *cmachine) run() { // release it still owed has happened. // // The second half cannot be a WaitGroup. A release deferred past a missed -// deadline is issued by a goroutine that first waits for the Worker hook to +// deadline is issued by a goroutine that first waits for the Go hook to // return, so between that hook finishing and the release starting there is a // moment when no hook is running and the work is still owed. Polling for the // owed set to empty is what closes that gap, and it keeps C9 to the property @@ -1022,7 +1022,7 @@ func TestMachineConcurrentSeeds(t *testing.T) { seeds := [][]byte{ {0, 1, 0, 1, 0, 1, 3, 0, 0, 0, 6, 1, 0, 0, 0, 6, 0, 0, 0, 0}, // scoped in gc, then stop c1 and root {0, 0, 0, 2, 1, 5, 0, 0, 0, 0, 1, 1, 0, 0, 0, 6, 0, 0, 0, 0}, // start racing a resolve from a child - {0, 0, 0, 3, 1, 5, 0, 0, 0, 0, 6, 1, 0, 0, 0, 6, 0, 0, 0, 0}, // a Worker hook, then overlapping stops + {0, 0, 0, 3, 1, 5, 0, 0, 0, 0, 6, 1, 0, 0, 0, 6, 0, 0, 0, 0}, // a Go hook, then overlapping stops {0, 0, 0, 4, 1, 1, 3, 0, 0, 0, 1, 1, 0, 0, 0, 5, 0, 0, 0, 0}, // late build racing Start's hook phase {0, 3, 0, 0, 0, 1, 3, 0, 0, 0, 6, 3, 0, 0, 0, 6, 1, 0, 0, 0, 6}, // stop the grandchild and its ancestors {0, 1, 0, 3, 0, 1, 1, 0, 0, 0, 6, 1, 0, 0, 0, 6, 0, 0, 0, 0}, // a drain hook in c1, then c1 and root stopped at once diff --git a/di.go b/di.go index 1b03e37..88fd9bf 100644 --- a/di.go +++ b/di.go @@ -54,7 +54,8 @@ // built. [Scope.Stop] first drains, which lets work already in flight finish // while the scope still resolves, then stops child scopes, then services in // reverse build order, and afterwards the scope refuses to resolve anything. -// [Binding.Worker] runs a long-lived function that is cancelled on stop. +// [Binding.Go] runs a worker, a long-lived function in a goroutine of its +// own, cancelled on stop. // [Scope.Run] ties it together // for a main function: start, wait for a signal or [Scope.Shutdown], stop // with a deadline. [Scope.Observe] reports every step for logging and @@ -144,7 +145,7 @@ const ( EventBuild EventKind = "build" // a constructor ran EventStart EventKind = "start" // an OnStart hook ran EventDrain EventKind = "drain" // an OnDrain hook ran - EventStop EventKind = "stop" // a Worker hook was cancelled and/or an OnStop hook ran + EventStop EventKind = "stop" // a worker was cancelled and/or an OnStop hook ran EventShutdown EventKind = "shutdown" // Shutdown was called ) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 4deb049..4adab9c 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -166,7 +166,7 @@ stateDiagram-v2 Built --> Starting: the scope is running, so OnStart runs Starting --> Started: OnStart returned Starting --> Failed: OnStart failed or panicked - Started --> Stopped: Stop, after OnDrain and any Worker + Started --> Stopped: Stop, after OnDrain and any worker Built --> Stopped: Stop, no OnStart was owed Failed --> [*]: served as an error to every later resolution Stopped --> [*] @@ -254,8 +254,8 @@ nothing is torn down while something that depends on it is still alive. And uses to finish in-flight requests: those requests still hold their scopes and everything under them, which would not be true from `OnStop`. -`Stop` is synchronous. It waits for start steps, drain hooks and `Worker` -functions it has cancelled. The single exception is its own context expiring, +`Stop` is synchronous. It waits for start steps, drain hooks and the workers +it has cancelled. The single exception is its own context expiring, in which case the missed deadline is reported to the caller and the release finishes on its own goroutine, reaching observers either way. A second `Stop`, concurrent or later, does not run a second teardown: it waits for the @@ -277,14 +277,15 @@ cancels the stop context, so a hung hook cannot keep the process alive. ``` Run(ctx) ── Start ──► running ──┬── SIGINT / SIGTERM ──┐ ├── s.Shutdown(cause) ─┼──► Stop(timeout) ──► return cause - └── a Worker returned ─┘ and every stop error + └── a worker returned ─┘ and every stop error ``` `Shutdown(cause)` never blocks, may be called from any goroutine, and propagates to ancestor scopes, so a service in a child can stop the application. The first cause wins and is what `Run` returns. -A `Worker` is a function that runs for as long as its service does. It is +A worker, registered with `Go`, is a function that runs for as long as its +service does, which is errgroup's `Go` applied to a service. It is started in its own goroutine as part of the start step, its context is cancelled by `Stop`, and `Stop` waits for it to return before `OnStop` runs and before anything it depends on is released. A worker that returns its own diff --git a/drain_test.go b/drain_test.go index a17bc9f..110fab5 100644 --- a/drain_test.go +++ b/drain_test.go @@ -485,7 +485,7 @@ func TestReview3DrainHookCanStopASiblingScope(t *testing.T) { // A Stop whose context runs out while another Stop's drain hook holds the // instance still owes the release: it took the instance off the scope's list, // so nothing else will reach it. The release is finished off the hook's own -// return, as it is for a Worker hook that outlasts the same deadline. +// return, as it is for a Go hook that outlasts the same deadline. // (review 3, 2) func TestReview3LostDrainWaitStillReleases(t *testing.T) { root := di.New() diff --git a/example_features_test.go b/example_features_test.go index 6eb8f8f..cc4440a 100644 --- a/example_features_test.go +++ b/example_features_test.go @@ -29,11 +29,11 @@ func ExampleBinding_Scoped() { type Queue struct{ jobs chan string } -func ExampleBinding_Worker() { +func ExampleBinding_Go() { app := di.New() done := make(chan string, 1) app.Provide(func(*di.Scope) *Queue { return &Queue{jobs: make(chan string, 1)} }).Eager(). - Worker(func(ctx context.Context, q *Queue) error { + Go(func(ctx context.Context, q *Queue) error { for { select { case job := <-q.jobs: diff --git a/examples/app/main.go b/examples/app/main.go index 2f67387..c06e943 100644 --- a/examples/app/main.go +++ b/examples/app/main.go @@ -1,5 +1,5 @@ // A complete service: request scopes through middleware, a background -// worker with a Worker hook, a health endpoint, and graceful shutdown. +// worker with a Go hook, a health endpoint, and graceful shutdown. package main import ( @@ -49,7 +49,7 @@ func main() { // DB it depends on is closed. Returning an error stops the application. app.Wire[*Mailer](func(*DB) *Mailer { return &Mailer{queue: make(chan string, 16)} }). Eager(). - Worker(func(ctx context.Context, m *Mailer) error { + Go(func(ctx context.Context, m *Mailer) error { for { select { case msg := <-m.queue: diff --git a/examples/guide/internal/mail/mail.go b/examples/guide/internal/mail/mail.go index 4b5baae..5810990 100644 --- a/examples/guide/internal/mail/mail.go +++ b/examples/guide/internal/mail/mail.go @@ -40,5 +40,5 @@ func (m *Mailer) Run(ctx context.Context) error { func Module(s *di.Scope) { s.Wire[*Mailer](New). Eager(). - Worker(func(ctx context.Context, m *Mailer) error { return m.Run(ctx) }) + Go(func(ctx context.Context, m *Mailer) error { return m.Run(ctx) }) } diff --git a/lifecycle.go b/lifecycle.go index db9a71e..8075c61 100644 --- a/lifecycle.go +++ b/lifecycle.go @@ -90,7 +90,7 @@ type instance struct { // concurrent builds visible. builder *resolver - // Worker hook bookkeeping, guarded by the phase machine rather than a mutex. + // Worker bookkeeping, guarded by the phase machine rather than a mutex. // cancel and runDone are written by start, on the goroutine that owns the // start step, and read by stop, which stopIfNeeded reaches only after // startClaimed has moved the phase past phaseStarting under the owning @@ -207,7 +207,7 @@ func callHook(hook func(context.Context, any) error, ctx context.Context, v any) return hook(ctx, v) } -// start runs OnStart and launches the Worker hook. The worker's context is +// start runs OnStart and launches the worker. The worker's context is // detached from ctx so the worker is cancelled by Stop, in dependency order, // rather than the moment the application context is cancelled. func (in *instance) start(ctx context.Context, owner *state) error { @@ -415,12 +415,12 @@ func (in *instance) drainIfNeeded(ctx context.Context, owner *state) (bool, erro return true, nil } -// stop cancels the Worker hook, waits for it within ctx, then runs OnStop. +// stop cancels the worker, waits for it within ctx, then runs OnStop. // -// A Worker hook that outlasts ctx still holds the value, so OnStop cannot run yet -// without racing the worker. The missed deadline is reported to the caller and -// the release is finished when the worker returns, as Stop does for a start -// step in flight. +// A worker that outlasts ctx still holds the value, so OnStop cannot run yet +// without racing it. The missed deadline is reported to the caller and the +// release is finished when the worker returns, as Stop does for a start step +// in flight. func (in *instance) stop(ctx context.Context, owner *state) error { b := in.b if in.cancel == nil && b.onStop == nil { @@ -436,7 +436,7 @@ func (in *instance) stop(ctx context.Context, owner *state) error { errs = append(errs, in.runErr) } case <-ctx.Done(): - err := fmt.Errorf("di: stopping %s: Worker hook did not return: %w", b.key, ctx.Err()) + err := fmt.Errorf("di: stopping %s: worker did not return: %w", b.key, ctx.Err()) if b.onStop == nil { owner.report(EventStop, b, t0, err) return err @@ -455,7 +455,7 @@ func (in *instance) stop(ctx context.Context, owner *state) error { return err } -// releaseAfterWorker finishes a stop step whose Worker hook outlasted Stop's +// releaseAfterWorker finishes a stop step whose worker outlasted Stop's // context, once the hook returns. missed is what Stop returned to its caller; // the instance's single EventStop is emitted here and carries it along with // the release's own result, so no observer sees a service stopped twice. @@ -628,7 +628,7 @@ func (s *Scope) Context() context.Context { // // Stop is synchronous. It waits out whatever another goroutine is still // running for a service it is tearing down -- a start step in flight, a drain -// hook another Stop began, a Worker hook being cancelled -- so when it returns, +// hook another Stop began, a worker being cancelled -- so when it returns, // the teardown has happened and its failures are in the error. A teardown // outlives the call only when ctx expires first: the missed deadline is // reported here, and the release is finished once the outstanding step diff --git a/machine_test.go b/machine_test.go index d0be80c..26461c0 100644 --- a/machine_test.go +++ b/machine_test.go @@ -21,7 +21,7 @@ package di_test // I4 A singleton is stable: two successful resolutions of a key from one // scope return the identical value. // I5 Nothing is stopped more often than it was built. -// I6 Once the root is stopped, every Worker hook has returned. +// I6 Once the root is stopped, every Go hook has returned. // I7 Explain and Graph render whatever state the sequence reached, // panicking only where a resolution from the same scope would, and // never deadlocking against the phase machine they read. @@ -151,7 +151,7 @@ type machine struct { starts map[string]int stops map[string]int - runsLive atomic.Int32 // Worker hooks currently executing + runsLive atomic.Int32 // Go hooks currently executing // values seen per (scope, key), to check singleton stability seen map[string]any @@ -560,7 +560,7 @@ func (m *machine) finish() { time.Sleep(5 * time.Millisecond) } if n := m.runsLive.Load(); n != 0 { - m.fail("%d Worker hooks still executing after the root was stopped", n) + m.fail("%d Go hooks still executing after the root was stopped", n) } } @@ -627,7 +627,7 @@ func regShape[T any](m *machine, s *di.Scope, o op, plain func() T, dep func(*di OnStart(hook("OnStart")).OnStop(hook("OnStop")) case 4: b = s.Provide(func(sc *di.Scope) T { return built(sc, plain()) }). - Worker(func(ctx context.Context, _ T) error { + Go(func(ctx context.Context, _ T) error { m.runsLive.Add(1) defer m.runsLive.Add(-1) <-ctx.Done() diff --git a/site/src/content/en.tsx b/site/src/content/en.tsx index dad2260..4a3386a 100644 --- a/site/src/content/en.tsx +++ b/site/src/content/en.tsx @@ -143,7 +143,7 @@ export const en: Content = { body: (f) => ( <>

- A Worker runs for as long as its service does: started in its own goroutine + A worker, registered with Go, runs for as long as its service does: started in its own goroutine when the service starts, cancelled by Stop, and waited for before anything it depends on is torn down. Returning an error from it stops the application.{' '} Eager says the mailer exists by the time Start returns rather than on diff --git a/site/src/content/ja.tsx b/site/src/content/ja.tsx index 0fd5d1f..596d616 100644 --- a/site/src/content/ja.tsx +++ b/site/src/content/ja.tsx @@ -139,7 +139,7 @@ export const ja: Content = { body: (f) => ( <>

- Worker + Goで登録した worker はそのサービスと同じだけ生きます。サービスの起動時に専用のゴルーチンで開始され、Stop でキャンセルされ、それが依存している何かが片付けられる前に完了を待たれます。ここからエラーを返すとアプリケーションが停止します。 Eagerは、メーラーが最初の利用時ではなくStart diff --git a/site/src/content/ru.tsx b/site/src/content/ru.tsx index 9ca015d..1053caa 100644 --- a/site/src/content/ru.tsx +++ b/site/src/content/ru.tsx @@ -145,7 +145,7 @@ export const ru: Content = { body: (f) => ( <>

- Worker живёт ровно столько, сколько его сервис: запускается в собственной + Воркер, зарегистрированный через Go, живёт ровно столько, сколько его сервис: запускается в собственной горутине, когда сервис стартует, отменяется по Stop, и его дожидаются прежде, чем начать останавливать то, от чего он зависит. Возврат ошибки из него останавливает приложение. Eager говорит, что мейлер существует уже к diff --git a/site/src/content/zh.tsx b/site/src/content/zh.tsx index 80c21d8..cc18c1d 100644 --- a/site/src/content/zh.tsx +++ b/site/src/content/zh.tsx @@ -128,7 +128,7 @@ export const zh: Content = { body: (f) => ( <>

- Worker与它的服务同寿:服务启动时它在自己的 goroutine 里启动,由Stop + 用Go注册的 worker 与它的服务同寿:服务启动时它在自己的 goroutine 里启动,由Stop 取消,并且在它依赖的任何东西被拆除之前会等它结束。从它返回错误会停止整个应用。Eager 表示邮件服务在Start返回时就已经存在,而不是等到第一次使用才创建。

diff --git a/teardown_test.go b/teardown_test.go index a4f35ce..9e4365d 100644 --- a/teardown_test.go +++ b/teardown_test.go @@ -104,12 +104,12 @@ func TestRegressionRollbackStopsChildren(t *testing.T) { } } -// Rollback must wait for Worker hooks even when the caller's context is done. +// Rollback must wait for Go hooks even when the caller's context is done. func TestRegressionRollbackAwaitsWorkerHook(t *testing.T) { returned := make(chan struct{}) s := di.New() s.Value(&Worker{}).Eager(). - Worker(func(ctx context.Context, w *Worker) error { + Go(func(ctx context.Context, w *Worker) error { <-ctx.Done() time.Sleep(50 * time.Millisecond) close(returned) @@ -125,7 +125,7 @@ func TestRegressionRollbackAwaitsWorkerHook(t *testing.T) { select { case <-returned: default: - t.Fatal("rollback returned without awaiting the Worker hook") + t.Fatal("rollback returned without awaiting the Go hook") } } @@ -135,7 +135,7 @@ func TestRegressionLateUndoHonoursDeadline(t *testing.T) { release := make(chan struct{}) s := di.New() s.Provide(func(*di.Scope) *Worker { close(building); <-release; return &Worker{} }). - Worker(func(ctx context.Context, w *Worker) error { time.Sleep(3 * time.Second); return nil }) + Go(func(ctx context.Context, w *Worker) error { time.Sleep(3 * time.Second); return nil }) if err := s.Start(context.Background()); err != nil { t.Fatal(err) } @@ -233,7 +233,7 @@ func TestStopFromAHookIsReported(t *testing.T) { }}, {"Run", func(s *di.Scope, got chan<- error) { s.Value(&DB{}).Eager(). - Worker(func(ctx context.Context, _ *DB) error { got <- s.Stop(ctx); <-ctx.Done(); return nil }) + Go(func(ctx context.Context, _ *DB) error { got <- s.Stop(ctx); <-ctx.Done(); return nil }) }}, } { t.Run(tc.name, func(t *testing.T) { @@ -293,7 +293,7 @@ func TestStopFromAHookWithItsOwnContextIsBounded(t *testing.T) { } // A Stop whose deadline expires while a start step is in flight must not -// orphan the instance: its Worker hook is cancelled and its OnStop runs. Stop +// orphan the instance: its Go hook is cancelled and its OnStop runs. Stop // waits for the step, so this is the deadline ending the caller's wait rather // than the teardown, which finishes on a goroutine of its own. // (pass 3) @@ -304,7 +304,7 @@ func TestRegressionExpiredStopDoesNotOrphan(t *testing.T) { s := di.New() s.Provide(func(*di.Scope) *Worker { return &Worker{} }). OnStart(func(context.Context, *Worker) error { close(entered); time.Sleep(150 * time.Millisecond); return nil }). - Worker(func(ctx context.Context, _ *Worker) error { <-ctx.Done(); close(runCancelled); return nil }). + Go(func(ctx context.Context, _ *Worker) error { <-ctx.Done(); close(runCancelled); return nil }). OnStop(func(context.Context, *Worker) error { close(stopped); return nil }) if err := s.Start(context.Background()); err != nil { t.Fatal(err) @@ -320,7 +320,7 @@ func TestRegressionExpiredStopDoesNotOrphan(t *testing.T) { select { case <-ch: case <-time.After(5 * time.Second): - t.Fatal("the instance was orphaned: its Worker hook or OnStop never ran") + t.Fatal("the instance was orphaned: its Go hook or OnStop never ran") } } } diff --git a/worker_test.go b/worker_test.go index 03d8ae3..602d428 100644 --- a/worker_test.go +++ b/worker_test.go @@ -1,6 +1,6 @@ package di_test -// Regressions in Worker hooks and Shutdown: how a worker's own failure reaches +// Regressions in Go hooks and Shutdown: how a worker's own failure reaches // the caller, and what may still be holding the value when OnStop wants it. // // One test per defect, named for the rule it pins. The tag at the end of a @@ -26,11 +26,11 @@ import ( "github.com/floatdrop/di" ) -// A Worker hook that dies on its own is reported by Stop, not only by Run. +// A Go hook that dies on its own is reported by Stop, not only by Run. func TestRegressionWorkerHookErrorReachesStop(t *testing.T) { boom := errors.New("queue disconnected") s := di.New() - s.Value(&Worker{}).Eager().Worker(func(context.Context, *Worker) error { return boom }) + s.Value(&Worker{}).Eager().Go(func(context.Context, *Worker) error { return boom }) if err := s.Start(context.Background()); err != nil { t.Fatal(err) } @@ -46,7 +46,7 @@ func TestRegressionWorkerHookErrorReachesStop(t *testing.T) { func TestRegressionRunErrorWrappingCanceled(t *testing.T) { s := di.New() s.Value(&Worker{}).Eager(). - Worker(func(ctx context.Context, _ *Worker) error { return fmt.Errorf("upstream dial: %w", context.Canceled) }) + Go(func(ctx context.Context, _ *Worker) error { return fmt.Errorf("upstream dial: %w", context.Canceled) }) done := make(chan error, 1) go func() { done <- s.Run(context.Background()) }() select { @@ -67,7 +67,7 @@ func TestReviewDetachedChildWorkerFailureReachesRun(t *testing.T) { child := root.Child("c") failed := make(chan struct{}) child.Provide(func(*di.Scope) *Worker { return &Worker{} }).Eager(). - Worker(func(context.Context, *Worker) error { defer close(failed); return errors.New("worker died") }) + Go(func(context.Context, *Worker) error { defer close(failed); return errors.New("worker died") }) runDone := make(chan error, 1) go func() { runDone <- root.Run(context.Background(), di.StopTimeout(time.Second)) }() @@ -95,7 +95,7 @@ func TestReviewDetachedChildWorkerFailureReachesRun(t *testing.T) { func TestReviewWorkerFailureIsNotDuplicated(t *testing.T) { boom := errors.New("queue disconnected") s := di.New() - s.Value(&Worker{}).Eager().Worker(func(context.Context, *Worker) error { return boom }) + s.Value(&Worker{}).Eager().Go(func(context.Context, *Worker) error { return boom }) err := s.Run(context.Background()) if !errors.Is(err, boom) { t.Fatalf("got %v", err) @@ -105,7 +105,7 @@ func TestReviewWorkerFailureIsNotDuplicated(t *testing.T) { } } -// Reported alongside the six: OnStop must not run while a Worker hook that +// Reported alongside the six: OnStop must not run while a Go hook that // outlasted Stop's context is still using the value. Stop reports the missed // deadline and the release follows the worker's own return. // (review 2, 9) @@ -117,7 +117,7 @@ func TestReview2OnStopWaitsForALiveWorkerHook(t *testing.T) { root := di.New() root.Value(&Worker{}).Eager(). - Worker(func(context.Context, *Worker) error { close(runLive); <-release; return nil }). + Go(func(context.Context, *Worker) error { close(runLive); <-release; return nil }). OnStop(func(context.Context, *Worker) error { select { case <-release: @@ -140,7 +140,7 @@ func TestReview2OnStopWaitsForALiveWorkerHook(t *testing.T) { } select { case <-stopped: - t.Fatal("OnStop ran while the Worker hook was still live") + t.Fatal("OnStop ran while the Go hook was still live") default: } close(release) @@ -151,7 +151,7 @@ func TestReview2OnStopWaitsForALiveWorkerHook(t *testing.T) { t.Fatal("the release never happened") } if overlap.Load() { - t.Fatal("OnStop ran while the Worker hook was still live") + t.Fatal("OnStop ran while the Go hook was still live") } } @@ -166,7 +166,7 @@ func TestReview3RunReportsAShutdownPublishedDuringStop(t *testing.T) { root := di.New() child := root.Child("worker") child.Value(&r3Worker{}). - Worker(func(ctx context.Context, _ *r3Worker) error { + Go(func(ctx context.Context, _ *r3Worker) error { <-ctx.Done() root.Shutdown(fail) return fail @@ -200,7 +200,7 @@ func TestReview4RunReportsACausePublishedDuringRollback(t *testing.T) { root := di.New() child := root.Child("worker") - child.Value(&Worker{}).Worker(func(ctx context.Context, _ *Worker) error { + child.Value(&Worker{}).Go(func(ctx context.Context, _ *Worker) error { <-ctx.Done() root.Shutdown(fail) return fail @@ -235,7 +235,7 @@ func TestReview4RunReportsACausePublishedDuringRollback(t *testing.T) { func TestWorkerFailureJoinedWithCancellationIsReported(t *testing.T) { failure := errors.New("flush failed") s := di.New() - s.Value(&Worker{}).Eager().Worker(func(ctx context.Context, _ *Worker) error { + s.Value(&Worker{}).Eager().Go(func(ctx context.Context, _ *Worker) error { <-ctx.Done() return errors.Join(ctx.Err(), failure) }) @@ -251,7 +251,7 @@ func TestWorkerFailureJoinedWithCancellationIsReported(t *testing.T) { // A wrapped cancellation, with nothing else in it, is still nothing to // report. quiet := di.New() - quiet.Value(&Worker{}).Eager().Worker(func(ctx context.Context, _ *Worker) error { + quiet.Value(&Worker{}).Eager().Go(func(ctx context.Context, _ *Worker) error { <-ctx.Done() return fmt.Errorf("loop: %w", ctx.Err()) }) diff --git a/workerhook_test.go b/workerhook_test.go index 02052c0..5c0ea4c 100644 --- a/workerhook_test.go +++ b/workerhook_test.go @@ -17,7 +17,7 @@ func TestWorkerHookLifecycle(t *testing.T) { stopped := make(chan struct{}) s := di.New() s.Provide(func(*di.Scope) *Worker { return &Worker{} }).Eager(). - Worker(func(ctx context.Context, w *Worker) error { + Go(func(ctx context.Context, w *Worker) error { log = append(log, "run") <-ctx.Done() close(stopped) @@ -35,7 +35,7 @@ func TestWorkerHookLifecycle(t *testing.T) { select { case <-stopped: default: - t.Fatal("Stop returned before the Worker hook was cancelled") + t.Fatal("Stop returned before the Go hook was cancelled") } if got := strings.Join(log, ","); got != "run,stop" { t.Fatalf("order %q", got) @@ -46,7 +46,7 @@ func TestWorkerHookFailureStopsApplication(t *testing.T) { boom := errors.New("queue disconnected") s := di.New() s.Provide(func(*di.Scope) *Worker { return &Worker{} }).Eager(). - Worker(func(ctx context.Context, w *Worker) error { return boom }) + Go(func(ctx context.Context, w *Worker) error { return boom }) done := make(chan error, 1) go func() { done <- s.Run(context.Background()) }() select { @@ -62,7 +62,7 @@ func TestWorkerHookFailureStopsApplication(t *testing.T) { func TestWorkerHookErrorAfterCancelIsReportedByStop(t *testing.T) { flushFailed := errors.New("flush failed") s := di.New() - s.Value(&Worker{}).Eager().Worker(func(ctx context.Context, w *Worker) error { <-ctx.Done(); return flushFailed }) + s.Value(&Worker{}).Eager().Go(func(ctx context.Context, w *Worker) error { <-ctx.Done(); return flushFailed }) if err := s.Start(context.Background()); err != nil { t.Fatal(err) } @@ -73,7 +73,7 @@ func TestWorkerHookErrorAfterCancelIsReportedByStop(t *testing.T) { func TestWorkerHookIgnoringCancelHitsStopTimeout(t *testing.T) { s := di.New() - s.Value(&Worker{}).Eager().Worker(func(ctx context.Context, w *Worker) error { + s.Value(&Worker{}).Eager().Go(func(ctx context.Context, w *Worker) error { time.Sleep(2 * time.Second) return nil }) @@ -92,7 +92,7 @@ func TestWorkerHookStartsForLateBuiltService(t *testing.T) { running := make(chan struct{}) s := di.New() s.Provide(func(*di.Scope) *Worker { return &Worker{} }). - Worker(func(ctx context.Context, w *Worker) error { close(running); <-ctx.Done(); return nil }) + Go(func(ctx context.Context, w *Worker) error { close(running); <-ctx.Done(); return nil }) if err := s.Start(context.Background()); err != nil { t.Fatal(err) } @@ -100,7 +100,7 @@ func TestWorkerHookStartsForLateBuiltService(t *testing.T) { select { case <-running: case <-time.After(time.Second): - t.Fatal("Worker hook not started for a service built after Start") + t.Fatal("Go hook not started for a service built after Start") } if err := s.Stop(context.Background()); err != nil { t.Fatal(err) @@ -121,7 +121,7 @@ func TestWorkerHookFailureDecidedBeforeCancelReachesRun(t *testing.T) { root := di.New() child := root.Child("c") child.Provide(func(*di.Scope) *Worker { return &Worker{} }).Eager(). - Worker(func(ctx context.Context, _ *Worker) error { + Go(func(ctx context.Context, _ *Worker) error { close(failed) // the failure is decided here <-ctx.Done() // the worker flushes while the scope winds down return boom // and is reported here @@ -152,7 +152,7 @@ func TestWorkerHookFailureDecidedBeforeCancelReachesRun(t *testing.T) { func TestWorkerHookCancellationIsNotAFailure(t *testing.T) { root := di.New() root.Value(&Worker{}).Eager(). - Worker(func(ctx context.Context, _ *Worker) error { <-ctx.Done(); return ctx.Err() }) + Go(func(ctx context.Context, _ *Worker) error { <-ctx.Done(); return ctx.Err() }) if err := root.Start(context.Background()); err != nil { t.Fatal(err) }