Skip to content

Hold the background indexing engine weakly, so a batch cannot read a released engine - #118

Open
Mx-Iris wants to merge 1 commit into
mainfrom
fix/background-indexing-engine-released-mid-batch
Open

Mx-Iris wants to merge 1 commit into
mainfrom
fix/background-indexing-engine-released-mid-batch

Conversation

@Mx-Iris

@Mx-Iris Mx-Iris commented Oct 1, 2026

Copy link
Copy Markdown
Member

An engine released while one of its background indexing batches is running aborts the process: Fatal error: Attempted to read an unowned reference but object … was already destroyed, on the batch's next image load.

Cause

RuntimeBackgroundIndexingManager is owned by its engine (RuntimeEngine.backgroundIndexingManager) and has held the engine unowned since c96f229 broke their retain cycle, on the premise that the engine always outlives the manager. It does not: startBatch's driving task keeps the manager alive while the batch runs, so when the engine goes away first, the next runSingleIndex reads it.

The app's engine swap does not hit this: RuntimeBackgroundIndexingCoordinator.handleEngineSwap cancels every batch on the old manager from a task that holds the old engine, and runSingleIndex checks for cancellation before it reads the engine, serialized on the manager's actor. Nothing guards the other ways an engine can go away — the coordinator's deinit stops its event pumps but does not cancel batches.

Fix

engine is now weak (a strong reference would bring the retain cycle back):

  • expandDependencyGraph holds a strong reference for the whole walk and returns no items when the engine is gone.
  • runSingleIndex takes a strong reference after its cancellation check and records a missing engine as a cancelled item.
  • runBatch stops dispatching once the engine is gone, so the batch ends with .batchCancelled and its remaining images cancelled.

An image being loaded holds the engine for the duration of the call and finishes normally.

Verified

  • RuntimeBackgroundIndexingManagerTests.batchWhoseEngineGoesAwayEndsCancelled starts a 21-image batch on a mock engine and releases the engine right after startBatch returns. Before the fix the test process aborted with the error above; after it the batch ends with .batchCancelled.
  • Full RuntimeViewerCore suite, Xcode 26.6 against main's pins: 435 of 436 pass. The one failure, ConnectionTransportRegressionTests' DirectTCP trailing-message case, passes in each of three runs of that suite on its own; it is a load-dependent timeout of the IPC tests under the full parallel run, unrelated to this change.

Write-up: Documentations/ResolvedIssues/2026-10-01-background-indexing-engine-released-mid-batch.md, plus a decision-log row in proposal 0002, whose design text still described the unowned reference.

The same pattern in the Find navigator's corpus store, which exists only on feature/find-navigator, was fixed there the same day. This PR and #117 both add a line at the top of the resolved-issues list in Documentations/README.md, so whichever merges second has a one-line conflict to resolve.

…released engine

RuntimeBackgroundIndexingManager held its engine unowned since c96f229
broke their retain cycle, on the premise that the engine, which owns the
manager, always outlives it. It does not: a batch's driving task keeps
the manager alive, so an engine released mid-batch left the next
runSingleIndex reading it, aborting the process in
swift_abortRetainUnowned. The app's engine swap is safe only because the
coordinator cancels every batch before it lets go of the old engine;
nothing guards the other ways an engine can go away.

The engine is now weak. expandDependencyGraph holds a strong reference
for the whole walk; runSingleIndex takes one after its cancellation check
and treats a missing engine as cancellation; runBatch stops dispatching
once the engine is gone, so the batch ends with .batchCancelled and its
remaining images cancelled. An image being loaded holds the engine for
the call and finishes normally.

batchWhoseEngineGoesAwayEndsCancelled releases the engine right after
starting a 21-image batch; it aborted the test process before the fix.
Copilot AI balanced review requested due to automatic review settings October 1, 2026 08:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Engine loss can still produce .batchFinished in empty-graph and last-dispatched-item paths instead of cancelling the batch.

Review effort: Balanced
Findings: 1 Medium severity · 1 Low severity

Open (2)
What changed in this PR

Replaces an unsafe unowned indexing-engine reference with weak ownership to prevent crashes when engines are released mid-batch.

Changes:

  • Adds weak-engine cancellation handling.
  • Adds a regression test for engine release during indexing.
  • Documents the incident and design decision.
File Description
RuntimeBackgroundIndexingManagerTests.swift Adds engine-release regression coverage.
RuntimeBackgroundIndexingManager.swift Uses weak engine ownership and cancellation checks.
2026-10-01-background-indexing-engine-released-mid-batch.md Documents the defect and fix.
Documentations/​README.md Links the resolved issue.
0002-background-indexing.md Records the ownership decision.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

func expandDependencyGraph(rootPath: String, depth: Int)
async -> [RuntimeIndexingTaskItem] {
// Held for the whole walk, so the engine cannot go away halfway.
guard let engine else { return [] }
/// this manager alive through its driving task, so the engine can go away
/// first, and the batch's next load then read a released object and
/// aborted the process. A batch that finds the engine gone ends cancelled.
private weak var engine: (any RuntimeBackgroundIndexingEngineRepresenting)?

This branch has not been deployed

No deployments
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.

2 participants