Skip to main content
Version: 1.2

4. Runtime Pipeline

Read this chapter's framing note before the diagrams. This framework's own architecture audit (finding AP-01, see §20 — Appendix) found that the pipeline's own XML doc comment describes a richer, fully-staged execution sequence than the shipped code actually implements. This chapter documents both: what IBackgroundJobExecutionPipeline actually does today (§4.2), and the fuller conceptual stage sequence a job author can manually compose around it (§4.3) — clearly labeled as manual composition, not automatic pipeline behavior, because presenting it otherwise would misrepresent the current source.

4.1 The concepts, in the order the user-facing stage sequence is usually described

Scheduler → Pipeline → Execution Context → Tenant Context → Distributed Lock → Executor → Adapter → Business Service → Result → Completion

Every one of these ten concepts is a real, working, tested component in this framework. What this section exists to make precise is which of these steps happen automatically inside the pipeline, and which ones a job author must wire up explicitly — because the framework's own pipeline does not chain all ten automatically.

4.2 What IBackgroundJobExecutionPipeline.RunAsync actually does (verified from source)

That is the entire body of the shipped BackgroundJobExecutionPipeline.RunAsync: a cancellation fast-path, a lifetime timer that is started but whose elapsed value is never consumed by anything, an invocation of the caller-supplied next delegate, and exception classification that computes a category and then discards it before re-throwing. It does not itself resolve or call IBackgroundJobTenantContext, IBackgroundJobDistributedLock, IBackgroundJobLoggingAdapter, or IBackgroundJobNotificationAdapter anywhere. Those are genuine, working services — they simply are not automatically chained around every execution by the pipeline itself.

4.3 The fuller conceptual sequence — manually composed by the caller, not automatic

What is genuinely automatic, for every job routed through the pipeline: context construction (by the bridge, always required), the pipeline's cancellation/classification/re-throw behavior, and the executor's call into the job adapter.

What is opt-in, composed manually per call site, and currently unused by every one of the six migrated production jobs: tenant context resolution and distributed-lock acquisition. Both are fully working, independently tested capabilities — they are simply not needed by any job migrated so far, since all six operate host-level (tenantId: null) and rely on a business-layer atomic claim (ClaimAsync) rather than a job-level lock (see §9.4). A future job that genuinely needs per-tenant scoping or a coarse job-level mutual exclusion would compose these around its own pipeline.RunAsync call explicitly, following the E2E test suite's own RuntimeTestHelpers.RunPipelineAsync pattern as the reference example.

4.4 The result and completion model

BackgroundJobExecutionResult is a sealed record: (bool Success, BackgroundJobStatus Status, DateTimeOffset CompletedOn, string? ErrorMessage = null, IReadOnlyDictionary<string,string>? Data = null), with static factories Succeeded(data?), Failed(msg), Cancelled(), RetryRequired(reason), and PermanentFailure(msg).

RetryRequired/PermanentFailure exist as forward-looking hooks — no code in either repository currently calls them outside unit tests, since Hangfire's own AutomaticRetry state machine is what actually decides whether to retry, driven by whether an exception escaped, not by inspecting this result's Status.

4.5 Registration invariant that keeps this honest

IBackgroundJobExecutionPipeline, IBackgroundJobScheduler, IBackgroundJobTenantContext, and IBackgroundJobDistributedLock are all registered in DI (AddBackgroundJobsFrameworkCore), so they are always available to be composed. Whether a given job composes them is a decision made in that job's *PipelineJob bridge class — never something the framework itself decides on the job's behalf. This is a deliberate consequence of the framework owning generic capability, not job-specific policy — see §1.4.