Skip to content

Shallow snapshot exported at frontiers with an odd number of heads (≥3) cannot be imported into an empty document #1095

Description

@DRMacIver

export(ExportMode::shallow_snapshot(&frontiers)) returns Ok, but the bytes it produces fail to import into a new LoroDoc when the frontiers have an odd number of heads greater than one. A single head round-trips, even head counts round-trip, state_only at the same frontiers round-trips, and the failing bytes import fine into a document that already has a local commit of its own:

use loro::{ExportMode, LoroDoc};

fn merged(peers: u64) -> LoroDoc {
    let doc = LoroDoc::new();
    doc.set_peer_id(1).unwrap();
    doc.get_text("text").insert(0, "a").unwrap();
    doc.commit();
    for p in 2..=peers {
        let other = LoroDoc::new();
        other.set_peer_id(p).unwrap();
        other.get_text("text").insert(0, "b").unwrap();
        other.commit();
        doc.import(&other.export(ExportMode::all_updates()).unwrap())
            .unwrap();
    }
    doc
}

fn empty_doc() -> LoroDoc {
    let doc = LoroDoc::new();
    doc.set_peer_id(100).unwrap();
    doc
}

fn doc_with_one_commit() -> LoroDoc {
    let doc = empty_doc();
    doc.get_text("other").insert(0, "z").unwrap();
    doc.commit();
    doc
}

fn import_into(doc: LoroDoc, bytes: &[u8]) -> String {
    match doc.import(bytes) {
        Ok(_) => format!("Ok({:?})", doc.get_text("text").to_string()),
        Err(e) => format!("Err({e:?})"),
    }
}

fn main() {
    for peers in 1..=7 {
        let doc = merged(peers);
        let frontiers = doc.oplog_frontiers();
        let shallow = doc
            .export(ExportMode::shallow_snapshot(&frontiers))
            .unwrap();
        let state_only = doc
            .export(ExportMode::state_only(Some(&frontiers)))
            .unwrap();
        println!(
            "{} heads (source doc {:?}): shallow into empty doc {}, shallow into doc with one commit {}, state_only into empty doc {}",
            frontiers.len(),
            doc.get_text("text").to_string(),
            import_into(empty_doc(), &shallow),
            import_into(doc_with_one_commit(), &shallow),
            import_into(empty_doc(), &state_only),
        );
    }
}

Output:

1 heads (source doc "a"): shallow into empty doc Ok("a"), shallow into doc with one commit Ok("a"), state_only into empty doc Ok("a")
2 heads (source doc "ab"): shallow into empty doc Ok("ab"), shallow into doc with one commit Ok("ab"), state_only into empty doc Ok("ab")
3 heads (source doc "abb"): shallow into empty doc Err(SwitchToVersionBeforeShallowRoot), shallow into doc with one commit Ok("abb"), state_only into empty doc Ok("abb")
4 heads (source doc "abbb"): shallow into empty doc Ok("abbb"), shallow into doc with one commit Ok("abbb"), state_only into empty doc Ok("abbb")
5 heads (source doc "abbbb"): shallow into empty doc Err(SwitchToVersionBeforeShallowRoot), shallow into doc with one commit Ok("abbbb"), state_only into empty doc Ok("abbbb")
6 heads (source doc "abbbbb"): shallow into empty doc Ok("abbbbb"), shallow into doc with one commit Ok("abbbbb"), state_only into empty doc Ok("abbbbb")
7 heads (source doc "abbbbbb"): shallow into empty doc Err(SwitchToVersionBeforeShallowRoot), shallow into doc with one commit Ok("abbbbbb"), state_only into empty doc Ok("abbbbbb")

Either the export should reject frontiers it cannot produce an importable snapshot for, or the snapshot should import. Returning Ok and failing at import time is the wrong outcome either way.

Tested on loro 1.13.9 and on main at 5c6b9f6.

BTW, this bug was found using hegel. Happy to contribute the tests if you're interested.

Activity

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