- enqueue middleware wraps one logical producer call and controls whether, how, and how many times the inner enqueue operation runs;
- insert hooks observe the begin and end of each actual call to the store;
- plugins keep related middleware and hooks together and optionally scope them to one or more job kinds.
Headgate’s current plugin boundary is producer-side. It does not wrap handler execution.
Execution telemetry uses the worker or runner telemetry interface, while death handlers,
periodic hooks, and subscriptions have their own lifecycle-specific APIs.
The producer pipeline
The first registered middleware is the outermost wrapper. Authorization evaluates the final envelope after middleware enrichment, and insert hooks run only when the request actually reaches the store.Choose the right extension point
Prefer a hook when observation is enough. Use middleware only when the extension needs
control flow, request mutation, a propagated context, or a before/after frame.
Enqueue middleware
Middleware receives an ownedEnqueueRequest and an EnqueueNext. Headgate deep-clones
the caller’s envelope batch before entering the chain, including payloads, unique keys,
and headers. A middleware mutation therefore affects authorization and durable storage
without changing caller memory.
Calling next has precise meaning:
- zero calls intentionally veto the operation;
- one call performs the normal inner chain;
- multiple calls perform multiple inner attempts and therefore require idempotent IDs or unique keys and classified transient errors.
Rust middleware
ClientError::Middleware(EnqueueMiddlewareError::new(...)). Use
EnqueueMiddlewareFn when a named type would add no clarity.
Go middleware
errors.Is and errors.As.
The context passed to next.Run reaches inner middleware, authorization, hooks, and the
store. Authentication should already be established by the embedding HTTP or RPC stack;
do not manufacture an identity from an untrusted envelope header.
Insert hooks
Insert hooks are synchronous, non-wrapping observers. They have nonext, cannot mutate
the request, and cannot replace the store result. They run in registration order for
both phases; unlike middleware, end hooks do not unwind in reverse.
For middleware A and hooks H1, H2, one successful inner call is:
next twice and both calls reach the store, hooks receive two complete
begin/end lifecycles.
End hooks classify results as:
Hooks are not a durable audit log. A process abort can prevent an end callback, and a
hook panic follows ordinary language panic semantics. Keep hooks non-panicking and
locally bounded; hand network export to a bounded asynchronous pipeline.
Define and install a plugin
A plugin has a non-empty name, an immutable scope, and zero or more middleware and hooks. The name identifies the configuration but is not automatically added to job data or telemetry.Rust plugin
Plugin::global for every job kind, Plugin::for_kind for one kind, or
Plugin::for_kinds for several kinds. Construction validates job-kind syntax,
deduplicates the kind set, and rejects an empty plugin name or empty scoped set.
Go plugin
WithPluginKinds to create a global Go plugin. Invalid configuration returns a
*headgate.PluginConfigError that unwraps to headgate.ErrInvalidPlugin.
Global and job-kind scope
Plugin scope is evaluated against an atomic batch:- a global plugin always activates;
- a scoped plugin activates when any envelope in the batch matches one configured kind;
- once activated, every component in that plugin sees the complete batch;
- Headgate never splits a mixed-kind batch to apply a plugin because that would break the store’s all-or-nothing enqueue contract.
mail.send also wraps this entire two-job batch:
Ordering multiple plugins
Given standalone middlewareS, global plugins G1, G2, and scoped plugins K1,
K2, the middleware nesting order is:
Direct, bulk, transactional, and HTTP behavior
The configuredClient applies the same middleware/plugin/hook stack to ordinary and
batch enqueue. Transactional enqueue uses the same chain and selects the transactional
store terminal; changing EnqueueOperation inside middleware cannot switch terminals.
To install the stack on the control API:
Store::enqueue or Store.Enqueue is a trusted low-level bypass and does not run
client extensions. Inside handlers, configure WorkerConfig.producer or
headgate.Config.Producer with the same client if follow-on jobs must retain application
authorization, middleware, hooks, plugins, and circuit behavior.
Failure and retry rules
Middleware may intentionally return beforenext, but it should return a meaningful
error so callers can distinguish policy veto from success. If it retries:
- retry only errors known to be transient;
- preserve job IDs, unique keys, and the original transactional boundary;
- use bounded attempts and backoff;
- remember that every successful trip to the store produces another hook lifecycle;
- never invoke a caller-owned transaction concurrently.
Test extensions
Use compile-time assertions so signature drift cannot silently disable a Go component:Arc<dyn EnqueueMiddleware> or Arc<dyn InsertHook>. Unit tests should additionally
record the call sequence and assert:
- outer middleware enters first and exits last;
- hooks remain in forward order at both phases;
- a veto produces no authorization, circuit, hook, or store call;
- scoped plugins ignore non-matching batches and receive the whole matching batch;
- direct and transactional calls use the same extension chain;
- middleware changes the stored clone but not the caller’s original envelope.
Current boundaries
- Plugins contain producer middleware and insert hooks only; there is no worker-middleware plugin interface today.
- Plugin registration and configuration are process-local and are not persisted with a job.
- A scoped plugin matches job kind, not queue, tenant, partition, or arbitrary payload.
- Insert hooks are synchronous observers, not durable events.
- Plugins cannot weaken or replace atomic store admission policy.
OpenTelemetry
Propagate trace context with middleware and export worker signals.
Enqueueing
See how producer validation, authorization, and storage fit together.