erlang hot reloading post
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
- Code server keeps two versions
- Local calls within an old version of the genserver route to the old version (via VSN)
- New calls route in the newer version
- Need to be very careful about suspending a process
- But you can also track processes, and loop over them to reload one by one
code_changeitself takes callbacks, so you can write arbitrary migration code- You can do this without even removing dependencies, such as TCP socket open
- TCP socket demo