Repository navigation
Conversation
…1118) Importing history from a client that shared a peer id trimmed the "known" part by version vector and applied the tail on top of different local history: it panicked in state apply (poisoning the doc, aborting on drop) or silently scrambled text. Compare the overlap with local history before anything is mutated and return UsedOpID; also reject changes that skip counters of their peer instead of tripping a DAG assertion. Rationale: context/import-peer-id-reuse.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Member
Author
|
Superseded by #1171 (same commit, now part of a combined issue-sweep PR; GitHub closed this one when the branch was renamed). |
Contributor
WASM Size Report
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Importing history from a client that shared a peer id (#1118) used to panic in
RichtextState::apply_diffwith the doc locks held. That poisoned the doc, and dropping it aborted the process. In other cases the import returnedOkand scrambled the text. Now the import returnsErrbefore anything is mutated. The doc stays unchanged and usable, and dropping it is fine.LoroError::UsedOpID { id }(existing variant: "ID {id} has been used. You should use a new PeerID"), naming the first conflicting id.p@c-1missing) →DecodeError. It used to trip theAppDag::update_version_on_new_changeassertion under the op log lock and abort the same way.Covers
import(updates and snapshot into a non-empty doc),import_batchandimport_json_updates. From JS,doc.importnow throws instead of trapping withRuntimeError: unreachable.Rationale
An import trims the part of each change the doc already has, by version vector, and applies the rest on top. With a reused peer id, the "known" part is different history. The new
OpLog::check_and_trim_known_part_of_changes(crates/loro-internal/src/oplog/known_history.rs) runs right after decode inimport_changes_and_apply_delta_to_state_if_needed, before the preflight. So an error takes the existing early-exit path, which only rolls back the arena (#1125/#1159 conventions). No rollback scope or lock-order change is needed.NaNor binary.import_changes_to_oplogon counter ends only. Pending changes already wait for their own predecessor.Details, what counts as equal, and what cannot be caught:
context/import-peer-id-reuse.md.One existing test changed:
movable_list_invalid_ops::forged_after_map_changewrote its forged change as2@0, whilebase_with_concurrent_edit()already has a local2@0. That is a reused id, now reported before the movable-list check the test targets. The forged peer is now 4.Validation
crates/loro/tests/import_reused_peer_id.rs(9 tests): theimportwith a reused peer id panics and poisons the doc; dropping it can abort #1118 repro through updates, snapshot,import_batchand JSON; a same-length conflict that used to scramble; a conflicting known change followed by a new one; skipped counters (including deps satisfied within the same import); drop insidecatch_unwind; and re-imports that must still succeed (piecewise vs merged change stores, every op kind includingNaNand one-atom reversed deletes, overlapping export windows, shallow docs).panic in a destructor during cleanupaborts the test binary. The same-length case importsOkwith scrambled text. The skipped-counter case aborts the same way.crates/loro-wasm/tests/import_reused_peer_id.test.ts. On main's WASM build:unreachabletrap, and a silently scrambled import (2 of 3 tests fail). Passes here.cargo test --no-fail-fast -p loro -p loro-internal: 1337 passed, 0 failed (after rebasing on fix: reject imported Text ops past the end of the text #1169).pnpm checkclean.pnpm -C crates/loro-wasm test(vitest 36 files / 446 tests, tsc, deno, bun) passes.Perf
crates/examples/examples/import_known_history_perf.rson a doc with 10k changes. Best of 7, three alternating main/fix rounds on 892ed30, loadavg ≈ 9–10:Common paths are unchanged within noise. The one real cost (about +15%) is re-importing the entire known history together with new changes, because that whole overlap is now compared. That is the shape where the bug lives.
Follow-ups (not fixed here)
validate_diff, but a tail that lands on valid positions still scrambles. Detecting that would need content hashes.p@2with deps[]on top of a localp@0..=1) are still accepted silently. They do not crash.p@c-1. The pre-check does not model pending changes that the same import unlocks out of lamport order.Dropstill panics on a poisonedLoroMutexafter any other panic inside the locks (import() panics when a remote update depends on ops folded into a shallow snapshot; still on main after #1064, and the panic poisons the doc mutex #1068). This PR removes this trigger, not that failure mode.Fixes #1118
🤖 Generated with Claude Code