Skip to content

Add a hibernate_after supervisor option - #39

Merged
chrismccord merged 4 commits into
mainfrom
cm-hibernate-after
Sep 30, 2026
Merged

chrismccord merged 4 commits into
mainfrom
cm-hibernate-after

Conversation

@chrismccord

@chrismccord chrismccord commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

A DurableServer process never hibernates, so an idle server keeps its heap at its high-water mark. In one production deployment, an idle server held about 1.06 MB, of which 54 KB survived a GC (live state: 38 KB). With thousands of mostly idle servers per node, that's gigabytes held for nothing.

Change

DurableServer.Supervisor accepts :hibernate_after on the mount, next to the other per-module settings:

{DurableServer.Supervisor,
 name: MyApp.DurableSup,
 max_children: %{MyApp.SessionServer => 5_000},
 hibernate_after: %{MyApp.SessionServer => 15_000, default: 60_000}}
  • An integer applies to every module under that supervisor.
  • A map of Module => ms applies to the listed modules. A :default key covers modules not listed; without it, unlisted modules don't hibernate.
  • nil (default): no change from today.

The supervisor starts every child through one child spec with its config, including start_child, restart claims, rehoming and preloaded boot. So DurableServer.start_link/1 looks up its module and passes hibernate_after: to GenServer.start_link/3. The lookup is by module name, so it works whether or not the module is loaded yet. Invalid values are rejected when the supervisor starts.

Tests

  • A per-module map hibernates only the listed modules.
  • :default covers unlisted modules.
  • An integer applies to every module.
  • With no setting, servers never hibernate.
  • Invalid values fail at start.

The "does hibernate" tests fail without the start_link change. On OTP 28, a hibernated gen_server reports gen_server:loop_hibernate/4; the tests accept that and the older erlang:hibernate/3.

A DurableServer process never hibernated, so an idle server kept its heap at
its high-water mark: in one production deployment an idle server held about
1 MB, of which 54 KB survived a GC.

DurableServer.Supervisor now accepts :hibernate_after, either one positive
integer for every module or a map of module => ms. Every child the supervisor
starts, including restarts and rehoming, passes it to GenServer.start_link,
so an idle server hibernates and its heap shrinks to live data. Default nil
keeps the current behaviour.
A server's idle behaviour belongs to its module, which travels with it
however the supervisor starts it. use DurableServer, hibernate_after: ms
sets it for that module and hibernate_after: false opts out; the
supervisor's :hibernate_after is now one integer used as the default for
modules that set nothing. The per-module map on the supervisor is gone.
function_exported?/3 is false for a module that is not loaded yet, so a
server type whose module had not been loaded on that node got no
hibernate_after on its first start. Load the module before reading it.
Per-module settings already live on the supervisor mount (max_children,
sticky_placement), so hibernate_after moves there too and the use option
goes away: an integer for every module, or a map of Module => ms with an
optional :default for modules not listed. The lookup is by module name, so
it no longer needs the module loaded.
@chrismccord
chrismccord merged commit 17e8a8c into main Sep 30, 2026
2 checks passed
@chrismccord
chrismccord deleted the cm-hibernate-after branch September 30, 2026 15:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant