Skip to content
Spice Framework on GitHub

Bounded asynchronous execution

frameworkMaturity: previewSource: spice@c0641b3Exact reviewed source

@async.Execute is compile-time provider-owned method metadata. The compiler accepts only exported non-generic, non-variadic func(receiver)(context.Context, arguments...) error methods owned by one exact provider output. It rejects unnameable argument types and generated submit-method collisions with source positions, preserves argument types in immutable application IR, and never executes the method body.

Generation creates one application-owned executor after all providers and registers its shutdown immediately, so accepted tasks drain before provider cleanup. Every annotated method becomes a typed generated API:

err := application.SubmitMailerSend(
admissionContext,
Message{OrderID: "order-42"},
)

The application must be started and ready before submission. The admission context controls backpressure only; the generated executor supplies its caller-owned lifetime context to the provider method. ApplicationOptions accepts AsyncContext and AsyncObservers, while spice.async.max-concurrency (environment SPICE_ASYNC_MAX_CONCURRENCY) configures the positive bounded concurrency and defaults to 16. Application.AsyncSnapshot() returns bounded aggregate state. Task arguments follow ordinary Go ownership rules and are not deep-copied.

async.Executor is an instance-owned, lifecycle-scoped alternative to unbounded go statements:

executor, err := async.NewExecutor(applicationContext, 16, taskObserver)
err = executor.Submit(admissionContext, async.Definition{
ID: "orders.SendConfirmation",
Module: "example.com/shop/orders",
}, sendConfirmation)

The execution context belongs to the application and is supplied to every accepted task. The admission context bounds how long a producer waits for a slot. At most the configured number of task goroutines exist; Submit applies backpressure instead of creating an implicit queue.

Shutdown atomically closes admission and waits for accepted tasks. Concurrent shutdown calls share one terminal result. Task failures are joined in submission order, regardless of completion order. If the shutdown context ends, the executor cancels task contexts and returns; tasks that ignore cancellation may continue, which remains visible through Done.

A panic cannot cross an asynchronous call boundary back to the submitter. The executor therefore contains it, reports *async.PanicError, and deliberately omits the recovered value from errors and observations. Snapshots and observers expose only stable task/module identity and bounded result metadata.

Generated applications can provide Shutdown as a lifecycle cleanup. The executor starts no worker, scheduler, or maintenance goroutine during construction; its only non-task goroutine is created when explicit shutdown needs a context-selectable wait.