Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/preserve-reentrant-ordering-repair.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/db': patch
---

Preserve ordering invalidations raised while an ordered repair retires its previous acquisitions, including an adapter delete of a row in the repaired prefix. Revalidate the affected source prefix before publishing the repaired window.
4 changes: 2 additions & 2 deletions docs/contributing/oracle-coverage.md

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion packages/db/src/query/live/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1126,7 +1126,11 @@ new repair acquisitions remain. Every acquisition in this chain uses
reusing completed or in-flight work. A source-order invalidation generation
fences the chain. If another qualifying mutation arrives before it finishes,
core starts a replacement repair before releasing the publication barrier and
before the obsolete chain can issue another tie or refill. A repeated prefix or
before the obsolete chain can issue another tie or refill. A qualifying change
that a retirement release delivers after the chain succeeds belongs to the
same fence, including a delete of a row in its prefix, such as the boundary row or a tie the prefix holds: the repair keeps that newer debt, the next ordered pass starts its
replacement before the barrier releases, and a window move waiting for the
chain stays pending until the replacement settles. A repeated prefix or
page fixed point ends the repair when no more data is needed; it cannot leave a
held repair without pending work.
The finite prefix and tie acquisitions they replace retire after the whole
Expand Down
51 changes: 38 additions & 13 deletions packages/db/src/query/live/ordered-source-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ export class OrderedSourceLoader {
// completion sets it again, so it never needs clearing.
private hasSettledSourceRequest = false
private settledSourceBoundary: Record<string, unknown> | undefined
// The settled prefix ending at that boundary: a row that ties with the
// boundary but is not among these rows lies outside the prefix.
private settledSourcePrefix: ReadonlyArray<
ChangeMessage<Record<string, unknown>, string | number>
> = []
// Independent of ordinary finite success. A live delete or order-changing
// update owes a bounded repair: unlike a failed request it cannot have
// partially written unknown rows, so re-reading the exact ordered prefix is
Expand Down Expand Up @@ -229,10 +234,21 @@ export class OrderedSourceLoader {
let hasOptimisticDeparture = false
for (const change of changes) {
const previous = sentRows?.get(change.key)
// A finished repair holds every row through its boundary, so releasing
// what it superseded removes only rows outside its coverage.
// A finished repair holds every row of its prefix, so releasing what it
// superseded removes only rows after it, such as a tie only an old tie
// request held. Deleting a sent row of the prefix is new source
// information.
if (change.type === `delete` && this.retiringSupersededAcquisitions) {
continue
const order =
previous === undefined || this.settledSourceBoundary === undefined
? 1
: this.info.comparator(previous, this.settledSourceBoundary)
if (
order > 0 ||
(order === 0 &&
this.settledSourcePrefix.every(({ key }) => key !== change.key))
)
continue
}
// A sent row that leaves or moves. An optimistic overlay may still
// revert, so it only checks the local supply; a source move invalidates
Expand Down Expand Up @@ -765,7 +781,14 @@ export class OrderedSourceLoader {
if (!this.active || !hasSupersededAcquisition) {
this.cancelRepairRetry()
this.repairRetries = 0
this.moveWork({ kind: `idle` }, [`repair`], `none`)
// Retirement can deliver a newer invalidation or raise the debt to a
// full-source read. Keep that debt for the next pass; otherwise this
// chain has paid its own epoch.
this.moveWork(
{ kind: `idle` },
[`repair`],
this.repairNeedsReplacement() ? this.debt : `none`,
)
}
}
}
Expand Down Expand Up @@ -1220,15 +1243,17 @@ export class OrderedSourceLoader {
}
if (!isFullSource && options.orderBy) {
try {
this.settledSourceBoundary =
this.subscription
.readOrderedSnapshot({
...options,
// Provider limits include optimistic headroom. Those
// extra local ranks do not prove a source boundary.
limit: requestedCount ?? options.limit,
})
.at(-1)?.value ?? this.settledSourceBoundary
const prefix = this.subscription.readOrderedSnapshot({
...options,
// Provider limits include optimistic headroom. Those
// extra local ranks do not prove a source boundary.
limit: requestedCount ?? options.limit,
})
const boundary = prefix.at(-1)?.value
if (boundary) {
this.settledSourceBoundary = boundary
this.settledSourcePrefix = prefix
}
} catch (error) {
if (canSettleSynchronously) {
synchronousCompletionFailure = { error }
Expand Down
3 changes: 3 additions & 0 deletions packages/db/tests/oracle-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ const staticOracleProperties = [
`query-db.refetch-held-page-end`,
`ordered-loader.request-authority`,
`ordered-loader.unproven-report`,
`ordered-loader.replay-continuation`,
`ordered-loader.retirement-invalidation`,
`ordered-loader.retirement-receiver`,
`replay.ordered-window-faults`,
`replay.ordered-failed-replay`,
`replay.ordered-preload-recovery`,
Expand Down
2 changes: 1 addition & 1 deletion packages/db/tests/oracle-replay-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ const ownerGroups: ReadonlyArray<
[
`db/tests/query/ordered-source-loader-state-oracle.test.ts`,
`ordered-loader`,
`request-authority unproven-report`,
`request-authority unproven-report replay-continuation retirement-invalidation retirement-receiver`,
],
[
`db-sqlite-persistence-core/tests/sqlite-core-adapter-cli-runtime.test.ts`,
Expand Down
Loading
Loading