Fact.RecordCache (Fact v0.3.1)

View Source

An in-memory LFU (Least Frequently Used) cache for decoded event records.

Since event records are immutable once written, they can be cached indefinitely without concern for invalidation. This cache avoids repeated disk reads and JSON deserialization for frequently accessed records.

The cache uses two ETS tables per database instance:

  • A data table (:set, :public, read_concurrency: true) for O(1) lookups from any process without going through the GenServer
  • A frequency table (:ordered_set, :private) for LFU eviction, sorted by {frequency, record_id}

Cache reads bypass the GenServer entirely via direct ETS access. Frequency bumps and inserts are sent as casts to avoid blocking the caller.

To prevent stale entries from monopolizing the cache, a periodic frequency decay sweep halves all frequency counters at a configurable interval (default: 10 minutes). Entries whose frequency decays to zero are evicted. This ensures that records which were hot in the past but are no longer accessed gradually lose their position to actively read records.

The cache is disabled by default and only started when a :max_size is configured via Fact.open/2:

{:ok, db} = Fact.open("data/my_database", cache: [max_size: 512 * 1024 * 1024])

When no cache process is registered for a database, get/2 returns :miss and put/3 is a no-op, adding only a fast registry lookup to the read path.

This process is started and supervised by Fact.DatabaseSupervisor.

Summary

Types

Cache configuration options.

Options accepted by start_link/1.

Cache size information returned by size/1.

Functions

Returns a specification to start this module under a supervisor.

Removes all entries from the cache.

Returns the number of records currently in the cache.

Looks up a cached event record.

Caches a decoded event record.

Returns the current cache size, maximum capacity, and usage percentage.

Starts a Fact.RecordCache process linked to the calling process.

Returns the top n most frequently accessed cached records.

Types

cache_option()

(since 0.3.0)
@type cache_option() :: {:max_size, pos_integer()} | {:decay_interval, pos_integer()}

Cache configuration options.

  • :max_size - Maximum cache size in bytes. Required to enable the cache.
  • :decay_interval - Time in milliseconds between frequency decay sweeps. Defaults to 600_000 (10 minutes). All frequency counters are halved on each sweep, preventing stale entries from monopolizing the cache indefinitely.

option()

(since 0.3.0)
@type option() ::
  {:database_id, Fact.database_id()}
  | {:name, GenServer.name()}
  | {:max_size, pos_integer()}
  | {:decay_interval, pos_integer()}

Options accepted by start_link/1.

  • :database_id - (required) The database identifier.
  • :name - (required) The registered process name, typically constructed via Fact.Registry.via/2.
  • :max_size - (required) Maximum cache size in bytes.
  • :decay_interval - See cache_option/0.

size_info()

(since 0.3.0)
@type size_info() :: %{
  current: non_neg_integer(),
  max: pos_integer(),
  percentage: float()
}

Cache size information returned by size/1.

  • :current - Current cache usage in bytes.
  • :max - Maximum configured cache size in bytes.
  • :percentage - Current usage as a percentage of max capacity (0.0 to 100.0).

Functions

child_spec(init_arg)

(since 0.3.0)

Returns a specification to start this module under a supervisor.

See Supervisor.

clear(database_id)

(since 0.3.0)
@spec clear(Fact.database_id()) :: :ok | {:error, :not_enabled}

Removes all entries from the cache.

Returns {:error, :not_enabled} when the cache is not active for the given database.

count(database_id)

(since 0.3.0)
@spec count(Fact.database_id()) :: {:ok, non_neg_integer()} | {:error, :not_enabled}

Returns the number of records currently in the cache.

Performs a direct ETS read without going through the GenServer.

Returns {:error, :not_enabled} when the cache is not active for the given database.

get(database_id, record_id)

(since 0.3.0)
@spec get(Fact.database_id(), Fact.record_id()) :: {:ok, Fact.record()} | :miss

Looks up a cached event record.

Performs a direct ETS read without going through the GenServer. On a cache hit, a frequency bump is cast to the GenServer asynchronously.

Returns {:ok, {record_id, event_map}} on hit, or :miss when the record is not cached or the cache is disabled for this database.

put(database_id, record_id, event_map)

(since 0.3.0)

Caches a decoded event record.

The insert is performed asynchronously via a cast to the GenServer. If the cache is disabled for this database, this is a no-op.

size(database_id)

(since 0.3.0)
@spec size(Fact.database_id()) :: {:ok, size_info()} | {:error, :not_enabled}

Returns the current cache size, maximum capacity, and usage percentage.

Returns {:error, :not_enabled} when the cache is not active for the given database.

start_link(opts)

(since 0.3.0)
@spec start_link([option()]) :: GenServer.on_start()

Starts a Fact.RecordCache process linked to the calling process.

top(database_id, n)

(since 0.3.0)
@spec top(Fact.database_id(), pos_integer()) ::
  {:ok, [{Fact.record_id(), non_neg_integer()}]} | {:error, :not_enabled}

Returns the top n most frequently accessed cached records.

Each entry is a {record_id, frequency} tuple, sorted by frequency in descending order.

Returns {:error, :not_enabled} when the cache is not active for the given database.