Packages
ferricstore
0.9.1
0.11.14
0.11.12
0.11.11
0.11.10
0.11.9
0.11.8
0.11.7
0.11.6
0.11.5
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.3
0.10.2
0.10.1
0.10.0
0.9.1
0.9.0
0.8.0
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.0
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.3
0.4.2
0.4.1
0.4.0
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.2.0
0.1.0
FerricFlow durable workflows and queues with native-protocol storage, Raft durability, and Bitcask persistence.
Current section
Files
Jump to
Current section
Files
lib/ferricstore/raft/waraft_segment_reader.ex
defmodule Ferricstore.Raft.WARaftSegmentReader do
alias Ferricstore.Raft.WARaftSegmentReader.CommandValues
@moduledoc false
@table_prefix "raft_log_ferricstore_waraft_backend_"
@storage_root "ferricstore_waraft_backend"
@projection_dir "segment_projection_log"
@apply_projection_dir "apply_projection_log"
@apply_projection_table :ferricstore_waraft_apply_projection_cache
@apply_projection_count_tag :apply_projection_count
@apply_projection_bytes_tag :apply_projection_bytes
@apply_projection_lock_tag :apply_projection_lock
@apply_projection_disk_lock_tag :apply_projection_disk_lock
@apply_projection_disk_reader_tag :apply_projection_disk_reader
@apply_projection_select_page_size 512
@apply_projection_lock_retry_min_ms 1
@apply_projection_lock_retry_max_ms 32
@held_apply_projection_locks_key :ferricstore_waraft_apply_projection_held_locks
@held_apply_projection_disk_locks_key :ferricstore_waraft_apply_projection_held_disk_locks
@held_apply_projection_disk_read_locks_key :ferricstore_waraft_apply_projection_held_disk_read_locks
@spec put_apply_projection(binary(), non_neg_integer(), pos_integer(), [
{binary(), binary(), non_neg_integer()}
]) ::
:ok
def put_apply_projection(data_dir, shard_index, index, entries)
when is_binary(data_dir) and is_integer(shard_index) and shard_index >= 0 and
is_integer(index) and index > 0 and is_list(entries) do
table = ensure_apply_projection_table!()
root = storage_root(%{data_dir: data_dir}, shard_index)
with_apply_projection_lock(root, fn ->
:ok = ensure_apply_projection_counters(table, root)
{inserted, byte_delta} =
Enum.reduce(entries, {0, 0}, fn
{key, value, expire_at_ms}, {inserted, byte_delta}
when is_binary(key) and is_binary(value) and is_integer(expire_at_ms) ->
cache_key = {root, index, key}
entry = {cache_key, value, expire_at_ms}
case upsert_apply_projection_entry(table, cache_key, entry) do
{:inserted, value_bytes} ->
{inserted + 1, byte_delta + value_bytes}
{:replaced, previous_bytes, value_bytes} ->
{inserted, byte_delta + value_bytes - previous_bytes}
end
_invalid, acc ->
acc
end)
maybe_run_apply_projection_cache_mutation_hook(:after_upsert, %{
root: root,
index: index,
inserted: inserted,
byte_delta: byte_delta
})
increment_apply_projection_count(table, root, inserted)
adjust_apply_projection_bytes(table, root, byte_delta)
:ok
end)
end
@spec apply_projection_cache_count(binary(), non_neg_integer()) :: non_neg_integer()
def apply_projection_cache_count(data_dir, shard_index)
when is_binary(data_dir) and is_integer(shard_index) and shard_index >= 0 do
case :ets.whereis(@apply_projection_table) do
:undefined ->
0
table ->
root = storage_root(%{data_dir: data_dir}, shard_index)
read_apply_projection_count(table, root)
end
rescue
ArgumentError -> 0
end
def apply_projection_cache_count(_data_dir, _shard_index), do: 0
@doc false
@spec apply_projection_cache_bytes(binary(), non_neg_integer()) :: non_neg_integer()
def apply_projection_cache_bytes(data_dir, shard_index)
when is_binary(data_dir) and is_integer(shard_index) and shard_index >= 0 do
case :ets.whereis(@apply_projection_table) do
:undefined ->
0
table ->
root = storage_root(%{data_dir: data_dir}, shard_index)
read_apply_projection_bytes(table, root)
end
rescue
ArgumentError -> 0
end
def apply_projection_cache_bytes(_data_dir, _shard_index), do: 0
@doc false
@spec with_apply_projection_disk_lock(binary(), non_neg_integer(), (-> result)) :: result
when result: term()
def with_apply_projection_disk_lock(data_dir, shard_index, fun)
when is_binary(data_dir) and is_integer(shard_index) and shard_index >= 0 and
is_function(fun, 0) do
root = storage_root(%{data_dir: data_dir}, shard_index)
with_apply_projection_disk_lock_root(root, fun)
end
@spec apply_projection_dependency_ready?(binary(), non_neg_integer(), pos_integer()) ::
boolean()
def apply_projection_dependency_ready?(data_dir, shard_index, index)
when is_binary(data_dir) and is_integer(shard_index) and shard_index >= 0 and
is_integer(index) and index > 0 do
root = storage_root(%{data_dir: data_dir}, shard_index)
case read_apply_projection_latest_entries_from_disk(root, index) do
{:ok, _entries} -> true
:not_found -> not apply_projection_cache_entries_present?(root, index)
{:error, _reason} -> false
end
rescue
ArgumentError -> false
end
def apply_projection_dependency_ready?(_data_dir, _shard_index, _index), do: false
@spec spill_apply_projection_cache(binary(), non_neg_integer()) ::
{:ok, non_neg_integer()} | {:error, term()}
def spill_apply_projection_cache(data_dir, shard_index)
when is_binary(data_dir) and is_integer(shard_index) and shard_index >= 0 do
spill_apply_projection_cache(data_dir, shard_index, :all)
end
def spill_apply_projection_cache(_data_dir, _shard_index), do: {:ok, 0}
@spec spill_apply_projection_cache(binary(), non_neg_integer(), :all | non_neg_integer()) ::
{:ok, non_neg_integer()} | {:error, term()}
def spill_apply_projection_cache(data_dir, shard_index, max_entries)
when is_binary(data_dir) and is_integer(shard_index) and shard_index >= 0 do
root = storage_root(%{data_dir: data_dir}, shard_index)
with_apply_projection_disk_lock_root(root, fn ->
case :ets.whereis(@apply_projection_table) do
:undefined ->
{:ok, 0}
table ->
projection_root = Path.join(root, @apply_projection_dir)
table
|> apply_projection_cache_entries(root, max_entries)
|> Enum.group_by(fn {index, _key, _value, _expire_at_ms} -> index end)
|> Enum.sort_by(fn {index, _entries} -> index end)
|> spill_apply_projection_groups(data_dir, shard_index, projection_root)
end
end)
rescue
error -> {:error, {:spill_apply_projection_cache_failed, error}}
end
def spill_apply_projection_cache(_data_dir, _shard_index, _max_entries), do: {:ok, 0}
@doc false
@spec spill_apply_projection_cache(
binary(),
non_neg_integer(),
non_neg_integer(),
non_neg_integer()
) ::
{:ok, non_neg_integer()} | {:error, term()}
def spill_apply_projection_cache(data_dir, shard_index, min_entries, min_bytes)
when is_binary(data_dir) and is_integer(shard_index) and shard_index >= 0 and
is_integer(min_entries) and min_entries >= 0 and is_integer(min_bytes) and
min_bytes >= 0 do
root = storage_root(%{data_dir: data_dir}, shard_index)
with_apply_projection_disk_lock_root(root, fn ->
case :ets.whereis(@apply_projection_table) do
:undefined ->
{:ok, 0}
table ->
projection_root = Path.join(root, @apply_projection_dir)
table
|> apply_projection_cache_entries_for_limits(root, min_entries, min_bytes)
|> Enum.group_by(fn {index, _key, _value, _expire_at_ms} -> index end)
|> Enum.sort_by(fn {index, _entries} -> index end)
|> spill_apply_projection_groups(data_dir, shard_index, projection_root)
end
end)
rescue
error -> {:error, {:spill_apply_projection_cache_failed, error}}
end
def spill_apply_projection_cache(_data_dir, _shard_index, _min_entries, _min_bytes),
do: {:ok, 0}
@spec apply_projection_refs_before(binary(), non_neg_integer(), pos_integer()) :: [
{pos_integer(), binary()}
]
def apply_projection_refs_before(data_dir, shard_index, before_index)
when is_binary(data_dir) and is_integer(shard_index) and shard_index >= 0 and
is_integer(before_index) and before_index > 0 do
case :ets.whereis(@apply_projection_table) do
:undefined ->
[]
table ->
root = storage_root(%{data_dir: data_dir}, shard_index)
:ets.select(table, [
{{{root, :"$1", :"$2"}, :_, :_}, [{:<, :"$1", before_index}], [{{:"$1", :"$2"}}]}
])
end
rescue
ArgumentError -> []
end
def apply_projection_refs_before(_data_dir, _shard_index, _before_index), do: []
@spec delete_apply_projection_entries(binary(), non_neg_integer(), [
{pos_integer(), binary()}
]) :: non_neg_integer()
def delete_apply_projection_entries(data_dir, shard_index, refs)
when is_binary(data_dir) and is_integer(shard_index) and shard_index >= 0 and
is_list(refs) do
case :ets.whereis(@apply_projection_table) do
:undefined ->
0
table ->
root = storage_root(%{data_dir: data_dir}, shard_index)
with_apply_projection_lock(root, fn ->
:ok = ensure_apply_projection_counters(table, root)
{removed, removed_bytes} =
Enum.reduce(refs, {0, 0}, fn
{index, key}, {removed, removed_bytes}
when is_integer(index) and index > 0 and is_binary(key) ->
case :ets.take(table, {root, index, key}) do
[] ->
{removed, removed_bytes}
[{{^root, ^index, ^key}, value, _expire_at_ms}] when is_binary(value) ->
{removed + 1, removed_bytes + byte_size(value)}
end
_invalid, acc ->
acc
end)
decrement_apply_projection_count(table, root, removed)
adjust_apply_projection_bytes(table, root, -removed_bytes)
removed
end)
end
rescue
ArgumentError -> 0
end
def delete_apply_projection_entries(_data_dir, _shard_index, _refs), do: 0
@spec clear_apply_projection_cache(binary(), non_neg_integer()) :: non_neg_integer()
def clear_apply_projection_cache(data_dir, shard_index)
when is_binary(data_dir) and is_integer(shard_index) and shard_index >= 0 do
case :ets.whereis(@apply_projection_table) do
:undefined ->
0
table ->
root = storage_root(%{data_dir: data_dir}, shard_index)
with_apply_projection_lock(root, fn ->
removed =
:ets.select_delete(table, [
{{{root, :_, :_}, :_, :_}, [], [true]}
])
:ets.delete(table, apply_projection_count_key(root))
:ets.delete(table, apply_projection_bytes_key(root))
removed
end)
end
rescue
ArgumentError -> 0
end
def clear_apply_projection_cache(_data_dir, _shard_index), do: 0
@spec read_value(FerricStore.Instance.t(), non_neg_integer(), non_neg_integer(), binary()) ::
{:ok, binary()} | :not_found | {:error, term()}
def read_value(ctx, shard_index, index, key)
when is_integer(shard_index) and shard_index >= 0 and is_integer(index) and index > 0 and
is_binary(key) do
case read_main_log_value(ctx, shard_index, index, key) do
{:error, :segment_entry_not_found} ->
{:error, :segment_entry_not_found}
{:error, :key_not_in_segment_entry} ->
:not_found
other ->
other
end
end
def read_value(_ctx, _shard_index, _index, _key), do: {:error, :bad_segment_location}
@spec read_value_from_location(FerricStore.Instance.t(), non_neg_integer(), term(), binary()) ::
{:ok, binary()} | :not_found | {:error, term()}
def read_value_from_location(ctx, shard_index, {:waraft_segment, index}, key),
do: read_value(ctx, shard_index, index, key)
def read_value_from_location(ctx, shard_index, {:waraft_projection, index}, key),
do: read_projection_value_at(ctx, shard_index, index, key)
def read_value_from_location(ctx, shard_index, {:waraft_apply_projection, index}, key),
do: read_apply_projection_value_at(ctx, shard_index, index, key)
def read_value_from_location(_ctx, _shard_index, _file_id, _key),
do: {:error, :not_waraft_segment_location}
@doc false
@spec read_value_from_location_including_expired(
FerricStore.Instance.t(),
non_neg_integer(),
term(),
binary()
) ::
{:ok, binary()} | :not_found | {:error, term()}
def read_value_from_location_including_expired(
ctx,
shard_index,
{:waraft_apply_projection, index},
key
) do
read_apply_projection_value_at(ctx, shard_index, index, key, :include_expired)
end
def read_value_from_location_including_expired(ctx, shard_index, file_id, key) do
read_value_from_location(ctx, shard_index, file_id, key)
end
@spec read_values_from_location(FerricStore.Instance.t(), non_neg_integer(), term(), [
binary()
]) ::
{:ok, %{binary() => binary()}} | {:error, term()}
@spec read_values_from_location(
FerricStore.Instance.t(),
non_neg_integer(),
term(),
[binary()],
non_neg_integer()
) :: {:ok, %{binary() => binary()}} | {:error, term()}
def read_values_from_location(ctx, shard_index, file_id, keys, timeout_ms)
when is_list(keys) and is_integer(timeout_ms) and timeout_ms >= 0 do
deadline_ms = System.monotonic_time(:millisecond) + timeout_ms
if waraft_read_expired?(deadline_ms) do
{:error, :deadline_exceeded}
else
result = read_values_from_location(ctx, shard_index, file_id, keys)
if waraft_read_expired?(deadline_ms),
do: {:error, :deadline_exceeded},
else: result
end
end
def read_values_from_location(ctx, shard_index, {:waraft_segment, index}, keys)
when is_integer(index) and index > 0 and is_list(keys) do
case read_main_log_entry(ctx, shard_index, index) do
{:ok, entry} ->
{:ok, CommandValues.values_from_entry(entry, keys)}
{:error, :segment_entry_not_found} ->
{:error, :segment_entry_not_found}
{:error, :key_not_in_segment_entry} ->
{:ok, %{}}
{:error, reason} ->
{:error, reason}
end
end
def read_values_from_location(ctx, shard_index, {:waraft_projection, index}, keys)
when is_integer(index) and index > 0 and is_list(keys) do
projection_root = Path.join(storage_root(ctx, shard_index), @projection_dir)
root_chars = to_charlist(projection_root)
keyset = MapSet.new(keys)
case read_projection_entry_at(root_chars, index) do
{:ok, entry} ->
case entry do
{0, {:ferricstore_segment_projection_entry, key, value, _expire_at_ms}}
when is_binary(key) and is_binary(value) ->
if MapSet.member?(keyset, key), do: {:ok, %{key => value}}, else: {:ok, %{}}
_other ->
{:error, :bad_segment_projection_entry}
end
:not_found ->
{:ok, %{}}
{:error, reason} ->
{:error, reason}
end
end
def read_values_from_location(ctx, shard_index, {:waraft_apply_projection, index}, keys)
when is_integer(index) and index > 0 and is_list(keys) do
root = storage_root(ctx, shard_index)
read_mode = {:live, CommandValues.now_ms()}
{found, missing} =
keys
|> Enum.uniq()
|> Enum.reduce({%{}, []}, fn key, {found, missing} ->
case read_apply_projection_cache(root, index, key, read_mode) do
{:ok, value} -> {Map.put(found, key, value), missing}
:not_found -> {found, [key | missing]}
end
end)
case missing do
[] ->
{:ok, found}
[_ | _] ->
case read_apply_projection_latest_entries_from_disk(root, index) do
{:ok, entries} ->
found = collect_projection_entry_values(entries, missing, found, read_mode)
still_missing = reject_found_keys(missing, found)
if still_missing == [] do
{:ok, found}
else
read_apply_projection_missing_from_merged_disk(
root,
index,
still_missing,
found,
read_mode
)
end
:not_found ->
{:ok, found}
{:error, reason} ->
{:error, reason}
end
end
end
def read_values_from_location(ctx, shard_index, file_id, keys) when is_list(keys) do
Enum.reduce_while(keys, {:ok, %{}}, fn key, {:ok, acc} ->
case read_value_from_location(ctx, shard_index, file_id, key) do
{:ok, value} -> {:cont, {:ok, Map.put(acc, key, value)}}
:not_found -> {:cont, {:ok, acc}}
{:error, _reason} = error -> {:halt, error}
end
end)
end
defp waraft_read_expired?(deadline_ms),
do: System.monotonic_time(:millisecond) >= deadline_ms
defp read_apply_projection_missing_from_merged_disk(root, index, missing, found, read_mode) do
case read_apply_projection_entries_from_disk(root, index) do
{:ok, entries} ->
{:ok, collect_projection_entry_values(entries, missing, found, read_mode)}
:not_found ->
{:ok, found}
{:error, reason} ->
{:error, reason}
end
end
defp reject_found_keys(keys, found) when is_map(found) do
Enum.reject(keys, &Map.has_key?(found, &1))
end
defp reject_found_keys(keys, _found), do: keys
defp read_main_log_value(ctx, shard_index, index, key) do
with {:ok, entry} <- read_main_log_entry(ctx, shard_index, index) do
case CommandValues.value_from_entry(entry, key) do
{:ok, value} -> {:ok, value}
:deleted -> :not_found
:not_found -> {:error, :key_not_in_segment_entry}
{:error, _reason} = error -> error
end
end
end
defp read_main_log_entry(ctx, shard_index, index) do
table = log_table(shard_index)
case ets_log_lookup(table, index) do
{:ok, entry} ->
{:ok, entry}
:not_found ->
read_main_log_entry_from_disk(ctx, shard_index, index)
end
end
defp ets_log_lookup(table, index) do
case :ets.info(table) do
:undefined ->
:not_found
_info ->
case :ets.lookup(table, index) do
[{^index, entry}] -> {:ok, entry}
[] -> :not_found
end
end
rescue
ArgumentError -> :not_found
end
defp read_main_log_entry_from_disk(ctx, shard_index, wanted_index) do
root = storage_root(ctx, shard_index)
root_chars = to_charlist(root)
case :ferricstore_waraft_spike_segment_log.location_for_index(root_chars, wanted_index) do
{:ok, {_ordinal, offset, encoded_size}} ->
read_main_log_entry_from_disk_at(root_chars, wanted_index, offset, encoded_size)
:not_found ->
{:error, :segment_entry_not_found}
{:error, _reason} = error ->
error
end
end
defp read_main_log_entry_from_disk_at(root, wanted_index, offset, encoded_size) do
case :ferricstore_waraft_spike_segment_log.read_disk_at(
root,
wanted_index,
offset,
encoded_size
) do
{:ok, entry} -> {:ok, entry}
:not_found -> {:error, :segment_entry_not_found}
{:error, reason} -> {:error, reason}
end
end
defp read_projection_value_at(ctx, shard_index, index, key)
when is_integer(index) and index > 0 and is_binary(key) do
projection_root = Path.join(storage_root(ctx, shard_index), @projection_dir)
root_chars = to_charlist(projection_root)
case read_projection_entry_at(root_chars, index) do
{:ok, entry} ->
case entry do
{0, {:ferricstore_segment_projection_entry, ^key, value, _expire_at_ms}}
when is_binary(value) ->
{:ok, value}
{0, {:ferricstore_segment_projection_entry, _other_key, _value, _expire_at_ms}} ->
:not_found
_other ->
{:error, :bad_segment_projection_entry}
end
:not_found ->
:not_found
{:error, reason} ->
{:error, reason}
end
end
defp read_projection_value_at(_ctx, _shard_index, _index, _key),
do: {:error, :bad_segment_projection_location}
defp read_projection_entry_at(root_chars, index) do
case :ferricstore_waraft_spike_segment_log.location_for_index(root_chars, index) do
{:ok, {_ordinal, offset, encoded_size}} ->
case :ferricstore_waraft_spike_segment_log.read_disk_at(
root_chars,
index,
offset,
encoded_size
) do
{:ok, entry} -> {:ok, entry}
:not_found -> {:error, :projection_entry_missing_at_recorded_location}
{:error, reason} -> {:error, reason}
end
:not_found ->
:not_found
{:error, :enoent} ->
:not_found
{:error, reason} ->
{:error, reason}
end
end
defp read_apply_projection_value_at(ctx, shard_index, index, key)
when is_integer(index) and index > 0 and is_binary(key) do
root = storage_root(ctx, shard_index)
read_mode = {:live, CommandValues.now_ms()}
case read_apply_projection_cache(root, index, key, read_mode) do
{:ok, value} ->
{:ok, value}
:not_found ->
read_apply_projection_value_from_disk(root, index, key, read_mode)
end
end
defp read_apply_projection_value_at(_ctx, _shard_index, _index, _key),
do: {:error, :bad_segment_apply_projection_location}
defp read_apply_projection_value_at(ctx, shard_index, index, key, :include_expired)
when is_integer(index) and index > 0 and is_binary(key) do
root = storage_root(ctx, shard_index)
case read_apply_projection_cache(root, index, key, :include_expired) do
{:ok, value} ->
{:ok, value}
:not_found ->
read_apply_projection_value_from_disk(root, index, key, :include_expired)
end
end
defp read_apply_projection_value_at(_ctx, _shard_index, _index, _key, :include_expired),
do: {:error, :bad_segment_apply_projection_location}
defp read_apply_projection_cache(root, index, key, {:live, now_ms}) do
case :ets.whereis(@apply_projection_table) do
:undefined ->
:not_found
table ->
case :ets.lookup(table, {root, index, key}) do
[{{^root, ^index, ^key}, value, expire_at_ms}] ->
if CommandValues.live_expire_at?(expire_at_ms, now_ms),
do: {:ok, value},
else: :not_found
[] ->
:not_found
end
end
rescue
ArgumentError -> :not_found
end
defp read_apply_projection_cache(root, index, key, :include_expired) do
case :ets.whereis(@apply_projection_table) do
:undefined ->
:not_found
table ->
case :ets.lookup(table, {root, index, key}) do
[{{^root, ^index, ^key}, value, _expire_at_ms}] when is_binary(value) -> {:ok, value}
_ -> :not_found
end
end
rescue
ArgumentError -> :not_found
end
defp read_apply_projection_value_from_disk(root, index, key, read_mode) do
case read_apply_projection_latest_entries_from_disk(root, index) do
{:ok, entries} ->
case value_from_projection_entries(entries, key, read_mode) do
{:ok, _value} = ok -> ok
:not_found -> read_apply_projection_value_from_merged_disk(root, index, key, read_mode)
end
:not_found ->
:not_found
{:error, reason} ->
{:error, reason}
end
end
defp read_apply_projection_value_from_merged_disk(root, index, key, read_mode) do
case read_apply_projection_entries_from_disk(root, index) do
{:ok, entries} -> value_from_projection_entries(entries, key, read_mode)
:not_found -> :not_found
{:error, reason} -> {:error, reason}
end
end
defp read_apply_projection_latest_entries_from_disk(root, index) do
with_apply_projection_disk_read_lock_root(root, fn ->
projection_root = Path.join(root, @apply_projection_dir)
root_chars = to_charlist(projection_root)
maybe_run_apply_projection_disk_read_hook(root, index, :latest)
case :ferricstore_waraft_spike_segment_log.location_for_index(root_chars, index) do
{:ok, {_ordinal, offset, encoded_size}} ->
case :ferricstore_waraft_spike_segment_log.read_disk_at(
root_chars,
index,
offset,
encoded_size
) do
{:ok, entry} -> decode_apply_projection_entry(entry)
:not_found -> {:error, :apply_projection_entry_missing_at_recorded_location}
{:error, reason} -> {:error, reason}
end
:not_found ->
:not_found
{:error, :enoent} ->
:not_found
{:error, reason} ->
{:error, reason}
end
end)
end
defp decode_apply_projection_entry(
{0, {:ferricstore_segment_apply_projection_batch, _position, entries}}
)
when is_list(entries),
do: {:ok, entries}
defp decode_apply_projection_entry(_entry),
do: {:error, :bad_segment_apply_projection_entry}
defp read_apply_projection_entries_from_disk(root, index) do
with_apply_projection_disk_read_lock_root(root, fn ->
projection_root = Path.join(root, @apply_projection_dir)
root_chars = to_charlist(projection_root)
maybe_run_apply_projection_disk_read_hook(root, index, :merged)
with {:ok, entry} <-
:ferricstore_waraft_spike_segment_log.read_disk(root_chars, index) do
case entry do
{0, {:ferricstore_segment_apply_projection_batch, _position, entries}}
when is_list(entries) ->
{:ok, entries}
_other ->
{:error, :bad_segment_apply_projection_entry}
end
else
:not_found -> :not_found
{:error, :enoent} -> :not_found
{:error, reason} -> {:error, reason}
end
end)
end
defp apply_projection_cache_entries_present?(root, index) do
case :ets.whereis(@apply_projection_table) do
:undefined ->
false
table ->
:ets.select_count(table, [
{{{root, index, :_}, :_, :_}, [], [true]}
]) > 0
end
rescue
ArgumentError -> false
end
defp maybe_run_apply_projection_disk_read_hook(root, index, source) do
case Process.get(:ferricstore_waraft_apply_projection_disk_read_hook) do
fun when is_function(fun, 3) -> fun.(root, index, source)
_other -> :ok
end
end
defp value_from_projection_entries(entries, key, read_mode) do
Enum.reduce(entries, :not_found, fn
{^key, value, expire_at_ms}, _acc when is_binary(value) ->
if projection_entry_live?(expire_at_ms, read_mode), do: {:ok, value}, else: :not_found
_entry, acc ->
acc
end)
end
defp collect_projection_entry_values(entries, keys, acc, read_mode) do
keyset = MapSet.new(keys)
Enum.reduce(entries, acc, fn
{key, value, expire_at_ms}, values
when is_binary(key) and is_binary(value) ->
if MapSet.member?(keyset, key) do
if projection_entry_live?(expire_at_ms, read_mode),
do: Map.put(values, key, value),
else: Map.delete(values, key)
else
values
end
_entry, values ->
values
end)
end
defp projection_entry_live?(_expire_at_ms, :include_expired), do: true
defp projection_entry_live?(expire_at_ms, {:live, now_ms}),
do: CommandValues.live_expire_at?(expire_at_ms, now_ms)
defp ensure_apply_projection_table! do
case :ets.whereis(@apply_projection_table) do
:undefined ->
case Ferricstore.Raft.WARaftSegmentReader.TableOwner.ensure_table() do
:ok ->
case :ets.whereis(@apply_projection_table) do
:undefined -> raise "WARaft apply-projection cache table is unavailable"
table -> table
end
{:error, reason} ->
raise "WARaft apply-projection cache owner is unavailable: #{inspect(reason)}"
end
table ->
table
end
end
defp upsert_apply_projection_entry(table, cache_key, entry) do
case :ets.lookup(table, cache_key) do
[] ->
if :ets.insert_new(table, entry) do
{:inserted, apply_projection_entry_value_bytes(entry)}
else
upsert_apply_projection_entry(table, cache_key, entry)
end
[{^cache_key, _previous_value, _previous_expire_at_ms} = previous] ->
case :ets.select_replace(table, [{previous, [], [{:const, entry}]}]) do
1 ->
{:replaced, apply_projection_entry_value_bytes(previous),
apply_projection_entry_value_bytes(entry)}
0 ->
upsert_apply_projection_entry(table, cache_key, entry)
end
end
end
defp apply_projection_count_key(root), do: {@apply_projection_count_tag, root}
defp apply_projection_bytes_key(root), do: {@apply_projection_bytes_tag, root}
defp read_apply_projection_count(table, root) do
key = apply_projection_count_key(root)
case :ets.lookup(table, key) do
[{^key, count}] when is_integer(count) and count >= 0 ->
count
_missing_or_stale ->
with_apply_projection_lock(root, fn ->
read_or_rebuild_apply_projection_count(table, root)
end)
end
end
defp read_apply_projection_bytes(table, root) do
key = apply_projection_bytes_key(root)
case :ets.lookup(table, key) do
[{^key, bytes}] when is_integer(bytes) and bytes >= 0 ->
bytes
_missing_or_stale ->
with_apply_projection_lock(root, fn ->
read_or_rebuild_apply_projection_bytes(table, root)
end)
end
end
defp read_or_rebuild_apply_projection_count(table, root) do
key = apply_projection_count_key(root)
case :ets.lookup(table, key) do
[{^key, count}] when is_integer(count) and count >= 0 ->
count
_missing_or_stale ->
count = count_apply_projection_rows(table, root)
maybe_run_apply_projection_cache_mutation_hook(:before_counter_rebuild, %{
kind: :count,
root: root,
value: count
})
:ets.insert(table, {key, count})
count
end
end
defp read_or_rebuild_apply_projection_bytes(table, root) do
key = apply_projection_bytes_key(root)
case :ets.lookup(table, key) do
[{^key, bytes}] when is_integer(bytes) and bytes >= 0 ->
bytes
_missing_or_stale ->
bytes = count_apply_projection_value_bytes(table, root)
maybe_run_apply_projection_cache_mutation_hook(:before_counter_rebuild, %{
kind: :bytes,
root: root,
value: bytes
})
:ets.insert(table, {key, bytes})
bytes
end
end
defp ensure_apply_projection_counters(table, root) do
_count = read_or_rebuild_apply_projection_count(table, root)
_bytes = read_or_rebuild_apply_projection_bytes(table, root)
:ok
end
defp increment_apply_projection_count(_table, _root, 0), do: :ok
defp increment_apply_projection_count(table, root, count) when count > 0 do
key = apply_projection_count_key(root)
:ets.update_counter(table, key, {2, count}, {key, 0})
:ok
end
defp decrement_apply_projection_count(_table, _root, 0), do: :ok
defp decrement_apply_projection_count(table, root, count) when count > 0 do
key = apply_projection_count_key(root)
:ets.update_counter(table, key, {2, -count, 0, 0}, {key, 0})
:ok
end
defp adjust_apply_projection_bytes(_table, _root, 0), do: :ok
defp adjust_apply_projection_bytes(table, root, delta) when is_integer(delta) do
key = apply_projection_bytes_key(root)
if delta > 0 do
:ets.update_counter(table, key, {2, delta}, {key, 0})
else
:ets.update_counter(table, key, {2, delta, 0, 0}, {key, 0})
end
:ok
end
defp count_apply_projection_rows(table, root) do
:ets.select_count(table, [
{{{root, :_, :_}, :_, :_}, [], [true]}
])
end
defp count_apply_projection_value_bytes(table, root) do
table
|> :ets.select([
{{{root, :_, :_}, :"$1", :_}, [{:is_binary, :"$1"}], [{:byte_size, :"$1"}]}
])
|> Enum.sum()
end
defp apply_projection_entry_value_bytes({_cache_key, value, _expire_at_ms})
when is_binary(value),
do: byte_size(value)
defp apply_projection_cache_entries(table, root, :all) do
:ets.select(table, [
{{{root, :"$1", :"$2"}, :"$3", :"$4"}, [], [{{:"$1", :"$2", :"$3", :"$4"}}]}
])
end
defp apply_projection_cache_entries(_table, _root, max_entries)
when is_integer(max_entries) and max_entries <= 0,
do: []
defp apply_projection_cache_entries(table, root, max_entries) when is_integer(max_entries) do
selected =
case :ets.select(
table,
[
{{{root, :"$1", :"$2"}, :"$3", :"$4"}, [], [{{:"$1", :"$2", :"$3", :"$4"}}]}
],
max_entries
) do
{entries, _continuation} -> entries
:"$end_of_table" -> []
end
# Keep each Raft index in one spill record. Splitting an index would make
# misses fall back to a full apply-projection log fold to merge duplicates.
selected
|> Enum.map(fn {index, _key, _value, _expire_at_ms} -> index end)
|> MapSet.new()
|> Enum.flat_map(fn index ->
:ets.select(table, [
{{{root, index, :"$1"}, :"$2", :"$3"}, [], [{{index, :"$1", :"$2", :"$3"}}]}
])
end)
end
defp apply_projection_cache_entries_for_limits(_table, _root, 0, 0), do: []
defp apply_projection_cache_entries_for_limits(table, root, min_entries, min_bytes) do
# Byte/count targets choose indexes, then spill every row for those indexes
# so the common latest-record read stays O(1).
table
|> select_apply_projection_indexes(root, min_entries, min_bytes)
|> Enum.flat_map(fn index ->
:ets.select(table, [
{{{root, index, :"$1"}, :"$2", :"$3"}, [], [{{index, :"$1", :"$2", :"$3"}}]}
])
end)
end
defp select_apply_projection_indexes(table, root, min_entries, min_bytes) do
match_spec = [
{{{root, :"$1", :_}, :"$2", :_}, [{:is_binary, :"$2"}], [{{:"$1", {:byte_size, :"$2"}}}]}
]
case :ets.select(table, match_spec, @apply_projection_select_page_size) do
{rows, continuation} ->
consume_apply_projection_index_page(
rows,
continuation,
MapSet.new(),
0,
0,
min_entries,
min_bytes
)
:"$end_of_table" ->
MapSet.new()
end
end
defp consume_apply_projection_index_page(
rows,
continuation,
indexes,
selected_entries,
selected_bytes,
min_entries,
min_bytes
) do
{indexes, selected_entries, selected_bytes, complete?} =
Enum.reduce_while(
rows,
{indexes, selected_entries, selected_bytes, false},
fn {index, value_bytes}, {indexes, entry_count, byte_count, _complete?}
when is_integer(index) and index > 0 and is_integer(value_bytes) and
value_bytes >= 0 ->
indexes = MapSet.put(indexes, index)
entry_count = entry_count + 1
byte_count = byte_count + value_bytes
complete? = entry_count >= min_entries and byte_count >= min_bytes
result = {indexes, entry_count, byte_count, complete?}
if complete?, do: {:halt, result}, else: {:cont, result}
end
)
cond do
complete? ->
indexes
continuation == :"$end_of_table" ->
indexes
true ->
case :ets.select(continuation) do
{next_rows, next_continuation} ->
consume_apply_projection_index_page(
next_rows,
next_continuation,
indexes,
selected_entries,
selected_bytes,
min_entries,
min_bytes
)
:"$end_of_table" ->
indexes
end
end
end
defp spill_apply_projection_groups(groups, data_dir, shard_index, projection_root) do
{batches, cached_entries} =
Enum.map_reduce(groups, [], fn {index, entries}, ref_acc ->
batch =
Enum.map(entries, fn {_index, key, value, expire_at_ms} ->
{key, value, expire_at_ms}
end)
cached_entries = Enum.reduce(entries, ref_acc, fn entry, acc -> [entry | acc] end)
{{{:raft_log_pos, index, 0}, batch}, cached_entries}
end)
case write_apply_projection_spill(projection_root, batches) do
:ok ->
{:ok, delete_spilled_apply_projection_entries(data_dir, shard_index, cached_entries)}
{:error, _reason} = error ->
error
end
end
defp delete_spilled_apply_projection_entries(data_dir, shard_index, cached_entries) do
case :ets.whereis(@apply_projection_table) do
:undefined ->
0
table ->
root = storage_root(%{data_dir: data_dir}, shard_index)
maybe_run_apply_projection_cache_mutation_hook(:before_spill_delete_lock, %{
root: root,
cached_entries: length(cached_entries)
})
with_apply_projection_lock(root, fn ->
:ok = ensure_apply_projection_counters(table, root)
{removed, removed_bytes} =
Enum.reduce(cached_entries, {0, 0}, fn
{index, key, value, expire_at_ms}, {removed, removed_bytes}
when is_integer(index) and index > 0 and is_binary(key) and is_binary(value) and
is_integer(expire_at_ms) ->
cached_entry = {{root, index, key}, value, expire_at_ms}
case :ets.select_delete(table, [{cached_entry, [], [true]}]) do
1 -> {removed + 1, removed_bytes + byte_size(value)}
0 -> {removed, removed_bytes}
end
_invalid, acc ->
acc
end)
decrement_apply_projection_count(table, root, removed)
adjust_apply_projection_bytes(table, root, -removed_bytes)
removed
end)
end
rescue
ArgumentError -> 0
end
defp write_apply_projection_spill(_projection_root, []), do: :ok
defp write_apply_projection_spill(projection_root, batches) do
with :ok <- maybe_run_apply_projection_spill_hook(batches) do
case :ferricstore_waraft_spike_segment_log.write_projection_batches_sync(
to_charlist(projection_root),
batches
) do
:ok -> :ok
{:error, reason} -> {:error, {:write_apply_projection_spill_failed, reason}}
other -> {:error, {:write_apply_projection_spill_failed, other}}
end
end
end
defp maybe_run_apply_projection_spill_hook(batches) do
case Application.get_env(:ferricstore, :waraft_apply_projection_spill_hook) do
fun when is_function(fun, 1) ->
case fun.(batches) do
:ok -> :ok
nil -> :ok
{:error, _reason} = error -> error
other -> {:error, {:apply_projection_spill_hook, other}}
end
_other ->
:ok
end
end
defp with_apply_projection_lock(root, fun) when is_binary(root) and is_function(fun, 0) do
with_apply_projection_named_lock(
root,
@apply_projection_lock_tag,
@held_apply_projection_locks_key,
fun
)
end
defp with_apply_projection_disk_lock_root(root, fun)
when is_binary(root) and is_function(fun, 0) do
if apply_projection_named_lock_held?(@held_apply_projection_disk_read_locks_key, root) and
not apply_projection_named_lock_held?(@held_apply_projection_disk_locks_key, root) do
raise ArgumentError, "cannot upgrade an apply-projection disk read latch"
end
with_apply_projection_named_lock(
root,
@apply_projection_disk_lock_tag,
@held_apply_projection_disk_locks_key,
fn ->
:ok = wait_for_apply_projection_disk_readers(root)
fun.()
end
)
end
defp with_apply_projection_disk_read_lock_root(root, fun)
when is_binary(root) and is_function(fun, 0) do
if apply_projection_named_lock_held?(@held_apply_projection_disk_locks_key, root) do
fun.()
else
with_apply_projection_disk_read_lock(root, fun)
end
end
defp with_apply_projection_disk_read_lock(root, fun) do
held = Process.get(@held_apply_projection_disk_read_locks_key, %{})
case Map.get(held, root) do
nil ->
:ok = acquire_apply_projection_disk_read_lock(root)
Process.put(@held_apply_projection_disk_read_locks_key, Map.put(held, root, 1))
try do
fun.()
after
release_apply_projection_disk_read_lock(root)
end
count when is_integer(count) and count > 0 ->
Process.put(
@held_apply_projection_disk_read_locks_key,
Map.put(held, root, count + 1)
)
try do
fun.()
after
release_apply_projection_disk_read_lock(root)
end
end
end
defp with_apply_projection_named_lock(root, lock_tag, held_locks_key, fun) do
held = Process.get(held_locks_key, %{})
case Map.get(held, root) do
nil ->
:ok = acquire_apply_projection_lock(root, lock_tag)
Process.put(held_locks_key, Map.put(held, root, 1))
try do
fun.()
after
release_apply_projection_lock(root, lock_tag, held_locks_key)
end
count when is_integer(count) and count > 0 ->
Process.put(held_locks_key, Map.put(held, root, count + 1))
try do
fun.()
after
release_apply_projection_lock(root, lock_tag, held_locks_key)
end
end
end
defp acquire_apply_projection_lock(root, lock_tag),
do: acquire_apply_projection_lock(root, lock_tag, @apply_projection_lock_retry_min_ms)
defp acquire_apply_projection_lock(root, lock_tag, wait_ms) do
table = ensure_apply_projection_table!()
key = apply_projection_lock_key(lock_tag, root)
case :ets.insert_new(table, {key, self()}) do
true ->
:ok
false ->
wait_for_apply_projection_lock(table, key, root, lock_tag, wait_ms)
end
rescue
ArgumentError ->
apply_projection_lock_backoff(wait_ms)
acquire_apply_projection_lock(
root,
lock_tag,
next_apply_projection_lock_backoff(wait_ms)
)
end
defp wait_for_apply_projection_lock(table, key, root, lock_tag, wait_ms) do
next_wait_ms =
case :ets.lookup(table, key) do
[{^key, holder}] when is_pid(holder) ->
if Process.alive?(holder) do
apply_projection_lock_backoff(wait_ms)
next_apply_projection_lock_backoff(wait_ms)
else
:ets.select_delete(table, [{{key, holder}, [], [true]}])
@apply_projection_lock_retry_min_ms
end
_missing_or_invalid ->
:ets.delete(table, key)
@apply_projection_lock_retry_min_ms
end
acquire_apply_projection_lock(root, lock_tag, next_wait_ms)
rescue
ArgumentError ->
apply_projection_lock_backoff(wait_ms)
acquire_apply_projection_lock(
root,
lock_tag,
next_apply_projection_lock_backoff(wait_ms)
)
end
defp acquire_apply_projection_disk_read_lock(root),
do: acquire_apply_projection_disk_read_lock(root, @apply_projection_lock_retry_min_ms)
defp acquire_apply_projection_disk_read_lock(root, wait_ms) do
table = ensure_apply_projection_table!()
writer_key = apply_projection_lock_key(@apply_projection_disk_lock_tag, root)
reader_key = apply_projection_disk_reader_key(root, self())
case :ets.lookup(table, writer_key) do
[] ->
acquire_apply_projection_disk_read_lock_without_writer(
table,
writer_key,
reader_key,
root,
wait_ms
)
[{^writer_key, holder}] when is_pid(holder) ->
if Process.alive?(holder) do
apply_projection_lock_backoff(wait_ms)
acquire_apply_projection_disk_read_lock(
root,
next_apply_projection_lock_backoff(wait_ms)
)
else
:ets.select_delete(table, [{{writer_key, holder}, [], [true]}])
acquire_apply_projection_disk_read_lock(root)
end
_missing_or_invalid ->
:ets.delete(table, writer_key)
acquire_apply_projection_disk_read_lock(root)
end
rescue
ArgumentError ->
apply_projection_lock_backoff(wait_ms)
acquire_apply_projection_disk_read_lock(
root,
next_apply_projection_lock_backoff(wait_ms)
)
end
defp acquire_apply_projection_disk_read_lock_without_writer(
table,
writer_key,
reader_key,
root,
wait_ms
) do
case :ets.insert_new(table, {reader_key, self()}) do
true ->
case :ets.lookup(table, writer_key) do
[] ->
:ok
[{^writer_key, holder}] when is_pid(holder) ->
:ets.select_delete(table, [{{reader_key, self()}, [], [true]}])
if Process.alive?(holder) do
apply_projection_lock_backoff(wait_ms)
acquire_apply_projection_disk_read_lock(
root,
next_apply_projection_lock_backoff(wait_ms)
)
else
:ets.select_delete(table, [{{writer_key, holder}, [], [true]}])
acquire_apply_projection_disk_read_lock(root)
end
_missing_or_invalid ->
:ets.select_delete(table, [{{reader_key, self()}, [], [true]}])
:ets.delete(table, writer_key)
acquire_apply_projection_disk_read_lock(root)
end
false ->
:ets.select_delete(table, [{{reader_key, self()}, [], [true]}])
acquire_apply_projection_disk_read_lock(root)
end
end
defp wait_for_apply_projection_disk_readers(root),
do: wait_for_apply_projection_disk_readers(root, @apply_projection_lock_retry_min_ms)
defp wait_for_apply_projection_disk_readers(root, wait_ms) do
table = ensure_apply_projection_table!()
live_reader? =
table
|> :ets.select([
{{{@apply_projection_disk_reader_tag, root, :"$1"}, :"$2"}, [], [{{:"$1", :"$2"}}]}
])
|> Enum.reduce(false, fn {reader_pid, holder}, live_reader? ->
if is_pid(reader_pid) and holder == reader_pid and Process.alive?(reader_pid) do
true
else
reader_key = apply_projection_disk_reader_key(root, reader_pid)
:ets.select_delete(table, [{{reader_key, holder}, [], [true]}])
live_reader?
end
end)
if live_reader? do
apply_projection_lock_backoff(wait_ms)
wait_for_apply_projection_disk_readers(
root,
next_apply_projection_lock_backoff(wait_ms)
)
else
:ok
end
rescue
ArgumentError ->
apply_projection_lock_backoff(wait_ms)
wait_for_apply_projection_disk_readers(
root,
next_apply_projection_lock_backoff(wait_ms)
)
end
defp release_apply_projection_lock(root, lock_tag, held_locks_key) do
held = Process.get(held_locks_key, %{})
case Map.get(held, root) do
count when is_integer(count) and count > 1 ->
Process.put(held_locks_key, Map.put(held, root, count - 1))
1 ->
next = Map.delete(held, root)
if map_size(next) == 0 do
Process.delete(held_locks_key)
else
Process.put(held_locks_key, next)
end
table = ensure_apply_projection_table!()
key = apply_projection_lock_key(lock_tag, root)
:ets.select_delete(table, [{{key, self()}, [], [true]}])
_not_held ->
:ok
end
:ok
rescue
ArgumentError -> :ok
end
defp release_apply_projection_disk_read_lock(root) do
held = Process.get(@held_apply_projection_disk_read_locks_key, %{})
case Map.get(held, root) do
count when is_integer(count) and count > 1 ->
Process.put(
@held_apply_projection_disk_read_locks_key,
Map.put(held, root, count - 1)
)
1 ->
next = Map.delete(held, root)
if map_size(next) == 0 do
Process.delete(@held_apply_projection_disk_read_locks_key)
else
Process.put(@held_apply_projection_disk_read_locks_key, next)
end
table = ensure_apply_projection_table!()
reader_key = apply_projection_disk_reader_key(root, self())
:ets.select_delete(table, [{{reader_key, self()}, [], [true]}])
_not_held ->
:ok
end
:ok
rescue
ArgumentError -> :ok
end
defp apply_projection_named_lock_held?(held_locks_key, root) do
case Process.get(held_locks_key, %{}) do
%{^root => count} when is_integer(count) and count > 0 -> true
_not_held -> false
end
end
defp apply_projection_lock_key(lock_tag, root), do: {lock_tag, root}
defp apply_projection_disk_reader_key(root, reader_pid),
do: {@apply_projection_disk_reader_tag, root, reader_pid}
defp apply_projection_lock_backoff(wait_ms) do
maybe_run_apply_projection_lock_backoff_hook(wait_ms)
receive do
after
wait_ms -> :ok
end
end
defp next_apply_projection_lock_backoff(wait_ms),
do: min(wait_ms * 2, @apply_projection_lock_retry_max_ms)
if Mix.env() == :test do
defp maybe_run_apply_projection_cache_mutation_hook(phase, metadata) do
case Application.get_env(:ferricstore, :waraft_apply_projection_cache_mutation_hook) do
hook when is_function(hook, 2) -> hook.(phase, metadata)
_other -> :ok
end
end
defp maybe_run_apply_projection_lock_backoff_hook(wait_ms) do
case Process.get(:ferricstore_waraft_apply_projection_lock_backoff_hook) do
hook when is_function(hook, 1) -> hook.(wait_ms)
_other -> :ok
end
end
else
defp maybe_run_apply_projection_cache_mutation_hook(_phase, _metadata), do: :ok
defp maybe_run_apply_projection_lock_backoff_hook(_wait_ms), do: :ok
end
defp storage_root(%{data_dir: data_dir}, shard_index) do
Path.join([data_dir, "waraft", "#{@storage_root}.#{shard_index + 1}"])
end
defp log_table(shard_index) do
String.to_existing_atom("#{@table_prefix}#{shard_index + 1}")
rescue
ArgumentError -> nil
end
end