Composite: don't undo writes made while a full sync is scanning
A write made during a full sync could be scanned in the replica but not in the (read-ahead) source scan, so the diff removed it, and echo tracking then consumed the server's echo. Keys written during a sync are now left out of the diff. Deterministic regression test with a read-ahead source. Also exclude -0 from the Extended JSON property test (JSON has no -0). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qxbkp4tA7G5SCWjzWuxmE
This commit is contained in:
@@ -85,6 +85,12 @@ export class KVStorageComposite<S extends Spec = AnySpec> extends BaseKVStorage<
|
||||
* changed during the scan.
|
||||
*/
|
||||
applying: Promise<void>
|
||||
/**
|
||||
* Keys written through the composite while a full sync runs. The diff may have scanned the
|
||||
* source before such a write and the replica after it; these keys are left out of the diff
|
||||
* (the write already reached the source and the replicas).
|
||||
*/
|
||||
writtenDuringSync?: Set<string>
|
||||
replaying?: Promise<void>
|
||||
outboxSeq: number
|
||||
retryTimer?: ReturnType<typeof setTimeout>
|
||||
@@ -162,7 +168,9 @@ export class KVStorageComposite<S extends Spec = AnySpec> extends BaseKVStorage<
|
||||
protected async applyWriteIfAbsent(entries: [string, unknown][]): Promise<boolean[]> {
|
||||
await this.start()
|
||||
const written = await this.source.writeIfAbsent(entries)
|
||||
await this.applyToReplicas(entries.filter((_, i) => written[i]).map(([key, value]) => ({ key, value })))
|
||||
const changes = entries.filter((_, i) => written[i]).map(([key, value]) => ({ key, value }))
|
||||
for (const { key } of changes) this.state.writtenDuringSync?.add(key)
|
||||
await this.applyToReplicas(changes)
|
||||
return written
|
||||
}
|
||||
|
||||
@@ -243,11 +251,12 @@ export class KVStorageComposite<S extends Spec = AnySpec> extends BaseKVStorage<
|
||||
private async fullSync(): Promise<void> {
|
||||
// The diff re-reads the source, so outstanding echoes no longer matter (and may never arrive).
|
||||
this.echoes.clear()
|
||||
const writtenDuringSync = (this.state.writtenDuringSync = new Set())
|
||||
try {
|
||||
const id = this.watchable ? (this.source as unknown as WatchableSource).lastEventId : undefined
|
||||
let changes: KVChange[] = []
|
||||
const flush = async () => {
|
||||
const applied = changes.filter(({ key }) => !this.pending.has(key))
|
||||
const applied = changes.filter(({ key }) => !this.pending.has(key) && !writtenDuringSync.has(key))
|
||||
await this.applyToReplicas(applied)
|
||||
if (applied.length) this.emit('change', { changes: applied, origin: 'remote' })
|
||||
changes = []
|
||||
@@ -260,6 +269,8 @@ export class KVStorageComposite<S extends Spec = AnySpec> extends BaseKVStorage<
|
||||
} catch (error) {
|
||||
if (!(error instanceof StorageUnavailableError)) throw error
|
||||
this.scheduleRetry(() => this.enqueueApply(() => this.fullSync()))
|
||||
} finally {
|
||||
this.state.writtenDuringSync = undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,6 +307,7 @@ export class KVStorageComposite<S extends Spec = AnySpec> extends BaseKVStorage<
|
||||
// ------------------------------------------------------------------ local writes → source
|
||||
|
||||
private async writeThrough(entry: OutboxEntry): Promise<void> {
|
||||
for (const key of KVStorageComposite.keysOf(entry)) this.state.writtenDuringSync?.add(key)
|
||||
if (this.queued === 0) {
|
||||
try {
|
||||
await this.send(entry)
|
||||
|
||||
Reference in New Issue
Block a user