Skip to content

A batch job can stay RUNNING for ever with no log line, because the future's throwable is discarded #153

Description

@aborroy

Type: bug. Effort: S. Affects all three batch ingesters.

Summary

A batch sync job can be left RUNNING for ever, with no log line of any kind, if the task throws something that is not an Exception. Two things combine to make it silent:

  • runJob catches Exception, which does not catch Error or any other Throwable.
  • The task is launched with CompletableFuture.runAsync(...) and the returned future is discarded, so a throwable that escapes the catch is captured into that future and never observed, never logged, never surfaced.

The job record therefore keeps status: RUNNING and completedAt: null indefinitely. A caller polling /api/sync/status/{id} waits for ever; a test harness times out and blames the source system.

Observed

A real SharePoint run against Microsoft Graph, 2026-09-21:

GET /api/sync/status/652f0d4b-...
{ "status": "RUNNING", "completedAt": null, "discoveredCount": 52, "syncedCount": 51, "failedCount": 0 }

Nine minutes after the last log line, and it never changed afterwards. Diagnosis:

  • Container CPU 0.15%, so nothing was working.
  • 50 Chunking produced lines and 50 Completed sync for node lines, so the per-document work had finished.
  • docker kill -s QUIT thread dump: the single connector-batch-1 thread is parked in LinkedBlockingQueue.take inside ThreadPoolExecutor.getTask, i.e. back in the pool waiting for new work. No thread anywhere in ConnectorBatchIngestionService, ConnectorDiscoveryService or NodeSyncService.
  • No WARN or ERROR from our code, no OutOfMemoryError, no java.lang.Error, no stack trace.

So the task ended and neither job.complete() nor job.fail() ran. With the catch (Exception) in place, a Throwable that is not an Exception is the only way to reach that state, and the discarded future is why nothing was printed.

Note the absence of an Error in the log is consistent with this rather than evidence against it: a throwable captured by a CompletableFuture nobody inspects is never printed at all.

Where

Same shape in all three:

  • plugin-runtime/plugin-batch-ingester ConnectorBatchIngestionService:85
  • alfresco/alfresco-batch-ingester BatchIngestionService:84 and :97
  • nuxeo/nuxeo-batch-ingester NuxeoBatchIngestionService:67 and :74

Fix

Two independent changes, and both are worth making because either alone leaves a gap:

  1. Observe the future. CompletableFuture.runAsync(...).whenComplete((ignored, thrown) -> { if (thrown != null) { job.fail(); log.error("...", thrown); } }). This catches everything, including what the inner catch misses, and it is the only place that can.
  2. Catch Throwable in runJob, not Exception, so the job is marked failed at the point of failure with the source context still in scope. Rethrow after marking if that is preferred for an Error.

A job left RUNNING is worse than a job marked FAILED, because the second is actionable and the first is indistinguishable from slow.

Acceptance criteria

  • A task that throws an Error leaves the job FAILED, not RUNNING, and logs the throwable with the job id.
  • The same holds for a Throwable that is neither Exception nor Error.
  • A test per ingester, injecting a throwing collaborator, asserting the terminal status and that something was logged.
  • Consider whether a job with no progress for N minutes should be reported distinctly, since "stalled" and "working" are currently the same state to a caller. Out of scope for the fix, worth a decision.

Not in scope

What the original throwable was on that run is unknown and unknowable from here, which is the point of the issue. Once the future is observed, the next occurrence names itself.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions