Skip to content

Address storyboard scenes by durable id, and cancel the orphaned job when a stage write rejects #3413

Description

@atomantic

Problem

Storyboard scenes are addressed purely by array index, end to end: the route takes sceneIndex/shotIndex, the media-job owner string is pipeline:<issueId>:storyboards:scene<idx>[:shot<idx>], storyboardsFilenameHook.js routes a completed render back via scenes[parsed.sceneIndex], and the write path (post-#3400) patches scenes[index] inside the write lock. The sanitizer stamps no id at all — issuesShared.js:395 is just scenes: raw.scenes.slice(0, 200).

#3400 closed the clobbering race (the whole pre-read array is no longer written back), but left two index-addressing consequences. Both surfaced in review of PR #3411.

1. A concurrent reorder/removal retargets the write onto the wrong scene. If a scene is deleted or reordered between the caller's read and the locked write, and index is still in range, the job id / refined description lands on whatever scene now occupies that slot instead of failing. The out-of-range case already 404s (PIPELINE_SCENE_NOT_FOUND); the still-in-range case silently mis-targets. Same for shots[tIdx]. The completion hook then routes the finished render by the same stale index, so the mis-targeting is consistent rather than self-correcting.

2. A media job is orphaned when the locked write rejects. enqueueStoryboardSceneVideo / enqueueStoryboardShotStartFrame call enqueueJob / enqueueImageJob before patchStoryboardScene. When the mutator throws (scene/shot vanished), the request 404s but the job is already queued and will run to completion with nothing referencing it — burning local GPU time and leaving an untracked artifact. comicPages.js#persistComicPageSlot and covers.js#renderComicCoverLike have the identical enqueue-then-persist shape, so this is a surface-wide pattern, not storyboard-specific.

Decision

Do both, in one change, scoped to storyboards first:

  1. Give scenes a durable id. Stamp id in the storyboards branch of sanitizeVisualStage for any scene lacking one (shots already carry id). Ship a migration in scripts/migrations/ that backfills ids on existing installs' stages.storyboards.scenes[] — index-addressed data is live on every install, so this cannot be a read-time-only fix.
  2. Resolve by id inside the write region. patchStoryboardScene takes the scene id captured during the caller's read, resolves it against the fresh array inside updateStageWithLatest, and 409s (PIPELINE_SCENE_STALE_TARGET) when it is gone — distinct from the existing 404 for a never-existed index. Keep the index as the fallback only for pre-migration records with no id.
  3. Update the completion path to matchbuildStoryboardsShotOwner and storyboardsFilenameHook.js resolve by id, falling back to index for legacy owner strings already sitting in the queue. Do not break in-flight jobs enqueued under the old owner format.
  4. Cancel the orphan on the reject path. cancelJob is exported (mediaJobQueue/index.js:965); on a rejected stage write, cancel the job just enqueued and rethrow. Apply the same treatment to persistComicPageSlot and renderComicCoverLike so the three render surfaces stay consistent.

Acceptance

Follow-up to #3400 / PR #3411.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions