Skip to content

Reclaim FFmpeg sessions after late worker exit - #140

Draft
Blackspirits wants to merge 1 commit into
audit/ffmpeg-stale-session-publication-af88from
audit/ffmpeg-session-self-reclaim-0882
Draft

Blackspirits wants to merge 1 commit into
audit/ffmpeg-stale-session-publication-af88from
audit/ffmpeg-session-self-reclaim-0882

Conversation

@Blackspirits

@Blackspirits Blackspirits commented Sep 15, 2026

Copy link
Copy Markdown
Owner

Purpose

Dependent teardown-lifetime follow-up to #139 ("Fence stale FFmpeg session owner publication").

#115 correctly fails closed when a Session worker does not stop within the teardown timeout, retaining every worker-visible resource instead of risking use-after-free. #139 fences any worker that survives that timeout from publishing owner state into a newer load. One lifecycle gap remained: once CloseFile removes the Session from the owner, no later caller necessarily retains a path to retry Dispose, so a worker that exits shortly after the timeout can leave the retained native/session resources leaked for the rest of the process.

Current upstream SubtitleEdit/subtitleedit main was rechecked at d6917286e355976da953d0ee22208b4bf9461699; its latest change is unrelated and no open upstream PR was found for this teardown-reclamation boundary.

Finding fixed

#115 made Session.Dispose retry-safe internally, but FfmpegPlayer.CloseFile() exchanges _session to null before calling it.

If a worker misses the join timeout:

  1. Session resources are correctly retained;
  2. Dispose returns;
  3. the owner no longer references that Session;
  4. if the worker exits moments later, there may be no future Dispose call that can reclaim the retained AVFormatContext, audio sink, events and self GCHandle.

That turns a temporary teardown timeout into a permanent controlled leak.

Fix

Session now owns worker-exit accounting and one-shot deferred reclamation:

  • all four worker kinds start through StartWorker();
  • the active-worker count is incremented before Thread.Start and rolled back if start itself fails;
  • every worker decrements the count in an outer finally, after its loop/local codec cleanup has completed;
  • a teardown timeout marks cleanup as deferred but still retains the complete worker-visible boundary;
  • when the last active worker exits, it only schedules a one-shot cleanup task — it does not take the lifecycle lock from inside the worker being joined;
  • the task serializes through the existing lifecycle lock and releases resources only when the Session is still closing, not already disposed, and has zero active workers;
  • the timeout path also checks immediately after marking cleanup deferred, closing the race where the last worker exits between Join timeout and that mark;
  • a later explicit Dispose remains free to retry joins and win the cleanup race first;
  • permanently stuck workers remain a controlled leak, preserving Harden FFmpeg session startup and teardown lifetime #115's fail-closed safety.

No global retired-session collection, polling loop or permanently blocked reaper thread is introduced.

Regression coverage

Pure tests pin the deferred-cleanup gate:

  • no cleanup before a teardown has actually been deferred;
  • no cleanup while one or more workers remain;
  • cleanup becomes eligible exactly when deferred state is set and active-worker count reaches zero.

Existing #139 stale-publication tests remain unchanged and continue to fence the old Session until it exits.

Scope / branch state

  • dependency/base PR: Fence stale FFmpeg session owner publication #139
  • base HEAD: 088211bde3024739667ef55448b483064c14cb90
  • HEAD: 4edde2ba42d0e81ce3d1504818eb13a3e7af606f
  • 1 commit
  • 2 files
  • +117 / -28
  • current upstream main checked: d6917286e355976da953d0ee22208b4bf9461699
  • audit draft only; no merge/promotion intended

AI assistance: ChatGPT was used for adversarial timeout/reclamation review, worker-lifetime accounting, lock-order analysis, exact-once cleanup modelling and focused regression design.

Final CI

Authoritative run: SubtitleEdit#279 on 4edde2ba42d0e81ce3d1504818eb13a3e7af606f

  • SeConv: 488 passed / 2 skipped / 0 failed
  • LibUiLogic: 905 passed / 0 skipped / 0 failed
  • LibSE: 2017 passed / 0 skipped / 0 failed
  • UI: 5240 passed / 9 skipped / 0 failed
  • build: 0 errors / 7 pre-existing warnings
  • retry step skipped
  • failure-artifact upload skipped

Final adversarial review was performed on this exact HEAD after CI; no blocker found.

@Blackspirits Blackspirits left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Final adversarial review on 4edde2ba42d0e81ce3d1504818eb13a3e7af606f after CI SubtitleEdit#279: no blocker found. Verified worker accounting is rolled back on Thread.Start failure, deferred cleanup is one-shot, the last worker never blocks on _lifecycleLock, the timeout/last-exit race is closed by the immediate readiness check, and a later explicit Dispose can still win cleanup safely. Permanently stuck workers remain retained rather than freed unsafely. No merge/promotion performed.

@Blackspirits Blackspirits left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Final adversarial review on 4edde2ba42d0e81ce3d1504818eb13a3e7af606f after CI SubtitleEdit#279: no blocker found. Verified the worker counter is balanced across successful/failed Thread.Start, workers never take the lifecycle lock while being joined, deferred cleanup is one-shot and rechecks zero active workers under the lifecycle lock, a later explicit Dispose may win safely, and permanently stuck workers remain fail-closed retained resources. No merge/promotion performed.

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