Kind: Service
Source: atloria-monorepo/apps/api/src/documentation/services/job-reconcile.service.ts
Job Reconciliation — the honesty guarantee for documentation generation jobs.
A DocumentationJob goes PENDING → RUNNING and the external atlorix-runner POSTs progress back to /jobs/
/external-progress, bumpingupdatedAt on every report.
If that subprocess dies WITHOUT calling /jobs//external-failed, the row is
orphaned at RUNNING with whatever progress it last reported — the infamous
"Generation in progress (5%)" that never moves and never fails.
Before this service the ONLY reaper was inline in DocAutomationService.startJob(): it auto-expired stale RUNNING jobs, but only when a NEW job was started for the SAME project. So if nobody kicked off another run, an orphan could sit at 5% forever (verified live: a week-old 5% job on a real project's Overview).
This closes the gap two ways, so status can never lie:
-
reconcile(scope) — the shared core. Any PENDING/RUNNING job whose
updatedAt(its progress heartbeat) is older than the stall threshold is transitioned to FAILED with a clear, honest reason. Healthy jobs (recent heartbeat) and jobs already terminal (COMPLETED/FAILED/CANCELLED) are never touched. Called defensively at READ time by listJobs()/getJob() so a stale row can never even be served as in-progress — best-effort, it never throws into the read path. -
sweep() — an
JobReconcileService prevents documentation jobs from remaining falsely marked as PENDING or RUNNING after their external runner has stopped reporting progress. It detects jobs whose updatedAt heartbeat exceeds the configured stall threshold, transitions them to FAILED with an honest failure reason, and supports both defensive read-time reconciliation and periodic background sweeps.
Methods
| Method | Signature | Returns | Description |
|---|---|---|---|
stallMinutes | stallMinutes() | number | Stall threshold in minutes, tunable via DOC_JOB_STALL_MINUTES (floored at 1). |
reconcile | reconcile(scope: { organizationId?: string; projectId?: string; id?: string }) | Promise<number> | Reconcile stalled jobs → FAILED. |
sweep | sweep() | Promise<void> | Periodic global sweep — one replica per tick, so nothing sits stale unseen. |
onModuleDestroy | onModuleDestroy() | void |
Dependencies
PrismaServiceConfigService
Where it refuses work
JobReconcileServicestops the work with an early return when!Number.isFinite(raw) || raw <= 0.JobReconcileServicestops the work with an early return whenprocess.env['DOC_JOB_RECONCILE_DISABLED'] === 'true'.JobReconcileServicestops the work with an early return when!(await this.acquireLock()).
When something fails
JobReconcileServicehandles failure in 2 places: it turns it into a return value in all 2.
Diagram
mermaidsequenceDiagram participant Client participant JobsAPI as Jobs API participant Reconciler as JobReconcileService participant DB as Database participant Runner as atlorix-runner Runner->>JobsAPI: POST /jobs/:id/external-progress JobsAPI->>DB: Update progress and updatedAt Client->>JobsAPI: GET /jobs or /jobs/:id JobsAPI->>Reconciler: reconcile() Reconciler->>DB: Find stale PENDING/RUNNING jobs Reconciler->>DB: Mark stale jobs as FAILED JobsAPI->>DB: Read current job status JobsAPI-->>Client: Return reconciled status loop Periodic sweep Reconciler->>Reconciler: sweep() Reconciler->>DB: Reconcile stale jobs end
Usage
tsimport { Injectable } from '@nestjs/common';
import { JobReconcileService } from './job-reconcile.service';
@Injectable()
export class DocumentationJobsService {
constructor(
private readonly jobReconcileService: JobReconcileService,
) {}
async listJobs() {
// Best-effort reconciliation before serving job statuses.
await this.jobReconcileService.reconcile();
// Fetch and return jobs after stale in-progress records are failed.
return this.fetchJobsFromDatabase();
}
async runMaintenanceSweep(): Promise<void> {
// Intended for scheduled/background maintenance.
await this.jobReconcileService.sweep();
}
private async fetchJobsFromDatabase() {
return [];
}
}
AI Coding Instructions
- Treat
updatedAtas the job heartbeat: external progress reports must update it so healthy long-running jobs are not incorrectly failed. - Only reconcile non-terminal jobs (
PENDINGandRUNNING); never modifyCOMPLETED,FAILED, orCANCELLEDjobs. - Keep reconciliation best-effort when called from read paths—failures in
reconcile()must not preventlistJobs()orgetJob()from returning data. - Use
sweep()for scheduled/background cleanup and ensure timers or scheduler resources are stopped throughonModuleDestroy(). - When adding job states or runner callbacks, update reconciliation rules so stale jobs cannot be served indefinitely as in progress.
Relationships
- DEPENDS_ON →
PrismaService - DEPENDS_ON →
configservice
Referenced By
DocumentationModule(MODULE_PROVIDES)DocAutomationService(DEPENDS_ON)
Was this page helpful?