loop
This commit is contained in:
parent
0d9aebf900
commit
7198639db4
1 changed files with 631 additions and 247 deletions
|
|
@ -0,0 +1,80 @@
|
|||
import type { AssertableState } from "../utils/assertable-state";
|
||||
import type { TestDefinition } from "../test-definition";
|
||||
|
||||
export const localRenameSurvivesRemoteRenameTest: TestDefinition = {
|
||||
description:
|
||||
"Drain processes a RemoteChange (remote rename for doc D) while a " +
|
||||
"LocalUpdate (user rename of D) is also queued behind it. " +
|
||||
"`processRemoteUpdate` moves the disk file and, because there is a " +
|
||||
"pending LocalUpdate, takes the else branch — but its setDocument " +
|
||||
"uses the stale `record.path` (= the user-rename target) instead of " +
|
||||
"the actualPath the file just moved to. The queued LocalUpdate then " +
|
||||
"reads from `record.path`, throws FileNotFoundError, and is " +
|
||||
"silently dropped. Setup pins the queue order: a sentinel " +
|
||||
"LocalUpdate keeps drain busy on a SIGSTOPped HTTP roundtrip while " +
|
||||
"we resume client 0's WebSocket (enqueues RemoteChange) and then " +
|
||||
"user-rename D (enqueues LocalUpdate after the RemoteChange). On " +
|
||||
"server resume the drain pops the sentinel, then RemoteChange, then " +
|
||||
"LocalUpdate — exactly the order that triggers the bug.",
|
||||
clients: 2,
|
||||
steps: [
|
||||
{ type: "enable-sync", client: 0 },
|
||||
{ type: "enable-sync", client: 1 },
|
||||
{ type: "create", client: 0, path: "doc.md", content: "v1\n" },
|
||||
{ type: "create", client: 0, path: "sentinel.md", content: "s\n" },
|
||||
{ type: "barrier" },
|
||||
|
||||
// Pause client 0's WebSocket so the upcoming remote rename buffers.
|
||||
{ type: "pause-websocket", client: 0 },
|
||||
|
||||
// Server applies remote rename of doc.md -> remote.md. Broadcast
|
||||
// is buffered on client 0's WebSocket.
|
||||
{ type: "rename", client: 1, oldPath: "doc.md", newPath: "remote.md" },
|
||||
{ type: "sync", client: 1 },
|
||||
|
||||
// Pause the server BEFORE arming the sentinel, so the sentinel's
|
||||
// HTTP request will buffer at the kernel and keep drain occupied.
|
||||
{ type: "pause-server" },
|
||||
|
||||
// Sentinel: a LocalUpdate on a *different* doc that drain pops
|
||||
// first. Its HTTP roundtrip stalls on SIGSTOP, freezing drain
|
||||
// until we resume the server. While drain is frozen we can grow
|
||||
// the queue with additional events whose order we control.
|
||||
{
|
||||
type: "update",
|
||||
client: 0,
|
||||
path: "sentinel.md",
|
||||
content: "s\nedit\n"
|
||||
},
|
||||
|
||||
// Resume the WebSocket — buffered remote rename enqueues as a
|
||||
// RemoteChange. Drain is still stuck on the sentinel HTTP.
|
||||
{ type: "resume-websocket", client: 0 },
|
||||
|
||||
// User renames doc.md -> local.md on client 0. queue.enqueue
|
||||
// mutates the doc's record.path to "local.md" and pushes a
|
||||
// LocalUpdate(rename) onto the tail of the queue. Queue is now
|
||||
// [sentinel-update (in-flight), RemoteChange, LocalUpdate-rename].
|
||||
{ type: "rename", client: 0, oldPath: "doc.md", newPath: "local.md" },
|
||||
|
||||
// Resume the server. Drain pops sentinel-update (succeeds), then
|
||||
// RemoteChange. Pre-fix: processRemoteUpdate moves disk
|
||||
// local.md -> remote.md, takes the else branch, and
|
||||
// setDocument(record.path = "local.md", …) leaves record.path
|
||||
// stale. Drain pops the LocalUpdate-rename and reads from the
|
||||
// stale record.path, hits FileNotFoundError, silent skip.
|
||||
// Post-fix: when a local event is pending, we re-queue the
|
||||
// remote update without touching disk or record, so the local
|
||||
// rename drains first and both ends converge.
|
||||
{ type: "resume-server" },
|
||||
|
||||
{ type: "barrier" },
|
||||
|
||||
{
|
||||
type: "assert-consistent",
|
||||
verify: (state: AssertableState): void => {
|
||||
state.assertFileCount(2);
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
Loading…
Reference in a new issue