Cyclium.AtomGuard (Cyclium v0.1.9)

Copy Markdown View Source

Bounded string-to-atom conversion for data sourced from outside the codebase (DB-defined dynamic actor/workflow definitions, persisted workflow results, and LLM-produced tool/output names).

Atoms are never garbage-collected and the BEAM atom table is bounded (:erlang.system_info(:atom_limit), ~1M by default). Converting high-cardinality external strings with String.to_atom/1 — actor ids, expectation/step ids, subject keys, JSON keys, LLM-chosen tool names — leaks atoms permanently and can crash every node in the cluster with system_limit.

intern!/1 is a drop-in replacement for String.to_atom/1 that:

  • returns an existing atom without minting (so reloading the same definition, or any value that matches a compiled atom, never grows the table), and
  • mints a new atom only while the atom table has headroom; once usage crosses the configured threshold it raises Cyclium.AtomGuard.LimitError.

The net effect: an abusive or runaway set of dynamic definitions fails to load (a localized, catchable error) instead of taking down the node.

Configuration

# Refuse to mint new atoms once the atom table is this full (0.0–1.0).
# Default: 0.8
config :cyclium, :atom_guard_max_usage, 0.8

Summary

Functions

Returns {:ok, atom} if string already corresponds to an atom, otherwise :error. Never mints a new atom.

Convert value to an atom, bounded by atom-table headroom.

Functions

existing_atom(string)

@spec existing_atom(String.t()) :: {:ok, atom()} | :error

Returns {:ok, atom} if string already corresponds to an atom, otherwise :error. Never mints a new atom.

intern!(value)

@spec intern!(term()) :: atom()

Convert value to an atom, bounded by atom-table headroom.

  • Atoms (and nil) pass through unchanged.
  • Binaries that already correspond to an existing atom are returned without minting — no table growth.
  • A genuinely new binary is minted only while atom-table usage is below the configured threshold; past it, raises LimitError.
  • Other terms are stringified first.