Skip to content

Tree import can miss the Create event for a child of a node revived from a deleted subtree #1157

Closed

Description

@zxch3n

Importing an update that revives a tree node from a deleted subtree can leave out the Create event for one of the node's children. The document state is correct; only the events are wrong, so an event-driven mirror loses the child.

Reproduces on main (1eb14b3) and on #1152 / #1153; found by the adversarial review of those PRs (their random checkout harness, seed 138, "partial" mode).

Shape

Node 0@1008 is created under a parent that another peer has already deleted, gets a child 3@1008, and is then moved to the root. A document that has the earlier part of the history imports the rest in one import. The events contain Create for 0@1008 but none for 3@1008, which is alive under 0@1008 afterwards. Simple two- or three-peer versions of this shape do emit the child's Create; this one needs the extra concurrency in the recorded history, so the repro uses the two update blobs.

Repro (loro crate)

use base64::Engine;
use loro::{event::Diff, ContainerTrait, LoroDoc};
use std::sync::{Arc, Mutex};

const BASE: &str = "bG9ybwAAAAAAAAAAAAAAAACHcG0ABEEAAQABARABAgAAAAAAAAABAQAAAAAABQEAAAEABgEEAQAAAggBawVidXJzdAAOAQQCAQACAQACAQsCAQEAAwPRB0wAAQEBARsC6QMAAAAAAAACAAAAAAAAAAEBAQEBAQAAAAAFAQAAAQAGAQQBAAACCAFrBWJ1cnN0AA4BBAIBAAIBAAIBCwIBAQADA9oG";
const REST: &str = "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";

#[test]
fn imported_revival_emits_create_for_every_child() {
    let b64 = base64::engine::general_purpose::STANDARD;
    let doc = LoroDoc::new();
    doc.import(&b64.decode(BASE).unwrap()).unwrap();
    let tree = doc.get_tree("tree");
    let before: Vec<_> = tree.get_nodes(false).into_iter().map(|n| n.id).collect();
    let created = Arc::new(Mutex::new(Vec::new()));
    let c2 = created.clone();
    let tid = tree.id();
    let _sub = doc.subscribe_root(Arc::new(move |e| {
        for c in e.events.iter() {
            if c.target == &tid {
                if let Diff::Tree(t) = &c.diff {
                    for i in t.diff.iter() {
                        if matches!(i.action, loro::TreeExternalDiff::Create { .. }) {
                            c2.lock().unwrap().push(i.target);
                        }
                    }
                }
            }
        }
    }));
    doc.import(&b64.decode(REST).unwrap()).unwrap();
    for n in tree.get_nodes(false) {
        if !before.contains(&n.id) {
            assert!(
                created.lock().unwrap().contains(&n.id),
                "node {} (parent {:?}) got no Create event",
                n.id,
                n.parent
            );
        }
    }
}

Output: node 3@1008 (parent Node(0@1008)) got no Create event. The events emitted by the second import are:

0@1003 Create  parent Root              index 0
2@1001 Create  parent Root              index 1
1@1017 Create  parent 0@1003            index 0
2@1001 Delete  old_parent Root          old_index 1
0@1010 Create  parent Root              index 1
3@1010 Create  parent Root              index 2
0@1008 Create  parent Root              index 1
0@1012 Create  parent 3@1010            index 0
0@1010 Delete  old_parent Root          old_index 2

Notes

  • A fresh document importing both blobs gets the same events, so it does not depend on an earlier checkout.
  • TreeState::apply_diff_and_convert adds a Create for each existing child when a node is revived; the child here is created in the same batch while its parent is still in the deleted subtree, which is probably where it gets lost (the diff item for the child is applied as a move inside a deleted subtree and never emitted, and the revival of the parent happens before the child is attached, or the other way around).

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions