Skip to content
bitzorcas
中EN

Reference

Database-Driven Background Jobs and Quartz Execution Safety

Say goodbye to fragile Linux cron scripts! Learn how BitzOrcas.Modern implements scheduler-agnostic IJobExecutor, Quartz clustered persistence, and graceful execution shutdown.

Last updated

In distributed enterprise architectures, naive background processing introduces severe operational hazards:

  • Fragile External Cron Scripts: Relying on server-level Linux cron jobs calling curl endpoints, which fail when nodes scale or drift;
  • In-Memory Timer Overruns: System.Threading.Timer triggering new iterations before previous runs finish, exhausting database pools;
  • Duplicate Multi-Instance Execution: Deploying 3 API instances causes all 3 to run “Monthly Billing” at midnight, billing customers three times!

BitzOrcas.Modern solves this with a database-backed Quartz cluster + scheduler-agnostic IJobExecutor<T> architecture: Business logic remains pure, while clustered database leases guarantee single-instance execution and automated failover.

Background Job Execution Flow

Single-Instance Lease

1. BackgroundJobCatalog (Loads Declarations)

2. Clustered Quartz Scheduler

3. IJobExecutorDispatcher (Locates Binding)

4. IJobExecutor (Pure Business Execution)

5. Database Persistence & Cursor Update

6. QuartzJobExecutionAuditor (Records Latency & State)


Step 1: Defining Typed Job Identities and Agnostic Executors

Core rule in BitzOrcas: Business handlers must never reference Quartz IJob or JobExecutionContext directly.

Define a typed identity alongside its IJobExecutor<T>:

AuditRetentionJobExecutor.cs: Typed Job Executor
using BitzOrcas.Application.Abstractions.Jobs;
using BitzOrcas.Domain.Results;
namespace BitzOrcas.Auditing.Application.Jobs;
// 1. Declare immutable job identity
public static class AuditJobIdentities
{
public sealed class RetentionPrune : IBackgroundJobIdentity
{
public static string JobName => "audit-retention"; // matches the built-in BackgroundJobIdentities entry
}
}
// 2. Pure business executor: depends only on application ports, zero Quartz dependencies
public sealed class AuditRetentionJobExecutor(
IAuditRetentionPort retentionPort,
IAppClock clock) : IJobExecutor<AuditJobIdentities.RetentionPrune>
{
public async Task<Result> ExecuteAsync(CancellationToken ct)
{
// Calculate retention cutoff (prune records older than 180 days)
var cutoffTime = clock.UtcNow.AddDays(-180);
// Execute batch pruning via domain port
await retentionPort.PruneExpiredLogsAsync(cutoffTime, ct);
return Result.Success();
}
}

Step 2: Declarative Configuration in JobHost

Configure execution schedules declaratively in appsettings.json:

appsettings.json: Declarative Job Scheduling
{
"BackgroundJobs": {
"audit-retention": {
"Enabled": true,
"CronExpression": "0 0 2 * * ?",
"IntervalSeconds": 86400
// The override path reads only CronExpression/IntervalSeconds/Enabled;
// Description belongs to module declaration and cannot be overridden.
// IntervalSeconds and CronExpression are mutually exclusive (fail-loud).
}
}
}
  • Compile-Time Safety: Unbound job identities cause fast startup failures instead of runtime exceptions;
  • Zero-Code Schedule Updates: Modify cron triggers via environment variables without rebuilding binaries.

Step 3: Execution Safety and Graceful Shutdown

Long-running jobs must respect two principles:

  • Cursor Batching: Process records in batches of 1,000, committing progress to prevent huge memory footprints;
  • Cancellation Respect: Inspect cancellationToken.IsCancellationRequested to safely stop at batch boundaries on container termination (SIGTERM).

Summary

BitzOrcas background processing delivers operational predictability:

  • Scheduler-Agnostic: Pure business handlers run in unit tests in microseconds;
  • Clustered Locking: Database locks eliminate duplicate concurrency;
  • Auditing by Default: Full execution history automatically feeds operations dashboards.

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%