Background Jobs

This guide covers background processing in Wallow using Hangfire for scheduled and recurring jobs, alongside Wolverine for event-driven processing.

Overview

Wallow uses Hangfire for scheduled and recurring background jobs, backed by PostgreSQL. Hangfire provides persistent job storage, a dashboard UI, automatic retries, cron scheduling, and fire-and-forget execution.

Use Hangfire for time-based work: scheduled tasks, recurring cron jobs, deferred execution, and jobs not triggered by events.

Use Wolverine for event-driven work: reacting to domain events, cross-module integration events, immediate command processing, and saga orchestration.

Hangfire Configuration

Service Registration

Hangfire is configured via AddHangfireServices in api/src/Wallow.Api/Extensions/HangfireExtensions.cs. It connects to PostgreSQL using the DefaultConnection connection string and stores jobs in the hangfire schema.

The Hangfire server is registered with shutdown/stop timeouts for graceful termination during deployments.

Dashboard

The Hangfire dashboard is available at /hangfire, protected by HangfireDashboardAuthFilter (api/src/Wallow.Api/Middleware/HangfireDashboardAuthFilter.cs). The filter requires an authenticated user holding the AdminAccess permission — it checks User.GetPermissions(), not a role claim, so a user whose roles expand to AdminAccess passes and a user merely named admin does not.

Anonymous access is not environment-driven. It is the Hangfire:AllowAnonymousDashboard configuration flag, read in UseHangfireDashboard and passed to the filter's constructor. The filter's own comment explains why: a configuration flag means no environment name can turn the dashboard open by accident. Local development sees an open dashboard only because api/src/Wallow.Api/appsettings.Development.json sets that flag to true.

Configuration URL Access
Hangfire:AllowAnonymousDashboard: true (the shipped Development default) http://localhost:5001/hangfire Open to all
Flag absent or false (every other environment) https://your-domain/hangfire AdminAccess permission required

IJobScheduler Abstraction

Wallow provides an IJobScheduler abstraction in api/src/Shared/Wallow.Shared.Kernel/BackgroundJobs/IJobScheduler.cs for enqueuing and scheduling jobs without depending on Hangfire directly. The Hangfire implementation lives in api/src/Shared/Wallow.Shared.Infrastructure.BackgroundJobs/HangfireJobScheduler.cs.

Recurring Jobs

Recurring jobs are registered at startup in Program.cs using IRecurringJobManager via a scoped DI call:

Job Schedule Location
SystemHeartbeatJob Every 5 minutes api/src/Wallow.Api/Jobs/SystemHeartbeatJob.cs
RetryFailedEmailsJob Every 5 minutes (feature-flagged) api/src/Modules/Notifications/Wallow.Notifications.Infrastructure/Jobs/RetryFailedEmailsJob.cs
OpenIddictTokenPruningJob Every 4 hours api/src/Modules/Identity/Wallow.Identity.Infrastructure/Jobs/OpenIddictTokenPruningJob.cs
ExpiredInvitationPruningJob Every hour api/src/Modules/Identity/Wallow.Identity.Infrastructure/Jobs/ExpiredInvitationPruningJob.cs
SessionPruningJob Daily (Cron.Daily()) api/src/Modules/Identity/Wallow.Identity.Infrastructure/Jobs/SessionPruningJob.cs
OrphanedObjectSweepJob Daily (Cron.Daily(), feature-flagged) api/src/Modules/Storage/Wallow.Storage.Infrastructure/Jobs/OrphanedObjectSweepJob.cs

The RetryFailedEmailsJob and OrphanedObjectSweepJob are conditionally registered behind their modules' feature flags (Modules.Notifications and Modules.Storage). The orphan sweep deletes storage-backend objects under the tenant- key prefix that no StoredFile row references and that are older than 24 hours — the residue of an upload whose backend write succeeded but whose database commit failed. The age threshold keeps it clear of in-flight uploads (presigned upload URLs expire in minutes).

Cron Expression Reference

Expression Description
*/5 * * * * Every 5 minutes
0 * * * * Every hour
0 */4 * * * Every 4 hours
0 2 * * * Daily at 2 AM
0 0 * * 0 Weekly on Sunday at midnight
0 0 1 * * Monthly on the 1st at midnight

Hangfire also provides helpers: Cron.Minutely, Cron.Hourly, Cron.Daily, Cron.Weekly, Cron.Monthly, Cron.Yearly, and overloads like Cron.Daily(hour: 3).

Job Patterns

Job Class Structure

Jobs use constructor injection, an ExecuteAsync method, and structured logging via [LoggerMessage] source generators. The SessionPruningJob is a representative example: it takes its DbContext, a TimeProvider, and an ILogger<T> through the primary constructor, queries for revoked or expired ActiveSession rows, removes them in a single SaveChangesAsync, and returns the number of rows pruned.

Error Handling

Use [AutomaticRetry] to configure retry behavior. The default is 10 retries with exponential backoff. Set Attempts = 0 to disable retries or Attempts = 3 for a limited count.

Per-item error handling within batch jobs prevents a single failure from aborting the entire run.

Job Parameters

Job parameters are serialized to JSON and stored in PostgreSQL. Pass simple types (IDs, strings) rather than complex objects. Retrieve full entities from the database inside the job.

Best Practices

  • Idempotency: Jobs may run multiple times due to retries or server restarts. Check state before processing.
  • Cancellation: Check CancellationToken regularly in long-running jobs.
  • Tenant context: For tenant-scoped jobs, use ITenantContextFactory.CreateScope(tenantId) to establish the tenant context before executing queries.
  • Batching: Process large datasets in batches to control memory usage.

Wolverine vs Hangfire

Scenario Use
React to domain event Wolverine
Scheduled task (cron) Hangfire
Cross-module notification Wolverine
Recurring maintenance Hangfire
Long-running workflow/saga Wolverine
Deferred execution (run in X minutes) Hangfire

The two systems complement each other. A Hangfire job can publish a Wolverine event upon completion, bridging time-based triggers with event-driven processing.