Rexd.InPlace (Rexd v1.0.0)

Copy Markdown View Source

In-place patching: rebuilding the new version inside the storage that holds the basis, without a second copy, following Rasch and Burns, In-Place Rsync (USENIX 2003).

A delta lists its commands in output order, and output positions follow from the command lengths. Applied in that order over the basis itself, a copy may read bytes that an earlier command has already overwritten. Copies are therefore applied in a dependency order instead: copy i must run before copy j when i reads bytes that j writes. Literal commands read nothing from the basis and are written last.

Dependencies can form cycles, for example two blocks that swap places. No order satisfies a cycle, so the sender breaks each one by turning a copy on it into a literal carrying the same bytes, choosing the shortest copy on the cycle. The result is an ordinary delta: rdiff patch and Rexd.patch/3 apply it as usual, and patch/4 can apply it in place.

  • Sender: make_safe/2, or Rexd.delta(signature, new, in_place: true).
  • Receiver: patch/4, with functions that read and write the storage.

A copy that overlaps its own destination is applied in the direction that never reads a byte after writing it, like memmove.

Interruption

In-place patching overwrites the basis. If it stops part-way, through a crash or a failing write function, the storage holds a mixture of both versions and neither can be rebuilt from it. Use it where the new version can be obtained again in full, or where the storage offers its own journaling.

Cost

The dependency order is computed from the copy commands alone, in O(c log c) time for c copies, whatever their read ranges; a hostile delta cannot make it quadratic. The receiver holds the command list, including literal data, in memory.

Summary

Types

Reasons patch/4 can fail. Nothing has been written when they are returned.

Reads length bytes of the storage starting at offset.

Writes data into the storage starting at offset.

Functions

Returns an equivalent delta that patch/4 can apply in place.

Applies delta to storage that holds the basis, overwriting it with the new version.

Types

error()

@type error() ::
  {:copy_out_of_range, non_neg_integer(), non_neg_integer()}
  | :not_in_place_safe

Reasons patch/4 can fail. Nothing has been written when they are returned.

read_fun()

@type read_fun() :: (non_neg_integer(), pos_integer() -> binary())

Reads length bytes of the storage starting at offset.

write_fun()

@type write_fun() :: (non_neg_integer(), binary() -> term())

Writes data into the storage starting at offset.

Functions

make_safe(delta, new)

@spec make_safe(Rexd.Delta.t(), binary() | read_fun()) :: Rexd.Delta.t()

Returns an equivalent delta that patch/4 can apply in place.

Copies that close a dependency cycle are replaced by literals holding the same bytes, read from new: the new version as a binary, or a function fn offset, length -> binary end reading it. On each cycle the shortest copy is converted, the earliest in output order among equals. The rebuilt data is unchanged.

iex> basis = "AAAAAAAA" <> "BBBBBBBBBBBB"
iex> new = "BBBBBBBBBBBB" <> "AAAAAAAA"
iex> swapped = %Rexd.Delta{commands: [{:copy, 8, 12}, {:copy, 0, 8}]}
iex> safe = Rexd.InPlace.make_safe(swapped, new)
iex> safe.commands
[{:copy, 8, 12}, {:literal, "AAAAAAAA"}]
iex> Rexd.patch(basis, safe)
{:ok, "BBBBBBBBBBBBAAAAAAAA"}

patch(delta, basis_size, read, write)

@spec patch(Rexd.Delta.t(), non_neg_integer(), read_fun(), write_fun()) ::
  {:ok, non_neg_integer()} | {:error, error()}

Applies delta to storage that holds the basis, overwriting it with the new version.

basis_size is the size of the basis in the storage. read and write access the storage, reading and writing at most 64 KiB at a time. On success returns {:ok, new_size}; when the new version is shorter than the basis, the caller truncates the storage to new_size.

Everything that can be checked is checked before the first write: every copy must lie inside the basis, and the copies must have a dependency order (the delta was produced by make_safe/2 or has no cycles to begin with).

iex> basis = "0123456789"
iex> new = "456789" <> "0123"
iex> sig = Rexd.signature(basis, block_len: 2)
iex> delta = Rexd.delta(sig, new, in_place: true)
iex> {:ok, storage} = Agent.start_link(fn -> basis end)
iex> read = fn offset, len -> Agent.get(storage, &binary_part(&1, offset, len)) end
iex> write = fn offset, data ->
...>   Agent.update(storage, fn bin ->
...>     <<head::binary-size(offset), _::binary-size(byte_size(data)), tail::binary>> = bin
...>     head <> data <> tail
...>   end)
...> end
iex> Rexd.InPlace.patch(delta, byte_size(basis), read, write)
{:ok, 10}
iex> Agent.get(storage, & &1)
"4567890123"