You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Bugfix: store conflict sender before emit on local path
- Mirror the volume-side fix on the local-FS Stop branch in `conflict.rs`: store the oneshot sender in `state.conflict_resolution_tx` BEFORE emitting the `write-conflict` event, not after. A responder (the FE's `resolve_write_conflict`, or a sink that answers from within `emit_conflict`) can only answer a conflict it has observed; emitting before the slot was filled left a window where the take missed and `blocking_recv` would hang.
- The store statement is self-contained (lock guard released as it ends), so the reorder changes no lock-hold ordering across the emit or the `blocking_recv`.
- Add `stop_branch_store_before_emit_tests::stop_clash_answered_from_within_emit_resolves_without_hanging`: drives `resolve_conflict` directly (no runtime, so `blocking_recv` is legal) with a sink that answers synchronously inside `emit_conflict`. Passes instantly with the fix; deadlocks (nextest TIMEOUT) against the old emit-then-store order — verified by temporarily reverting.
- Update `write_operations/CLAUDE.md` to describe store-before-emit as load-bearing on both the local and volume paths.
Copy file name to clipboardExpand all lines: apps/desktop/src-tauri/src/file_system/write_operations/CLAUDE.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -99,7 +99,7 @@ Rates and ETA are computed in the backend (`eta.rs`) and shipped on every `Write
99
99
100
100
**Two-layer cancellation.**`AtomicU8` (`OperationIntent`) for fast in-loop checks in local file operations. Volume operations (MTP, SMB) use the same `AtomicU8` checks but run on the async executor (no `spawn_blocking`). `run_cancellable` wraps blocking local operations (for example, network-mount copies that may block indefinitely) in a separate thread, polling the flag every 100 ms via `mpsc::channel`.
101
101
102
-
**Stop-mode conflict resolution.** Emits `write-conflict` event, then blocks on a `tokio::sync::oneshot` channel (`blocking_recv()` inside `spawn_blocking`). A new oneshot channel is created per conflict. Frontend calls `resolve_write_conflict(operation_id, resolution, apply_to_all)` which takes the stored `Sender` and sends the `ConflictResolutionResponse`. `cancel_write_operation` drops the sender, causing the receiver to return `Err` (interpreted as cancellation). This is strictly better than the old Condvar+timeout approach: no polling, no 30 s safety timeout needed, immediate unblock on cancel.
102
+
**Stop-mode conflict resolution.** Creates a per-conflict `tokio::sync::oneshot` channel, **stores the sender BEFORE emitting the `write-conflict` event**, then blocks on the receiver (`blocking_recv()` inside `spawn_blocking`; the volume path `await`s instead). Store-before-emit is load-bearing: a responder can only answer a conflict it has observed, so if the event reached `resolve_write_conflict` (or a test responder sink) before the sender slot was filled, the take would miss and the recv would hang. Both the local-FS branch (`conflict.rs`) and the volume branch (`transfer/volume_conflict.rs`) order it this way. Frontend calls `resolve_write_conflict(operation_id, resolution, apply_to_all)` which takes the stored `Sender` and sends the `ConflictResolutionResponse`. `cancel_write_operation` drops the sender, causing the receiver to return `Err` (interpreted as cancellation). This is strictly better than the old Condvar+timeout approach: no polling, no 30 s safety timeout needed, immediate unblock on cancel. Pinned by `conflict.rs::stop_branch_store_before_emit_tests` (local) and the `ConflictResponderSink` suites (volume).
103
103
104
104
**Conflict-dispatch mutex (folder merges).** `WriteOperationState::conflict_dispatch_lock` (a `tokio::sync::Mutex`, next to `conflict_resolution_tx`) serializes the whole Stop-mode dispatch for an operation: there is exactly one human and one oneshot slot, so two tasks both hitting a Stop-mode clash at once — the concurrent volume-copy spawn loop, or two parallel deep directory merges — must queue rather than race to emit a `write-conflict` and clobber each other's sender. The dispatch sequence under the lock: check `is_cancelled` (bail with `Cancelled` so a queued task can't emit a prompt no one will answer after the dialog tears down — a hang), re-check the apply-to-all latch (a prior "…all" answer collapses the queued prompt), emit + await, store the latch, release. Released on every exit, NEVER held across the subsequent file write. Volume-side only today (the local-FS engine's per-file conflicts surface serially inside one `spawn_blocking`). See `transfer/CLAUDE.md` § "The conflict-dispatch mutex".
0 commit comments