# IJobHandler

> Tracon documentation. Tracon is a .NET package family that adds a control
> plane on Microsoft Agent Framework, published as NuGet packages that run
> inside your own ASP.NET Core application.
>
> Page: https://tracon.dev/api/tracon.ijobhandler/
> Summary: IJobHandler: The extension point that provides job execution logic for one handler key.

<div class="api-provenance">Namespace Tracon · Assembly <code>Tracon.Abstractions.dll</code></div>

The extension point that provides job execution logic for one handler key.

```csharp
public interface IJobHandler
```

## 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](/api/tracon.ijobhandler/#Tracon_IJobHandler_ExecuteAsync_Tracon_JobContext_System_Threading_CancellationToken_), so an `ITenantContext` the handler
resolves reports [JobRecord.TenantId](/api/tracon.jobrecord/#Tracon_JobRecord_TenantId), not the process default.
The record is also on [JobContext.Job](/api/tracon.jobcontext/#Tracon_JobContext_Job) for a handler that
prefers to read it explicitly.

```csharp
builder.Services.AddJobHandler<NightlyReportJobHandler>("contoso.nightly-report");
```

## Methods

### <a id="Tracon_IJobHandler_ExecuteAsync_Tracon_JobContext_System_Threading_CancellationToken_"></a> ExecuteAsync\(JobContext, CancellationToken\)

Executes the job.

```csharp
ValueTask ExecuteAsync(JobContext context, CancellationToken cancellationToken = default)
```

#### Parameters

`context` [JobContext](/api/tracon.jobcontext/)

The job context: record, items, reporting, and cancellation check.

`cancellationToken` [CancellationToken](https://learn.microsoft.com/dotnet/api/system.threading.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

 [ValueTask](https://learn.microsoft.com/dotnet/api/system.threading.tasks.valuetask)

The completion task.

#### 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](/api/tracon.jobstatus/) or gone. The job stays cancelled whatever the handler does next.
- The attempt ran past [JobRecord.MaxDuration](/api/tracon.jobrecord/#Tracon_JobRecord_MaxDuration) (or `TraconSchedulingOptions.MaxDurationByHandlerKey`): a handler that throws then fails the attempt with [JobErrorCodes.MaxDurationExceeded](/api/tracon.joberrorcodes/#Tracon_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](/api/tracon.jobcontext/#Tracon_JobContext_Job), with the SAME
unfiltered [JobContext.Items](/api/tracon.jobcontext/#Tracon_JobContext_Items) list — can reach
[IJobHandler.ExecuteAsync](/api/tracon.ijobhandler/#Tracon_IJobHandler_ExecuteAsync_Tracon_JobContext_System_Threading_CancellationToken_) more than once: if the handler throws, the
job is retried with [IJobStore.ReleaseForRetryAsync](/api/tracon.ijobstore/#Tracon_IJobStore_ReleaseForRetryAsync_System_Guid_System_String_System_Nullable_System_TimeSpan__System_Threading_CancellationToken_) (if
the attempt limit is not exceeded) or marked as
[JobStatus.Failed](/api/tracon.jobstatus/); if the process crashes or its lease
lapses, another worker (or the same one) re-leases the SAME job and
calls [IJobHandler.ExecuteAsync](/api/tracon.ijobhandler/#Tracon_IJobHandler_ExecuteAsync_Tracon_JobContext_System_Threading_CancellationToken_) again from scratch. Neither case
resets item progress. A store error after [IJobHandler.ExecuteAsync](/api/tracon.ijobhandler/#Tracon_IJobHandler_ExecuteAsync_Tracon_JobContext_System_Threading_CancellationToken_)
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](/api/tracon.ijobhandler/#Tracon_IJobHandler_ExecuteAsync_Tracon_JobContext_System_Threading_CancellationToken_) has not returned, and it renews without a
limit. A job with no time limit ([JobRecord.MaxDuration](/api/tracon.jobrecord/#Tracon_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](/api/tracon.jobitemrecord/#Tracon_JobItemRecord_Status) itself and skip any item
that is not [JobItemStatus.Pending](/api/tracon.jobitemstatus/) — see
[JobContext.Items](/api/tracon.jobcontext/#Tracon_JobContext_Items). The handlers Tracon ships all do
the latter, and the reusable `JobHandlerContract` in
`Tracon.Testing.Contracts.Xunit` asserts it.
