Skip to content

fix(coordinator): carry a cancelled navigation into the schema column fetch it started - #2063

Merged
datlechin merged 1 commit into
mainfrom
fix/2058-supersede-prepare-cancel
Aug 10, 2026
Merged

fix(coordinator): carry a cancelled navigation into the schema column fetch it started#2063
datlechin merged 1 commit into
mainfrom
fix/2058-supersede-prepare-cancel

Conversation

@datlechin

Copy link
Copy Markdown
Member

Closes #2058.

Clicking through the sidebar quickly burned 75-90ms per navigation on schema-column loading that was thrown away. From the trace in the issue, ten consecutive clicks each reached schemaColumnsEnd well after being superseded, and only the eleventh displayed anything.

Why cancelling did nothing

cancelTableLoad cancels the wrapper task, and prepareTableTabFirstLoad checks Task.isCancelled afterwards, so the result was correctly discarded. The work still ran to completion, and the reason is one line in SchemaColumnStore:

let task = Task { if let entry = await fetch() { self.entries[key] = entry } }
loads[key] = task
await task.value

That inner task is unstructured, so it inherits nothing from whoever awaits it. Cancelling the caller could not reach the fetch at all. Every Task.checkCancellation() further down, in MetadataConnectionPool.runSerially and withPinnedSessionDriver, was checking a task that was never cancelled.

This is not only wasted server work. Both of those paths are serial, so an abandoned fetch holds the metadata connection and every table clicked after it queues behind it. That is what makes ten fast clicks feel progressively slower rather than merely wasteful.

What this changes

SchemaColumnStore.load now wraps its wait in withTaskCancellationHandler and carries the caller's cancellation into the shared fetch.

The fetch is shared by every caller asking for the same key, so it cannot simply be cancelled when one of them leaves. The store counts live waiters and only cancels once the last has withdrawn. Two tabs opening the same table still share one fetch, and clicking past one of them does not take the result away from the other.

A cancelled load is also never joined. Between the last waiter leaving and its load clearing the entry there is a window where the task is cancelled but still in the map; a caller adopting it would wait on a fetch that will never produce anything. Clicking away from a table and straight back to it lands exactly there.

Two related fixes in the same area:

  • A cancellation is no longer logged as a fetch failure. loadSchemaColumns logged every catch at error level, so normal navigation would have filled the log with errors once cancellation started working.
  • rebuildSelectedTableColumnScopedQuery captured a tab index before its await and rebuilt at that index afterwards, with only a bounds check. After a tab close or reorder that index is a different tab. It now re-resolves by tab id and re-checks the table name, matching the guard prepareTableTabFirstLoad already used.
  • That guard now also compares the schema. A retarget to a same-named table in another schema used to pass it.

Tests

SchemaColumnStoreCancellationTests covers the sole waiter cancelling the fetch, a second waiter keeping it alive, a caller arriving after a cancel starting fresh, coalescing, the cached path, removeAll, and a failed fetch leaving the next caller free to retry. The store takes its fetch as a closure, so all of it runs without a database.

swiftlint lint --strict reports 0 violations in 1299 files.

Not fixed here

Cancellation is cooperative, so a fetch already blocked inside the driver still runs to completion. The win is on queued fetches, which is what the measured case is: clicks 5 through 13 in the issue's trace were all waiting behind the serial gate. See also #2061.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@datlechin
datlechin merged commit 3104bab into main Aug 10, 2026
4 checks passed
@datlechin
datlechin deleted the fix/2058-supersede-prepare-cancel branch August 10, 2026 05:15
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.

Schema column loading is not cancelled when a navigation is superseded

1 participant