barrel_timeline (barrel_docdb v1.1.1)

View Source

Timeline: branch a database, work on the branch, merge back.

A branch is an instant fork: both RocksDB stores (docs and attachments) are checkpointed into the branch's directory (hard links, O(1) in data size, copy-on-write at the file level), and the branch opens as a normal database whose storage keys keep the parent's name (keyspace indirection, see barrel_keyspace). The fork instant is minted inside the parent's writer, so the checkpoint is exactly "everything up to fork_hlc".

The two stores are checkpointed back to back, not atomically: attachment writes do not serialize through the writer, so an in-flight attachment may straddle the fork. Attachments are LWW state with origin guards and sync out of band, the same tolerance replication has.

v1 lineage is linear: branching a branch is rejected, which keeps barrel_keyspace:resolve/1 single-level.

Summary

Functions

Fork a database. Opts: - at => now (default): the branch starts at the fork instant. - data_dir => Dir: base directory for the branch (defaults to the parent's base, i.e. the branch lives next to its parent). Every other option is merged over the inherited parent config for the branch's FIRST open (channels are always inherited: the copied feed rows are keyspace-scoped and a mismatched config would orphan them). Reopening the branch later follows the normal create_db contract: config is runtime-only and must be supplied again.

The OPEN branches of a database, from the registry (a parent tracks nothing; on-disk-but-closed branches are not listed).

Ship the branch's edits to its parent: one-shot replication over the local transports starting at the fork instant (exclusive), then the attachment phase from the same boundary. Conflicts resolve on the parent through its configured machinery (deterministic LWW, conflict_merger when set); redelivery is a no-op, and a merge checkpoint (a local doc on the branch) makes repeated merges incremental. A PITR rewind is NOT merged: restored versions sit at or before the fork and the parent's vectors already contain them.

Types

att_info/0

-type att_info() ::
          #{name := binary(),
            content_type := binary(),
            length := non_neg_integer(),
            digest := binary(),
            chunked => boolean(),
            chunk_size => pos_integer(),
            chunk_count => pos_integer()}.

change/0

-type change() :: map().

db_config/0

-type db_config() :: #{path => string(), store => module(), atom() => term()}.

db_name/0

-type db_name() :: binary().

db_ref/0

-type db_ref() :: pid() | atom().

doc/0

-type doc() :: #{binary() => term()}.

doc_info/0

-type doc_info() :: #{id := docid(), rev := revid(), deleted := boolean(), revtree := revtree()}.

docid/0

-type docid() :: binary().

endpoint/0

-type endpoint() :: db_name() | {node(), db_name()} | {module(), term()}.

rep_options/0

-type rep_options() ::
          #{continuous => boolean(),
            since => seq_string(),
            filter => fun((doc()) -> boolean()),
            atom() => term()}.

rev_info/0

-type rev_info() ::
          #{id := revid(),
            parent := revid() | undefined,
            deleted := boolean(),
            attachments => #{binary() => att_info()}}.

revid/0

-type revid() :: binary().

revtree/0

-type revtree() :: #{revid() => rev_info()}.

seq/0

-type seq() :: barrel_hlc:timestamp().

seq_string/0

-type seq_string() :: binary().

view_name/0

-type view_name() :: binary().

view_result/0

-type view_result() :: #{key := term(), value := term(), id := docid()}.

Functions

branch_db(Parent, BranchName, Opts)

-spec branch_db(binary(), binary(), map()) -> {ok, pid()} | {error, term()}.

Fork a database. Opts: - at => now (default): the branch starts at the fork instant. - data_dir => Dir: base directory for the branch (defaults to the parent's base, i.e. the branch lives next to its parent). Every other option is merged over the inherited parent config for the branch's FIRST open (channels are always inherited: the copied feed rows are keyspace-scoped and a mismatched config would orphan them). Reopening the branch later follows the normal create_db contract: config is runtime-only and must be supplied again.

list_branches(Parent)

-spec list_branches(binary()) -> [binary()].

The OPEN branches of a database, from the registry (a parent tracks nothing; on-disk-but-closed branches are not listed).

merge_branch(Branch, Opts)

-spec merge_branch(binary(), map()) -> {ok, map()} | {error, term()}.

Ship the branch's edits to its parent: one-shot replication over the local transports starting at the fork instant (exclusive), then the attachment phase from the same boundary. Conflicts resolve on the parent through its configured machinery (deterministic LWW, conflict_merger when set); redelivery is a no-op, and a merge checkpoint (a local doc on the branch) makes repeated merges incremental. A PITR rewind is NOT merged: restored versions sit at or before the fork and the parent's vectors already contain them.

Opts: batch_size (default 100), attachments (default true), att_batch_size. Limitations (documented): the parent's conflict_merger is db-open-time config, there is no per-merge override; the report carries replication stats, not per-doc conflict detail. Merge at least once per retention window: branch retention can forget an expired tombstone, and a forgotten delete never ships.