Fix deletions
This commit is contained in:
parent
935ed9c8e7
commit
36695e9361
4 changed files with 43 additions and 17 deletions
|
|
@ -94,6 +94,16 @@ export class SyncEventQueue {
|
||||||
// `clearAllState` / schema-version-mismatch reset.
|
// `clearAllState` / schema-version-mismatch reset.
|
||||||
private readonly _pendingServerDeletes = new Set<DocumentId>();
|
private readonly _pendingServerDeletes = new Set<DocumentId>();
|
||||||
|
|
||||||
|
// DocIds we've seen deleted in this session. `removeDocumentById`
|
||||||
|
// adds here so that any stale `RemoteChange` for that doc that
|
||||||
|
// arrives later (e.g. an older vuid buffered in the network-chaos
|
||||||
|
// jitter pipeline, or a re-enqueue that landed after the delete's
|
||||||
|
// `purgeRemoteChangesForDocumentId`) is recognised in
|
||||||
|
// `processRemoteChange` and skipped instead of falling through to
|
||||||
|
// `processRemoteCreateForNewDocument` and resurrecting the doc
|
||||||
|
// with pre-delete bytes. Cleared on `clearAllState`.
|
||||||
|
private readonly _deletedDocumentIds = new Set<DocumentId>();
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly settings: Settings,
|
private readonly settings: Settings,
|
||||||
private readonly logger: Logger,
|
private readonly logger: Logger,
|
||||||
|
|
@ -605,6 +615,15 @@ export class SyncEventQueue {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async removeDocumentById(documentId: DocumentId): Promise<void> {
|
public async removeDocumentById(documentId: DocumentId): Promise<void> {
|
||||||
|
// Record the tombstone unconditionally: `processRemoteChange`
|
||||||
|
// checks it to drop late RemoteChanges that would otherwise
|
||||||
|
// resurrect the doc via `processRemoteCreateForNewDocument`.
|
||||||
|
// Purging the queue (below) only catches events that are
|
||||||
|
// already enqueued; events that arrive after this point (e.g.
|
||||||
|
// a stale broadcast buffered in the network-chaos jitter
|
||||||
|
// pipeline, or a re-enqueue that lands after this purge) need
|
||||||
|
// the tombstone to be skipped.
|
||||||
|
this._deletedDocumentIds.add(documentId);
|
||||||
const record = this.byDocId.get(documentId);
|
const record = this.byDocId.get(documentId);
|
||||||
if (record === undefined) {
|
if (record === undefined) {
|
||||||
// Still clear any deletion-pending mark and purge stale
|
// Still clear any deletion-pending mark and purge stale
|
||||||
|
|
@ -634,6 +653,10 @@ export class SyncEventQueue {
|
||||||
return this.save();
|
return this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public hasBeenDeleted(documentId: DocumentId): boolean {
|
||||||
|
return this._deletedDocumentIds.has(documentId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark a doc as "HTTP DELETE has been acked by the server but the
|
* Mark a doc as "HTTP DELETE has been acked by the server but the
|
||||||
* WebSocket receipt that would call `removeDocumentById` hasn't arrived
|
* WebSocket receipt that would call `removeDocumentById` hasn't arrived
|
||||||
|
|
@ -739,6 +762,7 @@ export class SyncEventQueue {
|
||||||
this.byDocId.clear();
|
this.byDocId.clear();
|
||||||
this._byLocalPath.clear();
|
this._byLocalPath.clear();
|
||||||
this._pendingServerDeletes.clear();
|
this._pendingServerDeletes.clear();
|
||||||
|
this._deletedDocumentIds.clear();
|
||||||
this._lastSeenUpdateId.reset();
|
this._lastSeenUpdateId.reset();
|
||||||
await this.save();
|
await this.save();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -876,6 +876,23 @@ export class Syncer {
|
||||||
return this.processRemoteUpdate(trackedRecord, remoteVersion);
|
return this.processRemoteUpdate(trackedRecord, remoteVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tombstoned: we removed this doc in this session via
|
||||||
|
// `removeDocumentById` (either WS delete receipt or PUT response
|
||||||
|
// with `isDeleted=true`). A late RemoteChange for the same doc
|
||||||
|
// can still reach us — buffered in the network-chaos jitter
|
||||||
|
// pipeline, or re-enqueued after the delete's purge — and
|
||||||
|
// without this gate `processRemoteCreateForNewDocument` would
|
||||||
|
// happily fetch pre-delete bytes and resurrect the doc, blocking
|
||||||
|
// any other doc whose `remoteRelativePath` happens to be the
|
||||||
|
// same slot.
|
||||||
|
if (this.queue.hasBeenDeleted(remoteVersion.documentId)) {
|
||||||
|
this.queue.lastSeenUpdateId = remoteVersion.vaultUpdateId;
|
||||||
|
this.logger.debug(
|
||||||
|
`Discarding stale remote update for tombstoned ${remoteVersion.documentId} at ${remoteVersion.relativePath}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return this.processRemoteCreateForNewDocument(remoteVersion);
|
return this.processRemoteCreateForNewDocument(remoteVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { randomCasing } from "./utils/random-casing";
|
||||||
import { TimeoutError } from "./utils/with-timeout";
|
import { TimeoutError } from "./utils/with-timeout";
|
||||||
import { TestErrorTracker } from "./utils/test-error-tracker";
|
import { TestErrorTracker } from "./utils/test-error-tracker";
|
||||||
|
|
||||||
const TEST_ITERATIONS = 5;
|
const TEST_ITERATIONS = 50;
|
||||||
const MAX_INITIAL_DOCS = 10;
|
const MAX_INITIAL_DOCS = 10;
|
||||||
|
|
||||||
// Simulate async file access by injecting waiting time before returning from file operations.
|
// Simulate async file access by injecting waiting time before returning from file operations.
|
||||||
|
|
|
||||||
|
|
@ -91,25 +91,10 @@ print_failed_log() {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
E2E_TIMEOUT=${2:-3600}
|
echo "Monitoring $process_count processes"
|
||||||
start_time=$(date +%s)
|
|
||||||
echo "Monitoring $process_count processes (timeout: ${E2E_TIMEOUT}s)"
|
|
||||||
|
|
||||||
# Monitor processes
|
# Monitor processes
|
||||||
while true; do
|
while true; do
|
||||||
# Script-level timeout to prevent indefinite hangs
|
|
||||||
current_time=$(date +%s)
|
|
||||||
elapsed=$((current_time - start_time))
|
|
||||||
if [ $elapsed -ge $E2E_TIMEOUT ]; then
|
|
||||||
echo "E2E timeout reached (${E2E_TIMEOUT}s). Killing remaining processes."
|
|
||||||
for pid in "${pids[@]}"; do
|
|
||||||
if [ -n "$pid" ]; then
|
|
||||||
kill $pid 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if print_failed_log; then
|
if print_failed_log; then
|
||||||
# Kill remaining processes
|
# Kill remaining processes
|
||||||
for pid in "${pids[@]}"; do
|
for pid in "${pids[@]}"; do
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue