عجفت الغور

erlang hot reloading post

drafts, erlang

Hot reloading in the BEAM VM

  • Counter v1
-module(counter).
-behaviour(gen_server).
-vsn("1.0.0").
-export([start_link/0, get_count/0, increment/0]).
-export([init/1, handle_call/3, handle_cast/2, code_change/3, terminate/2, handle_info/2]).

%% -- API --
start_link() ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

get_count() ->
    gen_server:call(?MODULE, get_count).

increment() ->
    gen_server:cast(?MODULE, increment).

%% -- Callbacks --
init([]) ->
    {ok, 0}.

handle_cast(increment, Count) ->
    {noreply, Count + 1}.

handle_call(get_count, _From, Count) ->
    {reply, Count, Count}.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

handle_info(_Info, State) -> {noreply, State}.
terminate(_Reason, _State) -> ok.
  • Counter v2

-module(counter). -behaviour(gen_server). -vsn(“2.0.0”). -export([start_link/0, get_count/0, increment/0, get_info/0]). -export([init/1, handle_call/3, handle_cast/2, code_change/3, terminate/2, handle_info/2]).

%% State map -record(state, { count = 0, increment_count = 0, last_updated }).

%% – API – start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

get_count() -> gen_server:call(?MODULE, get_count).

increment() -> gen_server:cast(?MODULE, increment).

get_info() -> sys:get_state(?MODULE).

%% – Callbacks –

init([]) -> {ok, #state{}}.

handle_cast(increment, State) -> {noreply, State#state{count = State#state.count + 1, increment_count = 1, last_updated = erlang:timestamp() }}.

handle_call(get_count, _From, State) -> {reply, State#state.count, State}.

code_change(“1.0.0”, OldState, _Extra) when is_integer(OldState) -> NewState = #state{ count = OldState, increment_count = OldState, last_updated = erlang:timestamp()}, {ok, NewState};

code_change(_OldVsn, State, _Extra) -> {ok, State}.

handle_info(_Info, State) -> {noreply, State}. terminate(_Reason, _State) -> ok.

  • Running
peixian@Mac ~/c/erl-hot-reloading> erl
Erlang/OTP 27 [erts-15.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]

Eshell V15.2 (press Ctrl+G to abort, type help(). for help)
1> file:copy("counter_v1.erl", "counter.erl"), c(counter).
{ok,counter}
2> counter:start_link().
{ok,<0.95.0>}
3> counter:increment().
ok
4> counter:increment().
ok
5> counter:get_count().
2
6> file:copy("counter_v2.erl", "counter.erl"), c(counter).
{ok,counter}
7> counter:get_info().
2
8> sys:change_code(counter, counter, "1.0.0", []).
{error,{unknown_system_msg,{change_code,counter,"1.0.0",
                                        []}}}
9> sys:suspend(counter).
ok
10> sys:change_code(counter, counter, "1.0.0", []).
ok
11> sys:resume(counter).
ok
12> counter:get_info().
{state,2,2,{1769,908677,996152}}
13>
  • Basically
    1. Code server keeps two versions
    2. Local calls within an old version of the genserver route to the old version (via VSN)
    3. New calls route in the newer version
    4. Need to be very careful about suspending a process
    5. But you can also track processes, and loop over them to reload one by one
    6. code_change itself takes callbacks, so you can write arbitrary migration code
    7. You can do this without even removing dependencies, such as TCP socket open
    8. TCP socket demo