.NET API
IJobHandler
Tracon.Abstractions.dllThe extension point that provides job execution logic for one handler key.
public interface IJobHandlerRemarks
Section titled “Remarks”Registered with AddJobHandler<T>(handlerKey): the key lives on
the registration, not on the type, so the same type can serve two keys and
a duplicate key is caught at startup. The background worker looks the key
up by exact, ordinal match, so registration order never decides
which handler runs — a consumer handler cannot shadow a built-in
one, and a built-in one cannot shadow a consumer’s.
Lifetime: the handler is registered scoped and
resolved from a fresh dependency-injection scope per execution. Scoped
dependencies are therefore safe to take in the constructor: two jobs
running in parallel, and two attempts of the same job, never share an
instance.
Tenant: runs under the ambient tenant — the worker
opens the job’s own tenant scope before calling
IJobHandler.ExecuteAsync, so an ITenantContext the handler
resolves reports JobRecord.TenantId, not the process default.
The record is also on JobContext.Job for a handler that
prefers to read it explicitly.
builder.Services.AddJobHandler<NightlyReportJobHandler>("contoso.nightly-report");Methods
Section titled “Methods”ExecuteAsync(JobContext, CancellationToken)
Section titled “ ExecuteAsync(JobContext, CancellationToken)”Executes the job.
ValueTask ExecuteAsync(JobContext context, CancellationToken cancellationToken = default)Parameters
Section titled “Parameters”context JobContext
The job context: record, items, reporting, and cancellation check.
cancellationToken CancellationToken
The cancellation token. Cancelled when the worker shuts down, when the job is
cancelled (POST /api/jobs/{id}/cancel), and when the attempt reaches the
job’s time limit.
Returns
Section titled “Returns”The completion task.
Remarks
Section titled “Remarks”Stop when the token is cancelled. The worker cancels it for three reasons and tells them apart itself:
- The worker is shutting down: the job is left to its lease and is leased again later, as after a crash.
- The job was cancelled: the worker reads the job’s status every
TraconSchedulingOptions.PollInterval(ten seconds by default) while the handler runs, and cancels the token when it finds the job JobStatus.Cancelled or gone. The job stays cancelled whatever the handler does next. - The attempt ran past JobRecord.MaxDuration (or
TraconSchedulingOptions.MaxDurationByHandlerKey): a handler that throws then fails the attempt with JobErrorCodes.MaxDurationExceeded, and the job is leased again while attempts are left. One that returns normally completes the job.
The token is cooperative: a handler that never reads it is not stopped. Its worker waits for it, and its slot stays taken until it returns.
Execution is at-least-once, not exactly-once. The same job — the same JobContext.Job, with the SAME unfiltered JobContext.Items list — can reach IJobHandler.ExecuteAsync more than once: if the handler throws, the job is retried with IJobStore.ReleaseForRetryAsync (if the attempt limit is not exceeded) or marked as JobStatus.Failed; if the process crashes or its lease lapses, another worker (or the same one) re-leases the SAME job and calls IJobHandler.ExecuteAsync again from scratch. Neither case resets item progress. A store error after IJobHandler.ExecuteAsync has returned is not a handler failure: the worker tries the write of the outcome again while it still holds the lease. Only a write that keeps failing until then leaves the job to the lease-lapse case above.
A running handler is never interrupted by its own lease.
The worker renews the lease on a timer for as long as
IJobHandler.ExecuteAsync has not returned, and it renews without a
limit. A job with no time limit (JobRecord.MaxDuration
unset, and no entry for its key in
TraconSchedulingOptions.MaxDurationByHandlerKey) can therefore run
for ever, and a handler that ignores its token holds its worker slot even
past its limit. That slot counts against the worker’s concurrency limit,
so no replica can take the work instead. Give any handler that calls out
to the network its own timeout as well.
Because of this, a handler with side effects (sending an email,
charging a payment, calling an external API) MUST be idempotent, or
MUST check JobItemRecord.Status itself and skip any item
that is not JobItemStatus.Pending — see
JobContext.Items. The handlers Tracon ships all do
the latter, and the reusable JobHandlerContract in
Tracon.Testing.Contracts.Xunit asserts it.