Skip to content

fix(imports): retry a failed file import instead of blocking on the first attempt - #933

Open
m4bard wants to merge 1 commit into
Listenarrs:canaryfrom
m4bard:fix/bug31-stranded-import-retry
Open

fix(imports): retry a failed file import instead of blocking on the first attempt#933
m4bard wants to merge 1 commit into
Listenarrs:canaryfrom
m4bard:fix/bug31-stranded-import-retry

Conversation

@m4bard

@m4bard m4bard commented Sep 2, 2026

Copy link
Copy Markdown

A file import that fails once ends the download permanently, even though the job has a retry budget
it never spends.

What happens

In DownloadProcessingJobProcessor.ProcessJobAsync, if ImportDownloadFilesAsync returns any
unsuccessful ImportResult, the job goes straight to FailImportAsync. RetryCount is still 0 at
that point, and ProcessQueueAsync then blocks the download. A source file that is briefly
unavailable, say one the download client still has open, is therefore treated the same as a file
that will never import.

Most failure points in that method already go through ScheduleRetryAsync: the source path missing,
the client fetch, the files on disk not matching what was expected, marking the item imported,
enqueuing the scan, and committing finalization. Three do not. I have changed one of them.

  • the catch (InvalidOperationException) around the import call, which looks more like a state or
    programming error than something a retry would clear
  • the per-file ImportResult failures, which is this change
  • No audio files were registered after file import, in the case where the audiobook has no
    existing files either

I left the other two as they are. I am not confident their failures are transient, and I would
rather send you one change I can argue for than three I cannot. If you want either of them routed
the same way, that is easy to add.

The change

Send the per-file failure case through ScheduleRetryAsync while attempts remain, then call
FailImportAsync once they are spent.

Terminal behaviour does not change. With the budget exhausted the download still ends up
ImportBlocked, and the last call is still FailImportAsync rather than letting ScheduleRetryAsync
run itself out, because FailImportAsync is the only one that records failedResults on the history
entry. That per-file detail seems worth keeping for the attempt that gives up.

Tests

One new test, Import_FileImportFailure_RetriesBeforeBlockingTheDownload. It fails on canary and
passes here.

One existing test needed adapting, though not in what it asserts.
Import_FailedPublication_PersistsFailureContractInHistory checks the FailedResults contract on the
history entry, and it used to reach that assertion through the first import failure, which is no
longer terminal. It now seeds RetryCount = MaxRetries so it exercises the terminal attempt, which is
the one that writes the contract it is testing. The assertions themselves are untouched.

Where this sits

This touches DownloadProcessingJobProcessor.cs, which you have changed three times recently, most
recently in 76be79f. If it collides with something you have in progress, say so and I will drop it
or rework it around whatever lands. I have no attachment to this shape.


Worked through with Claude Code at my direction. The test claims above were checked by running
them, including the failure on canary with canary's own production file. The description of the
other two failure paths is source reading rather than something I exercised. I reviewed this
before posting.

…irst attempt

A download whose file import fails is marked Failed immediately, with
RetryCount still 0, and the download goes to ImportBlocked. So a source file
that is briefly unavailable, such as one the download client still holds
open, ends the download permanently.

Most failure points in ProcessJobAsync take the bounded retry path instead:
source path missing, client fetch, files on disk not matching, marking the
item imported, enqueuing the scan, and committing finalization all call
ScheduleRetryAsync. Three do not, and this changes one of them, the per-file
ImportResult failures. The catch of InvalidOperationException around the
import call and the "No audio files were registered after file import" case
are left alone, because it is less clear their failures are transient.

Route the per-file case through ScheduleRetryAsync while attempts remain.
Terminal behaviour is unchanged: once they are spent the download is blocked
exactly as before, and the terminal call is still FailImportAsync rather
than letting ScheduleRetryAsync exhaust itself, because FailImportAsync is
the only one that records failedResults on the history entry.

Import_FailedPublication_PersistsFailureContractInHistory reached its
assertion through the first import failure, which is no longer terminal. It
now seeds RetryCount = MaxRetries so it exercises the terminal attempt, the
one that writes the FailedResults contract it checks. Its assertions are
unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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