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
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.
- 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.