Sync metadata reads skip an earlier held transaction's writes while an optimistic transaction persists
维护者通常 1 天内回复
还没有人认领这个 Issue。
评估
- 难度
- 3/5
- 预计耗时
- 1-2 天
- 新手友好度
- 72/100
- Issue 类型
- 缺陷
- 描述清晰度
- 描述清楚
- 活跃度
- 活跃
- 技术栈
- typescript
- 领域
- database
调研方向
Start in packages/db/src/collection/sync.ts at CollectionSyncManager.createSyncMetadataApi, then inspect how pendingSyncedTransactions and syncedCollectionMetadata feed collection.get and collection.list. Reproduce with the supplied Vitest case: hold committed sync transactions behind a persisting optimistic transaction, then read metadata. Done when reads include writes from all held transactions in newest-write-first order and the reproduction passes.
由索引模型根据 Issue 内容生成。
描述
- I've validated the bug against the latest version of DB packages (
@tanstack/db0.15.2,@tanstack/react-db0.5.12)
Describe the bug
While an optimistic transaction is persisting, core holds committed sync transactions until it settles. During that window, metadata.collection.get(key) in a sync function only reads the newest pending sync transaction. If that one didn't write key, the read falls through to the applied metadata. A value an earlier held transaction wrote is skipped, and the read returns the old value (or undefined). metadata.collection.list() has the same gap.
This bites a sync source that keeps a resume cursor in collection metadata. It writes the cursor in one transaction and rows in the next, then reads the cursor back to fetch the next page. The read returns the old cursor, so the source fetches pages it already has. Once the optimistic transaction settles and the held transactions apply, the read is right again.
Cause
CollectionSyncManager.createSyncMetadataApi (packages/db/src/collection/sync.ts) reads this.state.pendingSyncedTransactions.at(-1) and then syncedCollectionMetadata. It doesn't walk the earlier entries of pendingSyncedTransactions. collection.list merges only .at(-1) too, and row.get uses the same pattern. I've reproduced collection.get and collection.list; row.get is from reading the source.
The persisted wrapper already reads its own staged writes newest first (readStagedMetadata in db-sqlite-persistence-core createWrappedSyncConfig). Once it has handed a transaction to core, though, the read lands back in core's .at(-1) lookup.
To Reproduce
Fails on 0.15.2:
import { createCollection, createTransaction } from '@tanstack/db'
import type { SyncConfig } from '@tanstack/db'
import { expect, it } from 'vitest'
type Todo = { id: string; title: string }
it(`reads collection metadata written by an earlier held sync transaction`, async () => {
let sync!: Parameters<SyncConfig<Todo, string>[`sync`]>[0]
const todos = createCollection<Todo, string>({
id: `todos`,
getKey: (todo) => todo.id,
startSync: true,
sync: {
sync: (params) => {
sync = params
params.begin()
params.commit()
params.markReady()
},
},
})
await todos.preload()
// A persisting optimistic transaction makes core hold committed sync transactions.
let release!: () => void
const gate = new Promise<void>((resolve) => {
release = resolve
})
const tx = createTransaction({ autoCommit: false, mutationFn: () => gate })
tx.mutate(() => todos.insert({ id: `1`, title: `optimistic` }))
const persisted = tx.commit()
sync.begin()
sync.metadata?.collection.set(`cursor`, 5)
sync.commit()
sync.begin()
sync.write({ type: `insert`, value: { id: `2`, title: `from server` } })
sync.commit()
expect(sync.metadata?.collection.get(`cursor`)).toBe(5) // received undefined
expect(sync.metadata?.collection.list()).toEqual([{ key: `cursor`, value: 5 }]) // received []
release()
await persisted
expect(sync.metadata?.collection.get(`cursor`)).toBe(5) // passes
})
Expected behavior
A metadata read inside a sync function sees every committed sync transaction, newest write first, before the applied metadata, whether or not core is holding them. list() merges the same way.
Workaround
We keep a map of the metadata our own held transactions wrote and read it before metadata.collection.get, dropping each entry once that transaction's commit receipt settles.
Additional context
main still has the .at(-1) reads. A fix could walk pendingSyncedTransactions from the newest entry back, stopping at the first write for the key, or at a truncate for row metadata as the current code does. list() would apply them oldest first over syncedCollectionMetadata.
- 主要语言
- TypeScript
- 星标
- 3.9k
- 派生
- 277
- 平均合并
- 17 小时 54 分钟
- 30 天内合并 PR
- 226
环境准备
- 没有 Dockerfile 或 Docker Compose 文件
- 有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
TanStack/db 的其他 Issue
-
难度 4/5 3-5 天 新手友好度 48/100
维护者通常 1 天内回复
-
难度 5/5 一周以上 新手友好度 25/100
维护者通常 1 天内回复
-
SQLite persistence: a sync transaction whose durable write fails still changes the public Collection未关闭
难度 4/5 3-5 天 新手友好度 48/100
维护者通常 1 天内回复
-
难度 5/5 一周以上 新手友好度 35/100
维护者通常 1 天内回复
-
sqlite-persistence: leader term write on every page load delays reading rows可能已有人在做 @KyleAMathews 今天认领。 未关闭
难度 4/5 3-5 天 新手友好度 50/100
维护者通常 1 天内回复
相似的 Issue
-
native
难度 2/5 1-3 小时 新手友好度 72/100
prime-radiant-inc/evener#4839 ·
维护者通常 1 天内回复
-
bug needs-triage
难度 2/5 1-3 小时 新手友好度 70/100
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 74/100
processone/fluux-messenger#1790 ·
维护者通常 1 天内回复
-
🛣️ Infrastructure
难度 2/5 1-3 小时 新手友好度 65/100
opencrvs/opencrvs-core#14232 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 72/100
kunchenguid/gh-axi#220 ·
维护者通常 1 天内回复