defmodule CubDB.Compactor do @moduledoc false # The `CubDB.Compactor` module takes care of the compaction, to reclaim disk # space. It does so by loading all entries in a new store (using the fast # algorithm provided by `Btree.load/2`). This will cause the new store to only # contain the entries reachable from the current root. This is the so called # compaction phase. # # After compacting, `CubDB.Compactor` also needs to catch up the compacted # Btree with updates performed on the live Btree while the compaction was # running. The catch up process can be iterated multiple times until the # compacted Btree is in sync with the live one. # # The catch up is performed by iterating through all updates happened after # the compaction (or the last catch up) started, using the `Btree.Diff` # enumerable to enumerate updates between two Btrees representing two # snapshots of the same (live) store. alias CubDB.Btree alias CubDB.Snapshot alias CubDB.Store alias CubDB.Tx import Btree, only: [value: 1, deleted: 0] @spec run(CubDB.server(), Store.File.t()) :: :ok def run(db, store) do {original_btree, compacted_btree} = CubDB.with_snapshot(db, fn %Snapshot{btree: btree} -> {btree, compact(btree, store)} end) send(db, :compaction_completed) catch_up(db, compacted_btree, original_btree) send(db, :catch_up_completed) end @spec compact(Btree.t(), Store.t()) :: Btree.t() def compact(btree, store) do Btree.load(btree, store) |> Btree.sync() end @spec catch_up(CubDB.server(), Btree.t(), Btree.t()) :: :ok def catch_up(db, compacted_btree, original_btree) do result = CubDB.transaction(db, fn %Tx{btree: ^original_btree} = tx -> {:commit, %Tx{tx | btree: compacted_btree}, :done} %Tx{btree: latest_btree} -> {:cancel, latest_btree} end) case result do :done -> :ok latest_btree -> compacted_btree = catch_up_iter(compacted_btree, original_btree, latest_btree) catch_up(db, compacted_btree, latest_btree) end end @spec catch_up_iter(Btree.t(), Btree.t(), Btree.t()) :: Btree.t() def catch_up_iter(compacted_btree, original_btree, latest_btree) do diff = Btree.Diff.new(original_btree, latest_btree) Enum.reduce(diff, compacted_btree, fn {key, value(val: value)}, compacted_btree -> Btree.insert(compacted_btree, key, value) {key, deleted()}, compacted_btree -> Btree.delete(compacted_btree, key) end) end end