fix(ingest): act on the transfer signal when it arrives, not a cycle later - #448
fix(ingest): act on the transfer signal when it arrives, not a cycle later#448KillerX wants to merge 6 commits into
Conversation
|
Added in You are right that it was missing, and the gap is worse than a lag. Once the file is complete and closed in Vidispine, it now transcodes a preview of the whole file and imports that over the same shape tag with Placement: started right after One incidental change to make it possible: Note the branches above this one in the stack (#449–#454) were rebased onto the new commit and force-pushed. Their bases and contents are unchanged; |
|
You are right to be suspicious of that line, and the re-transcode was the wrong answer — it was papering over a truncation instead of removing its cause. Reverted in There were two causes, and one of them is a regression I introduced in this PR. The missing final copy. The loop copies at the top and then waits for the signal, so the last copy always ran before the signal arrived — anything written to the source in between was never fetched. The old code did not have this problem, and not by design: it read the flag after the next copy, so breaking out already implied one more copy. Making the wait return as soon as the signal lands removed that copy without anyone noticing. The new path now does it explicitly, and the version gate leaves the old path exactly as it was. The preview being cancelled while ffmpeg is behind. Reading the activity: So the workflow now waits for the preview to reach the source duration before cancelling, which is what you asked for. The observable is the preview file itself: the activity remuxes its segments into it every minute, so a preview that has reached the source duration is one where ffmpeg has consumed everything. It gives up after ten minutes and cancels anyway — a late preview beats an ingest that never finishes. Worth knowing about the shape of the wait: it costs one |
91576d5 to
12aa02d
Compare
|
Follow-up: the full re-transcode had come back as This PR is also now two commits rather than four: the add-and-revert pair is dropped, so the history is the selector change plus the copy-and-wait fix, with nothing arguing with itself. The comment that claimed the preview is cancelled immediately went with the reverted commit, so the code and the comments agree again. I also trimmed the comments here to the same standard as #449 — the mechanism worth stating is that cancelling costs whatever |
|
Valid, and fixed in The preview wait was ungated, and that is the real bug. It emits one if workflow.GetVersion(ctx, versionPreviewCatchUp, workflow.DefaultVersion, 1) != workflow.DefaultVersion {
waitForPreviewToCatchUp(ctx, rawPath, previewPath)
}Its own ID rather than The final copy was already gated —
On "prove no executions are open during each deployment": not provable, and the deployment model argues the other way. Test: Two adjacent cases I checked while here, so they are not left to assumption:
|
8d378d3 to
3550b5b
Compare
…later The live ingest watched for the file-transferred signal from a background coroutine that set a bool, and the copy loop only read that bool after the current rsync returned and the one minute sleep elapsed. A transfer that finished thirty seconds into a sleep kept the ingest rsyncing a file nobody was writing to for another cycle, and the delay is charged to a live event. The wait now selects on the signal and the retry timer together, so it returns the moment the right signal lands — including immediately, when the signal arrived while the copy activity was running and is already queued on the channel. Signals for other files are still consumed and ignored. Cancelling the timer on the way out keeps an abandoned timer out of the history. Guarded with workflow.GetVersion, which this repository has not needed before. It is warranted here: cancelling a timer early is a command that executions started under the old code do not have in their history, so replaying them against this would fail the workflow task, and Temporal retries a failed workflow task forever. A live ingest running at deploy time would hang rather than fail. Old executions keep the coroutine and the full sleep; the branch can go once none can still be running, which is bounded by maxCopyAttempts minutes. Not changed: the loop is described as unbounded history growth, and it is not — maxCopyAttempts caps it at 1000 iterations, about 5,000 events and 16 hours. That is past the 10,000 event warning but well inside the hard limit, and ContinueAsNew across a workflow carrying signal state and a rolling sample window is a bigger change than this one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ch up Two separate reasons the ingest was losing the end of the recording. The final copy. The loop copies at the top and then waits for the signal, so the last copy always happened before the signal arrived and anything written to the source in between was never fetched. Executions from before the selector change do not have this problem: they read the flag after the next copy, so breaking out already implied one more copy. The new path now does that copy explicitly, and the version gate keeps the old one as it was. The preview. Cancelling it kills the tail feeding ffmpeg's stdin, and ffmpeg only ever sees what tail managed to write. rsync delivers a minute of video at a time, so at the moment the ingest ends the transcode is typically a segment or more behind, and everything past that point is dropped — the activity's careful shutdown, which closes stdin so ffmpeg can finalise the playlist and remux, can only finish what ffmpeg already has. So the workflow now waits for the preview to reach the source duration before cancelling. The activity remuxes its segments every minute, which is what makes progress observable; the wait gives up after ten minutes and cancels anyway, because a preview arriving late is better than an ingest that never finishes. This replaces the full re-transcode in 8e90add, reverted in the previous commit. That produced a complete preview, but by encoding the file a second time to work around a truncation rather than removing it — and it left the first cause, the missing copy, in place. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Same standard as the payload branch: keep what the code cannot say — that cancelling costs whatever tail has not written, and that the old path already copies after the signal — and drop the retelling. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The wait added activities and timers unconditionally, at a position where an older history has ListReaperFiles. Replaying an execution that is past that point produces a non-determinism error, which fails the workflow task — and Temporal retries a failed task indefinitely, so the symptom is a live ingest that hangs rather than one that reports anything. That is the failure this file already had a GetVersion gate for. The final copy was gated; the wait was not, which was an oversight rather than a judgement. Behind its own change ID rather than versionSignalSelector. Sharing would be sound only while the two changes always deploy together — true today, since they are one PR, but nothing enforces it, and the cost of it not holding is the hang above. Not left to a quiet deployment window: publish.yml tags on every merge to master and workers self-update within five minutes, while Incremental runs up to maxCopyAttempts minutes under a fixed ID during live events. A deploy landing mid-ingest is the normal case here. The test drives the whole workflow with the version mocked back to DefaultVersion and asserts AnalyzeFile is never reached, which is the property that matters; without the gate it reports two calls. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ew catch-up No incremental ingest will be running when this deploys, so neither change needs a replay-compatible branch. Removes both GetVersion gates, the background signal coroutine they preserved, and the two version constants. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two problems with the wait, both able to truncate the preview it protects or hold the ingest open far longer than it appears to: A single unchanged measurement was treated as a stalled transcode. The GrowingPreview remux is synchronous and lengthens as the recording grows, so a probe landing inside one legitimately reads the previous duration while ffmpeg is still working; cancelling there kills the tail feeding ffmpeg's stdin. Now requires previewCatchUpStaleSamples consecutive measurements with no progress. The iteration count bounded nothing. Each probe inherited the workflow's ten-attempt retry policy, so one failed measurement — the expected case before the preview exists — could spend about four minutes on backoff, making ten iterations closer to fifty minutes. The probe now runs with a single attempt and its own timeout, and the loop ends on an absolute workflow-clock deadline. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2985596 to
f3de6ee
Compare
9/n of a stack. Base:
fix/generate-short-bounded-polling(#447).The live ingest watched for the file-transferred signal from a background coroutine that set a bool, and the copy loop only read that bool after the current rsync returned and the one minute sleep elapsed. A transfer that finished thirty seconds into a sleep kept the ingest rsyncing a file nobody was writing to for another cycle — and the delay is charged to a live event.
The wait now selects on the signal and the retry timer together, so it returns the moment the right signal lands, including immediately when the signal arrived while the copy activity was running and is already queued on the channel. Signals for other files are still consumed and ignored. Cancelling the timer on the way out keeps an abandoned timer out of the history.
This introduces
workflow.GetVersion, which the repository has not used before, and I think it earns its keep here. Cancelling a timer early is a command that executions started under the old code do not have in their history, so replaying them against this fails the workflow task — and Temporal retries a failed workflow task forever. A live ingest running at deploy time would hang rather than fail. Old executions keep the coroutine and the full sleep; the branch can be deleted once none can still be running, which is bounded bymaxCopyAttemptsminutes.One correction to the finding. The loop is not unbounded:
maxCopyAttemptscaps it at 1000 iterations, roughly 5,000 events and 16 hours. That is past the 10,000-event warning but well inside the hard limit.ContinueAsNewacross a workflow carrying signal state and a rolling sample window is a bigger change than this one and is not attempted here.🤖 Generated with Claude Code