Skip to main content
Version: 1.2

5. Execution Flow

5.1 Fire-and-forget flow

The framework supports this structurally (IBackgroundJobScheduler.EnqueueAsyncIHangfireBackgroundJobAdapter.EnqueueAsyncIBackgroundJobClient.Enqueue), but no currently-migrated production job uses it — all six migrated jobs are recurring. The legacy Product Import fire-and-forget flow (ProductService.StartImportBackgroundAsyncIBackgroundJobService.Enqueue<IProductImportBackgroundService>) still runs entirely on the pre-framework Hangfire filter-chain path and has not been migrated — see §14.5.

5.2 Recurring job flow — the only flow currently in production for this framework

Registered via a direct, imperative Hangfire.RecurringJob.AddOrUpdate<TPipelineJob>(recurringId, job => job.RunAsync(ct), cron, ...) call in the ERP host's ApplicationBuilderExtensions.csnot via IBackgroundJobScheduler. This is worth being explicit about: the scheduler abstraction exists and is registered in DI, but every one of the six migrated jobs' recurring registration still uses raw Hangfire directly, pointed at the new *PipelineJob bridge class instead of the old worker class. See §10 — Scheduler for why.

5.3 Failure flow

A business exception thrown inside the real worker service propagates unmodified through *JobAdapter.RunAsyncBackgroundJobExecutor.ExecuteAsync (no catch there) → the pipeline's catch (Exception ex) classifies it (discarded — see AP-02) → re-throws → back up through *PipelineJob.RunAsync → Hangfire sees the exception and applies its own default AutomaticRetry.

One documented, deliberate exception to this shape: CampaignWorkflowExecutorService (Notification Framework Core) catches per-execution exceptions internally — logging and continuing its own loop — so that a single bad campaign execution never fails the whole Hangfire job. The CampaignWorkflowExecutorPipelineJob bridge only ever sees an exception escape if the service's own outer call (e.g. GetPendingAsync) fails, not from a per-row dispatch failure inside the loop.

5.4 Cancellation flow

The pipeline has a dedicated catch (OperationCanceledException) { throw; } fast path — the failure classifier is deliberately not invoked for cancellation, so cancelling a job never consumes retry budget. Verified behavior: a pre-cancelled token throws before the job ever runs (and if a lock had been requested, it is never acquired); cancellation raised mid-execution propagates the same way, and if a lock was held, it is still released correctly (via await using on the lock handle).

5.5 Exception flow

There is no flow distinct from Failure — the pipeline's exception handling is the failure-classification/ retry-signal mechanism. The only real branch at the pipeline's catch boundary is cancellation vs. everything else.

5.6 Retry flow

Entirely Hangfire's own, unmodified AutomaticRetry behavior — there is no custom retry engine anywhere in either the framework or the ERP host's integration of it. The framework's only contribution is the (currently unconsumed) BackgroundJobFailureCategory classification and the RetryRequired/ PermanentFailure result factories, both of which exist as forward-looking hooks with no current caller outside unit tests. Each retry is Hangfire re-invoking the job method from scratch: a fresh DI scope, a fresh BackgroundJobExecutionContext, a fresh job adapter and executor — no cross-attempt state persists inside the framework between retries. No job in the six migrations configures an explicit [AutomaticRetry(...)] policy — this is a documented, open gap (AP-04), not a considered choice per job.

5.7 What's shared across every flow, and what differs

Shared structural guarantees: never swallow an exception the caller needs to see; always classify before re-throwing (except cancellation, which skips classification entirely); always start a BackgroundJobExecutionLifetime timer (currently write-only — nothing consumes its Elapsed value yet).

What differs between flows: only the exception type encountered at the pipeline's catch boundary, and whether the specific *PipelineJob chose to manually compose tenant-context resolution or a distributed lock around its executor call — which, as of today, none of the six production jobs do.