View Source Alembic.Cache (alembic v0.1.0)

OTP GenServer owning a :public ETS table used as a compiled-template cache, keyed by {path, mtime}. A cache hit skips the tokenize → parse pipeline entirely; a modified source file (different mtime) is a different key, so it is automatically a miss.

get/1 reads ETS directly (:ets.lookup/2) — it never goes through the GenServer mailbox, so concurrent readers are never serialized against each other. put/2, invalidate/1, and clear/0 are casts, serialized through the GenServer as the single writer, giving mutual exclusion on writes without locks. sweep/0 is a call (it needs to return a count).

This is the project's first real OTP module — GenServer as an actor with a mailbox (casts/calls are serialized through the process automatically), ETS as shared memory (reads bypass the mailbox for concurrency, writes go through the GenServer for safety), and supervision as fault tolerance (the one_for_one supervisor in Alembic.Application restarts this GenServer — and therefore recreates its ETS table — if it crashes).

Deviation from issue 1.5.2: no :telemetry dependency

The issue's task list calls for :telemetry.execute/3 on hit/miss. :telemetry is a separate Hex package, and issue 1.1.1 established a hard zero-runtime-dependencies policy (ex_doc only, dev-only). Adding a telemetry dependency here would violate that project-wide constraint set two milestones earlier. Logger.debug/1 calls with the same path/hit-or-miss information stand in for the telemetry events instead — observable, but without the extra dependency.

Each call passes a zero-arity function (Logger.debug(fn -> ... end)), not a plain string, so the "Alembic.Cache hit: #{path}" interpolation only runs when the configured Logger level would actually emit it. do_get/1 is the hottest path in the module — this matters there more than the write paths below, which are already off the direct read path entirely.

Summary

Functions

Returns a specification to start this module under a supervisor.

Removes every entry in the cache.

Whether the cache is enabled (config :alembic, :cache, default true). Every other public function in this module is a no-op when this is false.

Looks up a compiled AST by its resolved path and current mtime. Reads ETS directly (:ets.lookup/2) — never goes through the GenServer mailbox, so concurrent readers are never serialized against each other.

Removes every cached entry for path, regardless of the mtime they were stored under.

Stores a compiled AST under {path, current_mtime}. A write, so it goes through the GenServer as a cast (serializing mutation); use sweep/0 (a call, sharing the same FIFO mailbox as casts) to wait for a prior put/2 to be processed before a synchronous get/1.

Removes entries whose source file no longer exists or has a different mtime than when it was cached. Returns the count removed.

Functions

Returns a specification to start this module under a supervisor.

See Supervisor.

@spec clear() :: :ok

Removes every entry in the cache.

Examples

iex> path = Path.join(System.tmp_dir!(), "alembic_cache_doctest.html")
iex> Alembic.Cache.put(path, [{:text, "hello"}])
iex> Alembic.Cache.clear()
iex> Alembic.Cache.sweep()
iex> Alembic.Cache.get(path)
:miss
@spec enabled?() :: boolean()

Whether the cache is enabled (config :alembic, :cache, default true). Every other public function in this module is a no-op when this is false.

Examples

iex> Alembic.Cache.enabled?()
true
@spec get(String.t()) :: {:hit, term()} | :miss

Looks up a compiled AST by its resolved path and current mtime. Reads ETS directly (:ets.lookup/2) — never goes through the GenServer mailbox, so concurrent readers are never serialized against each other.

Examples

iex> path = Path.join(System.tmp_dir!(), "alembic_cache_doctest.html")
iex> File.write!(path, "content")
iex> Alembic.Cache.invalidate(path)
iex> Alembic.Cache.sweep()
iex> Alembic.Cache.get(path)
:miss

iex> path = Path.join(System.tmp_dir!(), "alembic_cache_doctest_hit.html")
iex> File.write!(path, "content")
iex> Alembic.Cache.put(path, [{:text, "hello"}])
iex> Alembic.Cache.sweep()
iex> Alembic.Cache.get(path)
{:hit, [{:text, "hello"}]}
@spec invalidate(String.t()) :: :ok

Removes every cached entry for path, regardless of the mtime they were stored under.

Examples

iex> path = Path.join(System.tmp_dir!(), "alembic_cache_doctest.html")
iex> Alembic.Cache.put(path, [{:text, "hello"}])
iex> Alembic.Cache.invalidate(path)
iex> Alembic.Cache.sweep()
iex> Alembic.Cache.get(path)
:miss
@spec put(String.t(), term()) :: :ok

Stores a compiled AST under {path, current_mtime}. A write, so it goes through the GenServer as a cast (serializing mutation); use sweep/0 (a call, sharing the same FIFO mailbox as casts) to wait for a prior put/2 to be processed before a synchronous get/1.

Examples

iex> path = Path.join(System.tmp_dir!(), "alembic_cache_doctest.html")
iex> Alembic.Cache.put(path, [{:text, "hello"}])
:ok
@spec sweep() :: {:ok, non_neg_integer()}

Removes entries whose source file no longer exists or has a different mtime than when it was cached. Returns the count removed.

Examples

iex> path = Path.join(System.tmp_dir!(), "alembic_cache_doctest_sweep.html")
iex> File.write!(path, "content")
iex> Alembic.Cache.put(path, [{:text, "hello"}])
iex> Alembic.Cache.sweep()
iex> File.rm!(path)
iex> {:ok, removed} = Alembic.Cache.sweep()
iex> removed >= 1
true