Skip to content

feat: rescan attachments stuck in "Scanning" on server start - #512

Draft
cschuerings wants to merge 2 commits into
cap-js:mainfrom
cschuerings:feat/rescan-stuck-files-on-start
Draft

feat: rescan attachments stuck in "Scanning" on server start#512
cschuerings wants to merge 2 commits into
cap-js:mainfrom
cschuerings:feat/rescan-stuck-files-on-start

Conversation

@cschuerings

Copy link
Copy Markdown
Contributor

Related to #481

Problem

If the server crashes or restarts while a malware scan is in progress, attachment rows can get stuck in "Scanning" status permanently. The scanner sets a row to Scanning as its first step in _scanAttachmentsFile, then fetches content, scans, and writes back Clean/Infected/Failed. If the process dies between those steps, no trigger ever fires again to move the row to a terminal status — downloads of such rows return 202 forever.

The only existing recovery path is the on-download expiry check in enforceScanPolicy, but that requires someone to attempt a download. Rows with no subsequent download activity stay stuck indefinitely.

This was already called out as out-of-scope in #504:

#481 (attachment stuck in Scanning after a crash between the Scanning and Clean writes) is a separate crash-recovery concern and is not addressed here.

Fix

Add a rescanStuckAttachments() function that runs once on served, detached via cds.spawn so it never blocks startup or holds a DB connection on the serving path — consistent with the pool-contention rationale from #504.

It iterates cds.model.definitions, deduplicates by underlying physical table (so service projections don't cause redundant queries), selects all rows with status = 'Scanning', and re-emits ScanAttachmentsFile for each — reusing the existing scanner pipeline end-to-end without any new scanning logic.

Both entity types are handled:

  • Composition-based attachment entities (status column, keys up__ID + ID)
  • Inline attachment entities (<prefix>_status column, <prefix>_url passed through for object-store fetch)

The existing Semaphore (maxConcurrentScans) naturally throttles any large backlog.

Configuration

Opt-in via a new flag (defaults to false):

"cds": {
  "requires": {
    "attachments": {
      "rescanOnStart": true
    }
  }
}

Also respects the existing attachments.scan flag — if scanning is disabled, the sweep is skipped entirely.

Out of scope

Per-tenant reconciliation in multitenancy deployments (marked with a TODO(mt) comment). For MT, the on-download rescan path in enforceScanPolicy remains the safety net. Happy to extend if maintainers see value.

Question for maintainers

Is this the right approach for crash recovery, or would you prefer a different mechanism (e.g. a periodic reconciliation job, or hooking into the outbox retry logic)? Also open to feedback on the opt-in default — false felt safer for a first version but true could be argued since the operation is idempotent.

If the server crashes or restarts while a malware scan is in progress,
attachment rows can get stuck in "Scanning" status permanently — the
scanner never gets a chance to write the terminal Clean/Infected/Failed
result back.

Adds a `rescanStuckAttachments()` function that runs on `served` (via
`cds.spawn` to avoid blocking startup or holding a DB connection on the
serving path). It iterates all attachment entities in the compiled model,
deduplicates by physical table (so service projections don't trigger
redundant queries), selects rows with `status = 'Scanning'`, and
re-emits `ScanAttachmentsFile` for each — reusing the existing scanner
pipeline end-to-end.

Opt-in via config flag (defaults to false):

    cds.requires.attachments.rescanOnStart = true

Also respects the existing `attachments.scan` flag.
@cschuerings
cschuerings deployed to pr-approval August 4, 2026 15:23 — with GitHub Actions Active
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant