Packages
ferricstore
0.10.2
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/store/active_file.ex
defmodule Ferricstore.Store.ActiveFile do
@moduledoc """
Tracks the active log file for each shard.
Uses a per-row atomics generation counter plus a process dictionary cache.
Reads stay one `Process.get/1` plus one `:atomics.get/2` on the hot path,
while a rotation invalidates only callers that cached that exact shard.
## Usage
# In Shard init and rotation:
ActiveFile.publish(instance_ctx, shard_index, file_id, file_path, shard_data_path)
# In Router's local-origin write path (hot path):
{file_id, file_path, shard_data_path} = ActiveFile.get(instance_ctx, shard_index)
"""
@table :ferricstore_active_files
@doc """
Ensures the supervised active-file registry is available.
"""
@spec init(non_neg_integer()) :: :ok | {:error, :table_owner_unavailable}
def init(_shard_count) do
Ferricstore.Store.ActiveFile.TableOwner.ensure_table()
end
@doc """
Publishes the active file metadata for a shard.
Called from `Shard.init/1` and `Shard.maybe_rotate_file/1`.
"""
@spec publish(non_neg_integer(), non_neg_integer(), binary(), binary()) :: :ok
def publish(shard_index, file_id, file_path, shard_data_path) do
publish(nil, shard_index, file_id, file_path, shard_data_path)
end
@doc """
Publishes active file metadata for a shard in a specific instance.
Every instance uses `{instance_name, shard_index}` so registry identity is
uniform across default and embedded stores.
"""
@spec publish(map() | nil, non_neg_integer(), non_neg_integer(), binary(), binary()) :: :ok
def publish(ctx, shard_index, file_id, file_path, shard_data_path) do
table_key = table_key(ctx, shard_index)
publish_row(table_key, file_id, file_path, shard_data_path)
end
@doc """
Returns `{file_id, file_path, shard_data_path}` for the given shard.
~15ns hot path (atomics check + process dictionary cache hit).
~100ns cold (ETS lookup on generation mismatch).
"""
@spec get(non_neg_integer()) :: {non_neg_integer(), binary(), binary()}
def get(shard_index) do
get(nil, shard_index)
end
@doc """
Returns active file metadata for a shard in a specific instance.
Prefer this in production paths that already have an Instance context.
"""
@spec get(map() | nil, non_neg_integer()) :: {non_neg_integer(), binary(), binary()}
def get(ctx, shard_index) do
table_key = table_key(ctx, shard_index)
cache_key = {:active_file_cache, table_key}
case Process.get(cache_key) do
{ref, cached_gen, file_id, file_path, shard_data_path} ->
if :atomics.get(ref, 1) == cached_gen do
{file_id, file_path, shard_data_path}
else
refresh_cache(table_key, cache_key)
end
_ ->
refresh_cache(table_key, cache_key)
end
end
@doc false
@spec delete(non_neg_integer()) :: :ok
def delete(shard_index), do: delete(nil, shard_index)
@doc false
@spec delete(map() | nil, non_neg_integer()) :: :ok
def delete(ctx, shard_index) do
table_key = table_key(ctx, shard_index)
delete_row(table_key)
Process.delete({:active_file_cache, table_key})
:ok
end
@doc """
Removes active-file rows for a custom instance.
Called during instance cleanup. The generation bump invalidates process
dictionary caches that may still hold paths for the stopped instance.
"""
@spec cleanup_instance(map()) :: :ok
def cleanup_instance(%{name: :default}), do: :ok
def cleanup_instance(%{name: name, shard_count: shard_count}) do
if :ets.whereis(@table) != :undefined and shard_count > 0 do
Enum.each(0..(shard_count - 1), fn shard_index ->
table_key = {name, shard_index}
delete_row(table_key)
Process.delete({:active_file_cache, table_key})
end)
end
:ok
end
defp table_key(nil, shard_index), do: {:default, shard_index}
defp table_key(%{name: :default}, shard_index), do: {:default, shard_index}
defp table_key(%{name: name}, shard_index), do: {name, shard_index}
defp publish_row(table_key, file_id, file_path, shard_data_path) do
case :ets.lookup(@table, table_key) do
[{^table_key, ref, _old_file_id, _old_file_path, _old_shard_path}] ->
:ets.insert(@table, {table_key, ref, file_id, file_path, shard_data_path})
:atomics.add(ref, 1, 1)
:ok
[] ->
ref = :atomics.new(1, signed: false)
if :ets.insert_new(@table, {table_key, ref, file_id, file_path, shard_data_path}) do
:ok
else
publish_row(table_key, file_id, file_path, shard_data_path)
end
end
end
defp refresh_cache(table_key, cache_key) do
case :ets.lookup(@table, table_key) do
[{^table_key, ref, _file_id, _file_path, _shard_data_path}] ->
generation_before = :atomics.get(ref, 1)
case :ets.lookup(@table, table_key) do
[{^table_key, ^ref, file_id, file_path, shard_data_path}] ->
if :atomics.get(ref, 1) == generation_before do
Process.put(
cache_key,
{ref, generation_before, file_id, file_path, shard_data_path}
)
{file_id, file_path, shard_data_path}
else
refresh_cache(table_key, cache_key)
end
_changed_or_deleted ->
refresh_cache(table_key, cache_key)
end
[] ->
Process.delete(cache_key)
raise MatchError, term: []
end
end
defp delete_row(table_key) do
case :ets.take(@table, table_key) do
[{^table_key, ref, _file_id, _file_path, _shard_data_path}] ->
:atomics.add(ref, 1, 1)
:ok
[] ->
:ok
end
end
end