Bounded asynchronous execution
@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.