Background Jobs¶
TruLoad uses Hangfire with PostgreSQL storage for all background and recurring job processing.
Dashboard¶
The Hangfire dashboard is accessible at /hangfire (admin cookie authentication required).
Recurring Jobs¶
| Job Name | Class | Schedule | Queue | Retries | Description |
|---|---|---|---|---|---|
pesaflow-invoice-sync |
PesaflowInvoiceSyncJob |
*/15 * * * * |
payments |
3 | Syncs pending invoice statuses from Pesaflow API |
exchange-rate-sync |
ExchangeRateSyncJob |
0 0 * * * |
default |
3 | Updates USD/KES exchange rates |
automated-database-backup |
BackupScheduleJob |
0 2 * * * |
default |
3 | Creates a PostgreSQL backup to local storage |
mv-refresh |
MaterializedViewRefreshJob |
*/30 * * * * |
default |
3 | Refreshes all PostgreSQL materialized views |
report-schedule-runner |
ReportScheduleJob |
*/5 * * * * |
default |
3 | Runs any scheduled report definitions that are due |
stale-weighing-alert |
StaleWeighingNotificationJob |
*/30 * * * * |
default |
3 | Emails station managers about open first-weight-only transactions past the configured threshold |
portal-daily-summary |
PortalDailySummaryJob |
0 4 * * * |
default |
2 | Emails each portal transporter their previous day's weighing summary (04:00 UTC = 07:00 EAT) |
portal-anomaly-alert |
PortalAnomalyAlertJob |
0 * * * * |
default |
1 | Detects transactions where actual net weight differs from expected by >5% and emails the transporter |
Queues¶
| Queue | Purpose |
|---|---|
critical |
Reserved for time-sensitive system jobs |
payments |
Pesaflow and payment sync jobs (isolated from other work) |
default |
All other recurring and enqueued jobs |
Workers in production: 10. Workers in non-production: 5 (configurable via HANGFIRE_WORKER_COUNT env var logic in Program.cs).
On-Demand Jobs (Enqueued)¶
| Trigger | Job | Description |
|---|---|---|
POST /portal/weighings/bulk-download (>50 tickets) |
BulkDownloadJob |
Generates a ZIP of ticket PDFs asynchronously. Poll /portal/weighings/bulk-download/{jobId}/status to check when ready. |
Job Architecture¶
All jobs follow the IServiceScopeFactory pattern to avoid scoped-service lifetime issues:
public class PortalDailySummaryJob
{
private readonly IServiceScopeFactory _scopeFactory;
public async Task ExecuteAsync()
{
await using var scope = _scopeFactory.CreateAsyncScope();
var dbContext = scope.ServiceProvider.GetRequiredService<TruLoadDbContext>();
// ...
}
}
Important: Jobs have no ITenantContext. Any service call that depends on tenant context must use IgnoreQueryFilters() for DB queries and pass tenantSlug explicitly to notification services.
ASP.NET Core Hosted Services (BackgroundService)¶
In addition to Hangfire jobs, TruLoad runs long-lived background services registered via AddHostedService. These are not visible in the Hangfire dashboard — they run as .NET IHostedService instances within the process.
SubscriptionCacheInvalidationService¶
| Property | Value |
|---|---|
| Class | Services.Background.SubscriptionCacheInvalidationService |
| Trigger | NATS subject tenant.subscription.updated (event-driven, not scheduled) |
| Redis key invalidated | sub:status:{orgId} |
| Enabled by | Nats:Enabled = true in configuration |
What it does:
Whenever subscriptions-api publishes a tenant.subscription.updated event (on plan change or status change for any tenant), this service:
- Parses the
tenant_slugfrom the event payload - Resolves
tenant_slug → Organization.SsoTenantSlug → org.Idvia a scoped DB query - Deletes the
sub:status:{orgId}Redis key
Without this, SubscriptionEnforcementMiddleware would continue serving the cached (potentially stale) status for up to 60 seconds. With it, the next request after a plan change hits subscriptions-api for a fresh status.
Configuration:
"Nats": {
"Url": "nats://localhost:4222",
"Enabled": false
}
Set Nats:Enabled = false (the default) in development to prevent startup failures when NATS is not running locally. In production, override via:
NATS__URL=nats://nats.platform.svc.cluster.local:4222
NATS__ENABLED=true
Why sub:status:{orgId} and not tenant:{slug}?
All other BengoBox Go services use tenant:{slug} as their Redis subscription cache key. TruLoad predates the uniform pattern and uses sub:status:{orgId} (org UUID). The SubscriptionCacheInvalidationService handles the slug→UUID translation transparently.
Stale Weighing Alert Flow¶
stateDiagram-v2
[*] --> CheckThreshold: Every 30 min
CheckThreshold --> FindStale: Query transactions in FirstWeightCaptured state
FindStale --> Filter: Age > PendingWeighingThresholdHours (default 8h)
Filter --> GroupByStation: Group by station
GroupByStation --> EmailManagers: Send notification per station
EmailManagers --> [*]
Invoice Sync Flow¶
stateDiagram-v2
[*] --> QueryPending: Every 15 min
QueryPending --> CallPesaflow: For each pending/grace invoice
CallPesaflow --> UpdateStatus: Map Pesaflow status to TruLoad status
UpdateStatus --> SaveChanges
SaveChanges --> [*]
Job Retention¶
Completed and failed jobs are retained for 48 hours (configured via WithJobExpirationTimeout), then auto-deleted by Hangfire's built-in cleanup.
Monitoring¶
- View current job queue depth and failures in the Hangfire dashboard at
/hangfire - Failed jobs appear under the Failed tab and can be retried manually
- Worker health is visible under the Servers tab