├── rebar.lock ├── .gitignore ├── .travis.yml ├── src ├── pobox.app.src ├── samples │ └── pobox_queue_buf.erl ├── pobox_buf.erl └── pobox.erl ├── rebar.config ├── LICENSE.txt ├── test ├── pobox_state_changer.erl ├── pobox_give_away.erl ├── pobox_give_away_SUITE.erl ├── prop_pobox.erl ├── pobox_heir_SUITE.erl └── pobox_SUITE.erl └── README.md /rebar.lock: -------------------------------------------------------------------------------- 1 | []. 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ebin/*.beam 2 | erl_crash.dump 3 | *.swp 4 | deps/ 5 | *~ 6 | ebin/*.app 7 | .eunit/ 8 | .#* 9 | doc/*.html 10 | doc/erlang.png 11 | doc/stylesheet.css 12 | doc/edoc-info 13 | logs/ 14 | test/*.beam 15 | .DS_Store 16 | _build/ 17 | .rebar3 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | otp_release: 3 | - 21.1 4 | - 20.3 5 | - 19.3 6 | # - 18.3 # NOTE: We know gen_statem requires Erlang 19+ 7 | # - 17.3 8 | # - R16B03 9 | # - R15B03 10 | script: 11 | - rebar3 do ct -c --readable=false, proper -c -n 1000, cover -v 12 | -------------------------------------------------------------------------------- /src/pobox.app.src: -------------------------------------------------------------------------------- 1 | {application, pobox, [ 2 | {description, "External buffer processes to protect against mailbox overflow"}, 3 | {vsn, "1.2.0"}, 4 | {applications, [stdlib, kernel]}, 5 | {registered, []}, 6 | {modules, [pobox]}, 7 | 8 | {maintainers, ["Fred Hebert"]}, 9 | {licenses, ["MIT"]}, 10 | {links, [{"Github", "https://github.com/ferd/pobox/"}]} 11 | ]}. 12 | -------------------------------------------------------------------------------- /src/samples/pobox_queue_buf.erl: -------------------------------------------------------------------------------- 1 | -module(pobox_queue_buf). 2 | 3 | -behaviour(pobox_buf). 4 | %% API 5 | -export([new/0, push/2, pop/1, drop/2, push_drop/2]). 6 | 7 | new() -> 8 | queue:new(). 9 | 10 | push(Msg, Q) -> 11 | queue:in(Msg, Q). 12 | 13 | pop(Q) -> 14 | queue:out(Q). 15 | 16 | drop(N, Q) -> 17 | element(2, queue:split(N, Q)). 18 | 19 | push_drop(Msg, Q) -> 20 | push(Msg, drop(1, Q)). 21 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [ 2 | {platform_define, "^[0-9]+", namespaced_types} 3 | ]}. 4 | 5 | {profiles, [ 6 | {test, [{erl_opts, [nowarn_export_all]}]} 7 | ]}. 8 | 9 | %% the plugin itself 10 | {project_plugins, [rebar3_proper]}. 11 | %% The PropEr dependency is required to compile the test cases 12 | %% and will be used to run the tests as well. 13 | {profiles, 14 | [{test, [ 15 | {deps, [ 16 | %% hex 17 | {proper, "1.3.0"} 18 | ]} 19 | ]} 20 | ]}. -------------------------------------------------------------------------------- /src/pobox_buf.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @copyright Eric des Courtis 3 | %% @author Eric des Courtis 4 | %% @doc Generic message buffer behaviour. For more 5 | %% information, see README.txt 6 | %% @end 7 | %%%------------------------------------------------------------------- 8 | -module(pobox_buf). 9 | 10 | %% Behaviour API 11 | -callback new() -> Buf :: any(). 12 | -callback push(Msg :: any(), Buf :: any()) -> Buf :: any(). 13 | -callback pop(Buf :: any()) -> {empty, Buf :: any()} | {{value, Msg :: any()}, Buf :: any()}. 14 | -callback drop(N :: pos_integer(), Buf :: any()) -> Buf ::any(). 15 | -callback push_drop(Msg :: any(), Buf :: any()) -> Buf :: any(). 16 | -optional_callbacks([push_drop/2]). 17 | 18 | 19 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Heroku 2 | 3 | 4 | Permission is hereby granted, free of charge, to any person 5 | obtaining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without 7 | restriction, including without limitation the rights to use, 8 | copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /test/pobox_state_changer.erl: -------------------------------------------------------------------------------- 1 | -module(pobox_state_changer). 2 | 3 | -behaviour(gen_server). 4 | 5 | %% API 6 | -export([start_link/4, get_pobox/1, stop/1]). 7 | 8 | %% gen_server callbacks 9 | -export([init/1, 10 | handle_call/3, 11 | handle_cast/2, 12 | handle_info/2, 13 | terminate/2, 14 | code_change/3]). 15 | 16 | -define(SERVER, ?MODULE). 17 | 18 | -record(state, {pobox, behaviours, crash_vector, vector}). 19 | 20 | start_link(Heir, Behaviours, CrashVector, BufType) -> 21 | gen_server:start_link(?MODULE, [Heir, Behaviours, CrashVector, BufType], []). 22 | 23 | get_pobox(Pid) -> 24 | gen_server:call(Pid, get_pobox). 25 | 26 | stop(Pid) -> 27 | gen_server:call(Pid, stop). 28 | 29 | init([Heir, Behaviours, CrashVector, BufType]) -> 30 | %erlang:process_flag(error_handler, undefined), 31 | {ok, Box} = pobox:start_link(#{max => 10, heir=>Heir, type => BufType}), 32 | {ok, #state{pobox=Box, behaviours=Behaviours, crash_vector=CrashVector, vector=0}}. 33 | 34 | handle_call(stop, From, State) -> 35 | gen_server:reply(From, ok), 36 | {stop, normal, State}; 37 | handle_call(get_pobox, _From, State = #state{pobox=Box}) -> 38 | {reply, Box, State}; 39 | handle_call(_Request, _From, State) -> 40 | {reply, ok, State}. 41 | 42 | 43 | handle_cast(_Request, State) -> 44 | {noreply, State}. 45 | 46 | handle_info(_, State = #state{crash_vector=Vector, vector=Vector}) -> 47 | {stop, fake_crash, State}; 48 | handle_info(_, State = #state{behaviours=[]}) -> 49 | {stop, normal, State}; 50 | handle_info(_, State = #state{vector=Vector, behaviours = [Behaviour | Behaviours]}) -> 51 | do_behaviour(Behaviour, State), 52 | {noreply, State#state{vector=Vector + 1, behaviours = Behaviours}}; 53 | handle_info(_Info, State) -> 54 | {noreply, State}. 55 | 56 | terminate(_Reason, _State) -> 57 | ok. 58 | 59 | code_change(_OldVsn, State, _Extra) -> 60 | {ok, State}. 61 | 62 | do_behaviour({wait, Time}, State) -> 63 | timer:sleep(Time), 64 | State; 65 | do_behaviour(notify, State = #state{pobox=Box}) -> 66 | pobox:notify(Box), 67 | State; 68 | do_behaviour({active, all}, State = #state{pobox=Box}) -> 69 | pobox:active(Box, fun(Msg, nostate) -> {{ok, Msg}, nostate} end, nostate), 70 | State; 71 | do_behaviour({active, Batch}, State = #state{pobox=Box}) -> 72 | pobox:active( 73 | Box, 74 | fun 75 | (_Msg, Count) when Count =:= Batch -> 76 | skip; 77 | (Msg, Count) -> 78 | {{ok, Msg}, Count + 1} 79 | end, 80 | 0 81 | ), 82 | State. 83 | -------------------------------------------------------------------------------- /test/pobox_give_away.erl: -------------------------------------------------------------------------------- 1 | -module(pobox_give_away). 2 | 3 | -behaviour(gen_server). 4 | 5 | %% API 6 | -export([start_link/4, get_pobox/1, give_away/1]). 7 | 8 | %% gen_server callbacks 9 | -export([init/1, 10 | handle_call/3, 11 | handle_cast/2, 12 | handle_info/2, 13 | terminate/2, 14 | code_change/3]). 15 | 16 | -define(SERVER, ?MODULE). 17 | 18 | -record(state, {pobox, behaviours, give_away_vector, vector, target}). 19 | 20 | start_link(Target, Behaviours, GiveAwayVector, BufType) -> 21 | gen_server:start_link(?MODULE, [Target, Behaviours, GiveAwayVector, BufType], []). 22 | 23 | get_pobox(Box) -> 24 | gen_server:call(Box, get_pobox). 25 | 26 | give_away(Box) -> 27 | gen_server:cast(Box, give_away). 28 | 29 | init([Target, Behaviours, GiveAwayVector, BufType]) -> 30 | %erlang:process_flag(error_handler, undefined), 31 | {ok, Box} = pobox:start_link(#{max => 10, type => BufType}), 32 | {ok, #state{pobox=Box, behaviours=Behaviours, give_away_vector=GiveAwayVector, vector=0, target=Target}}. 33 | 34 | 35 | handle_call(get_pobox, _From, State=#state{pobox=Box}) -> 36 | {reply, Box, State}; 37 | handle_call(_Request, _From, State) -> 38 | {reply, ok, State}. 39 | 40 | handle_cast(give_away, State=#state{pobox=Box, target=Target}) -> 41 | true = pobox:give_away(Box, Target, 5000), 42 | {stop, normal, State}; 43 | handle_cast(_Request, State) -> 44 | {noreply, State}. 45 | 46 | handle_info(_, State = #state{give_away_vector=Vector, vector=Vector, pobox=Box, target=Target}) -> 47 | true = pobox:give_away(Box, Target, 5000), 48 | {stop, normal, State}; 49 | handle_info(_, State = #state{behaviours=[], pobox=Box, target=Target}) -> 50 | true = pobox:give_away(Box, Target, 5000), 51 | {stop, normal, State}; 52 | handle_info(_, State = #state{vector=Vector, behaviours=[Behaviour | Behaviours]}) -> 53 | do_behaviour(Behaviour, State), 54 | {noreply, State#state{vector=Vector + 1, behaviours=Behaviours}}; 55 | handle_info(_Info, State) -> 56 | {noreply, State}. 57 | 58 | terminate(_Reason, _State) -> 59 | ok. 60 | 61 | code_change(_OldVsn, State, _Extra) -> 62 | {ok, State}. 63 | 64 | do_behaviour({wait, Time}, State) -> 65 | timer:sleep(Time), 66 | State; 67 | do_behaviour(notify, State = #state{pobox=Box}) -> 68 | pobox:notify(Box), 69 | State; 70 | do_behaviour({active, all}, State = #state{pobox=Box}) -> 71 | pobox:active(Box, fun(Msg, nostate) -> {{ok, Msg}, nostate} end, nostate), 72 | State; 73 | do_behaviour({active, Batch}, State = #state{pobox=Box}) -> 74 | pobox:active( 75 | Box, 76 | fun 77 | (_Msg, Count) when Count =:= Batch -> 78 | skip; 79 | (Msg, Count) -> 80 | {{ok, Msg}, Count + 1} 81 | end, 82 | 0 83 | ), 84 | State. 85 | -------------------------------------------------------------------------------- /test/pobox_give_away_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(pobox_give_away_SUITE). 2 | -include_lib("common_test/include/ct.hrl"). 3 | -include_lib("stdlib/include/assert.hrl"). 4 | -include_lib("eunit/include/eunit.hrl"). 5 | -compile(export_all). 6 | 7 | all() -> [ 8 | owner_gives_away_ownership, 9 | owner_gives_away_ownership_with_data, 10 | owner_gives_away_ownership_to_dead_process, 11 | foreign_process_tries_to_gives_away_ownership, 12 | give_away_does_not_change_heir, 13 | owner_gives_away_ownership_when_in_notify, 14 | owner_gives_away_ownership_when_in_active 15 | ]. 16 | 17 | owner_gives_away_ownership(_Config) -> 18 | {ok, Box} = pobox:start_link(#{max => 10}), 19 | Self = self(), 20 | H = erlang:spawn_link(fun F () -> receive X -> Self ! X end, F() end), 21 | ?assert(pobox:give_away(Box, H, 5000)), 22 | ?assertMatch( 23 | {pobox_transfer, Box, Self, undefined, give_away}, 24 | receive 25 | Res -> Res 26 | after 27 | 5000 -> timeout 28 | end 29 | ). 30 | 31 | owner_gives_away_ownership_with_data(_Config) -> 32 | {ok, Box} = pobox:start_link([{max, 10}]), 33 | Self = self(), 34 | H = erlang:spawn_link(fun F () -> receive X -> Self ! X end, F() end), 35 | Ref = make_ref(), 36 | ?assert(pobox:give_away(Box, H, Ref, 5000)), 37 | ?assertMatch( 38 | {pobox_transfer, Box, Self, Ref, give_away}, 39 | receive 40 | Res -> Res 41 | after 42 | 5000 -> timeout 43 | end 44 | ). 45 | 46 | owner_gives_away_ownership_to_dead_process(_Config) -> 47 | {ok, Box} = pobox:start_link([{max, 10}]), 48 | Self = self(), 49 | H = erlang:spawn(fun F () -> receive X -> Self ! X end, F() end), 50 | erlang:exit(H, kill), 51 | timer:sleep(100), 52 | ?assertNot(pobox:give_away(Box, H, 5000)), 53 | ?assert(receive 54 | _ -> false 55 | after 56 | 100 -> true 57 | end). 58 | 59 | 60 | foreign_process_tries_to_gives_away_ownership(_Config) -> 61 | {ok, Box} = pobox:start_link([{max, 10}]), 62 | Self = self(), 63 | erlang:spawn_link(fun() -> Self ! pobox:give_away(Box, self(), 5000) end), 64 | ?assertNot(receive Res -> Res end). 65 | 66 | give_away_does_not_change_heir(_Config) -> 67 | Self = self(), 68 | {ok, Box} = pobox:start_link([{heir, Self}, {heir_data, first}, {max, 10}]), 69 | H = erlang:spawn(fun() -> receive X -> Self ! X end, throw("crash") end), 70 | ?assert(pobox:give_away(Box, H, second, 5000)), 71 | ?assertMatch( 72 | {pobox_transfer, Box, Self, second, give_away}, 73 | receive 74 | Res -> Res 75 | after 76 | 5000 -> timeout 77 | end 78 | ), 79 | ?assertMatch( 80 | {pobox_transfer, Box, H, first, Reason} when Reason =/= give_away, 81 | receive 82 | Res -> Res 83 | after 84 | 5000 -> timeout 85 | end 86 | ). 87 | 88 | owner_gives_away_ownership_when_in_notify(_Config) -> 89 | {ok, Box} = pobox:start_link([{max, 10}]), 90 | ok = pobox:notify(Box), 91 | Self = self(), 92 | H = erlang:spawn_link(fun F () -> receive X -> Self ! X end, F() end), 93 | ?assertMatch(notify, element(1, sys:get_state(Box))), 94 | ?assert(pobox:give_away(Box, H, 5000)), 95 | ?assertMatch( 96 | {pobox_transfer, Box, Self, undefined, give_away}, 97 | receive 98 | Res -> Res 99 | after 100 | 5000 -> timeout 101 | end 102 | ), 103 | ?assert( 104 | receive 105 | _ -> false 106 | after 107 | 100 -> true 108 | end 109 | ), 110 | ?assertMatch(passive, element(1, sys:get_state(Box))). 111 | 112 | owner_gives_away_ownership_when_in_active(_Config) -> 113 | {ok, Box} = pobox:start_link([{max, 10}]), 114 | ok = pobox:active(Box, fun(_Msg, _) -> skip end, nostate), 115 | Self = self(), 116 | H = erlang:spawn_link(fun F () -> receive X -> Self ! X end, F() end), 117 | ?assertMatch(active_s, element(1, sys:get_state(Box))), 118 | ?assert(pobox:give_away(Box, H, 5000)), 119 | ?assertMatch( 120 | {pobox_transfer, Box, Self, undefined, give_away}, 121 | receive 122 | Res -> Res 123 | after 124 | 5000 -> timeout 125 | end 126 | ), 127 | ?assert( 128 | receive 129 | _ -> false 130 | after 131 | 100 -> true 132 | end 133 | ), 134 | ?assertMatch(passive, element(1, sys:get_state(Box))). -------------------------------------------------------------------------------- /test/prop_pobox.erl: -------------------------------------------------------------------------------- 1 | -module(prop_pobox). 2 | -include_lib("proper/include/proper.hrl"). 3 | -export([ 4 | prop_we_always_go_to_passive_mode_after_an_automatic_transfer/0, 5 | prop_we_always_go_to_passive_mode_after_a_give_away_transfer/0, 6 | prop_will_discard_after_max/0 7 | ]). 8 | 9 | -type pobox_max() :: pos_integer(). 10 | -type pobox_initial_state() :: passive | notify. 11 | -type crash_vector() :: pos_integer(). 12 | -type give_away_vector() :: pos_integer(). 13 | -type message() :: {msg, binary()} | {resize, pos_integer()} | usage. 14 | -type messaging_strategy() :: async | sync | info. 15 | -type messaging_behaviour() :: {non_neg_integer(), messaging_strategy(), message()}. 16 | -type automatic_behaviour() :: {wait, non_neg_integer()} | notify | {active, non_neg_integer() | all}. 17 | -type give_away_behaviour() :: {wait, non_neg_integer()} | notify | {active, non_neg_integer() | all}. 18 | -type buffer_type() :: queue | keep_old | stack | {mod, pobox_queue_buf}. 19 | -type automatic_scenario() :: {crash_vector(), list(automatic_behaviour()), list(messaging_behaviour()), buffer_type()}. 20 | -type give_away_scenario() :: {give_away_vector(), list(give_away_behaviour()), list(messaging_behaviour()), buffer_type()}. 21 | -type max_scenario() :: {pobox_max(), buffer_type(), pobox_initial_state()}. 22 | 23 | prop_we_always_go_to_passive_mode_after_an_automatic_transfer() -> 24 | %% Random message sequence 25 | %% Random behaviour sequence 26 | %% Crash at random time vector 27 | %% pobox should be in passive mode 28 | erlang:process_flag(trap_exit, true), 29 | ?FORALL(_Scenario = {CrashVector, Behaviours, MessagingBehaviours, BufType}, automatic_scenario(), begin 30 | {ok, SCPid} = pobox_state_changer:start_link(self(), Behaviours, CrashVector, BufType), 31 | Box = pobox_state_changer:get_pobox(SCPid), 32 | %io:format("~p~n", [Scenario]), 33 | Procs = [erlang:spawn_opt(node(), fun() -> do_messaging_behaviour(Box, MessagingBehaviour) end, [monitor]) 34 | || MessagingBehaviour <- MessagingBehaviours 35 | ], 36 | lists:map( 37 | fun({Pid, Ref}) -> 38 | receive 39 | {'DOWN', Ref, _Type, Pid, _Info} -> ok 40 | after 41 | 20000 -> ok 42 | end 43 | end, 44 | Procs 45 | ), 46 | case is_process_alive(SCPid) of 47 | true -> catch pobox_state_changer:stop(SCPid); 48 | false -> ok 49 | end, 50 | receive 51 | {pobox_transfer, Box, SCPid, undefined, _Reason} -> 52 | element(1, sys:get_state(Box)) =:= passive 53 | after 54 | 20000 -> false 55 | end 56 | 57 | end). 58 | 59 | 60 | prop_we_always_go_to_passive_mode_after_a_give_away_transfer() -> 61 | ?FORALL(_Scenario = {GiveAwayVector, Behaviours, MessagingBehaviours, BufType}, give_away_scenario(), begin 62 | {ok, GAPid} = pobox_give_away:start_link(self(), Behaviours, GiveAwayVector, BufType), 63 | Box = pobox_give_away:get_pobox(GAPid), 64 | Procs = [erlang:spawn_opt(node(), fun() -> catch do_messaging_behaviour(Box, MessagingBehaviour) end, [monitor]) 65 | || MessagingBehaviour <- MessagingBehaviours 66 | ], 67 | lists:map( 68 | fun({Pid, Ref}) -> 69 | receive 70 | {'DOWN', Ref, _Type, Pid, _Info} -> ok 71 | after 72 | 20000 -> ok 73 | end 74 | end, 75 | Procs 76 | ), 77 | pobox_give_away:give_away(GAPid), 78 | receive 79 | {pobox_transfer, Box, _, undefined, _} -> 80 | element(1, sys:get_state(Box)) =:= passive 81 | after 82 | 20000 -> false 83 | end 84 | end). 85 | 86 | 87 | prop_will_discard_after_max() -> 88 | ?FORALL({MaxSize, BufType, InitialState}, max_scenario(), begin 89 | {ok, Box} = pobox:start_link(#{max => MaxSize, type => BufType, initial_state => InitialState}), 90 | length(lists:filter( 91 | fun(full) -> true; (ok) -> false end, 92 | [pobox:post_sync(Box, N, 5000) || N <- lists:seq(1, MaxSize * 2)] 93 | )) =:= MaxSize 94 | end). 95 | 96 | do_messaging_behaviour(Box, {Time, _, {resize, MaxSize}}) -> 97 | timer:sleep(Time), 98 | pobox:resize(Box, MaxSize); 99 | do_messaging_behaviour(Box, {Time, _, usage}) -> 100 | timer:sleep(Time), 101 | pobox:usage(Box); 102 | do_messaging_behaviour(Box, {Time, async, Msg={msg, _}}) -> 103 | timer:sleep(Time), 104 | pobox:post(Box, Msg); 105 | do_messaging_behaviour(Box, {Time, sync, Msg={msg, _}}) -> 106 | timer:sleep(Time), 107 | pobox:post_sync(Box, Msg); 108 | do_messaging_behaviour(Box, {Time, info, Msg={msg, _}}) -> 109 | timer:sleep(Time), 110 | Box ! {post, Msg}. 111 | 112 | -------------------------------------------------------------------------------- /test/pobox_heir_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(pobox_heir_SUITE). 2 | -include_lib("common_test/include/ct.hrl"). 3 | -include_lib("stdlib/include/assert.hrl"). 4 | -include_lib("eunit/include/eunit.hrl"). 5 | -compile(export_all). 6 | 7 | all() -> [ 8 | with_local_registered_name_owner, 9 | with_global_registered_name_owner, 10 | with_global_registered_name_heir, 11 | with_via_registered_name_owner, 12 | owner_crashes_heir_takes_over, 13 | heir_dies_then_owner_dies, 14 | goes_to_passive_mode_when_in_notify, 15 | goes_to_passive_mode_when_in_active 16 | ]. 17 | 18 | 19 | with_local_registered_name_owner(_Config) -> 20 | Self = self(), 21 | Ref = make_ref(), 22 | PreviousOwnerPid = erlang:spawn(fun() -> 23 | {ok, Box} = pobox:start_link({local, ?MODULE}, #{heir => Self, heir_data => Ref, max => 10}), 24 | Self ! Box, 25 | throw("crash") 26 | end), 27 | Box = receive 28 | BoxPid when is_pid(BoxPid) -> BoxPid 29 | after 30 | 100 -> false 31 | end, 32 | ?assertMatch( 33 | {pobox_transfer, Box, PreviousOwnerPid, Ref, _}, 34 | receive 35 | Res -> Res 36 | after 37 | 5000 -> timeout 38 | end 39 | ). 40 | 41 | 42 | with_global_registered_name_owner(_Config) -> 43 | Self = self(), 44 | Ref = make_ref(), 45 | PreviousOwnerPid = erlang:spawn(fun() -> 46 | {ok, Box} = pobox:start_link({global, ?MODULE}, [{heir, Self}, {heir_data, Ref}, {max, 10}]), 47 | Self ! Box, 48 | throw("crash") 49 | end), 50 | Box = receive 51 | BoxPid when is_pid(BoxPid) -> BoxPid 52 | after 53 | 100 -> false 54 | end, 55 | ?assertMatch( 56 | {pobox_transfer, Box, PreviousOwnerPid, Ref, _}, 57 | receive 58 | Res -> Res 59 | after 60 | 5000 -> timeout 61 | end 62 | ). 63 | 64 | 65 | with_global_registered_name_heir(_Config) -> 66 | Self = self(), 67 | Ref = make_ref(), 68 | yes = global:register_name(my_global_name, self()), 69 | PreviousOwnerPid = erlang:spawn(fun() -> 70 | {ok, Box} = pobox:start_link([{heir, {global, my_global_name}}, {heir_data, Ref}, {max, 10}]), 71 | Self ! Box, 72 | throw("crash") 73 | end), 74 | Box = receive 75 | BoxPid when is_pid(BoxPid) -> BoxPid 76 | after 77 | 100 -> false 78 | end, 79 | ?assertMatch( 80 | {pobox_transfer, Box, PreviousOwnerPid, Ref, _}, 81 | receive 82 | Res -> Res 83 | after 84 | 5000 -> timeout 85 | end 86 | ). 87 | 88 | with_via_registered_name_owner(_Config) -> 89 | Self = self(), 90 | Ref = make_ref(), 91 | PreviousOwnerPid = erlang:spawn(fun() -> 92 | {ok, Box} = pobox:start_link({via, global, fake_name}, [{heir, Self}, {heir_data, Ref}, {max, 10}]), 93 | Self ! Box, 94 | throw("crash") 95 | end), 96 | Box = receive 97 | BoxPid when is_pid(BoxPid) -> BoxPid 98 | after 99 | 100 -> false 100 | end, 101 | ?assertMatch( 102 | {pobox_transfer, Box, PreviousOwnerPid, Ref, _}, 103 | receive 104 | Res -> Res 105 | after 106 | 5000 -> timeout 107 | end 108 | ). 109 | 110 | 111 | owner_crashes_heir_takes_over(_Config) -> 112 | Self = self(), 113 | Ref = make_ref(), 114 | PreviousOwnerPid = erlang:spawn(fun() -> 115 | {ok, Box} = pobox:start_link([{heir, Self}, {heir_data, Ref}, {max, 10}]), 116 | Self ! Box, 117 | throw("crash") 118 | end), 119 | Box = receive 120 | BoxPid when is_pid(BoxPid) -> BoxPid 121 | after 122 | 100 -> false 123 | end, 124 | ?assertMatch( 125 | {pobox_transfer, Box, PreviousOwnerPid, Ref, _}, 126 | receive 127 | Res -> Res 128 | after 129 | 5000 -> timeout 130 | end 131 | ). 132 | 133 | heir_dies_then_owner_dies(_Config) -> 134 | Self = self(), 135 | Dest = erlang:spawn_link(fun() -> receive _ -> Self ! continue end end), 136 | {ok, Box} = pobox:start_link([{heir, Self}, {heir_data, heir_data}, {max, 10}]), 137 | ?assert(pobox:give_away(Box, Dest, dest_data, 500)), 138 | ?assertMatch(continue, receive 139 | Res -> Res 140 | after 141 | 5000 -> timeout 142 | end), 143 | ?assertMatch( 144 | {pobox_transfer, Box, Dest, heir_data, Reason} when Reason =/= give_away, 145 | receive 146 | Res -> Res 147 | after 148 | 5000 -> timeout 149 | end 150 | ). 151 | 152 | goes_to_passive_mode_when_in_notify(_Config) -> 153 | Self = self(), 154 | Ref = make_ref(), 155 | PreviousOwnerPid = erlang:spawn(fun() -> 156 | {ok, Box} = pobox:start_link(#{heir => Self, heir_data => Ref, max => 10}), 157 | ok = pobox:notify(Box), 158 | Self ! Box, 159 | throw("crash") 160 | end), 161 | Box = receive 162 | BoxPid when is_pid(BoxPid) -> BoxPid 163 | after 164 | 100 -> false 165 | end, 166 | ?assertMatch( 167 | {pobox_transfer, Box, PreviousOwnerPid, Ref, _}, 168 | receive 169 | Res -> Res 170 | after 171 | 5000 -> timeout 172 | end 173 | ), 174 | ?assert( 175 | receive 176 | _ -> false 177 | after 178 | 100 -> true 179 | end 180 | ), 181 | ?assertMatch(passive, element(1, sys:get_state(Box))). 182 | 183 | goes_to_passive_mode_when_in_active(_Config) -> 184 | Self = self(), 185 | Ref = make_ref(), 186 | PreviousOwnerPid = erlang:spawn(fun() -> 187 | {ok, Box} = pobox:start_link([{heir, Self}, {heir_data, Ref}, {max, 10}]), 188 | ok = pobox:active(Box, fun(_Msg, _) -> skip end, nostate), 189 | Self ! Box, 190 | timer:sleep(100), 191 | throw("crash") 192 | end), 193 | Box = receive 194 | BoxPid when is_pid(BoxPid) -> BoxPid 195 | after 196 | 100 -> false 197 | end, 198 | ?assertMatch(active_s, element(1, sys:get_state(Box))), 199 | ?assertMatch( 200 | {pobox_transfer, Box, PreviousOwnerPid, Ref, _}, 201 | receive 202 | Res -> Res 203 | after 204 | 5000 -> timeout 205 | end 206 | ), 207 | ?assert( 208 | receive 209 | _ -> false 210 | after 211 | 100 -> true 212 | end 213 | ), 214 | ?assertMatch(passive, element(1, sys:get_state(Box))). 215 | 216 | 217 | 218 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.com/ferd/pobox.svg?branch=master)](https://travis-ci.com/ferd/pobox) 2 | 3 | # PO Box 4 | 5 | High throughput Erlang applications often get bitten by the fact that 6 | Erlang mailboxes are unbounded and will keep accepting messages until the 7 | node runs out of memory. 8 | 9 | In most cases, this problem can be solved by imposing a rate limit on 10 | the producers, and it is recommended to explore this idea before looking 11 | at this library. 12 | 13 | When it is impossible to rate-limit the messages coming to a process and 14 | that your optimization efforts remain fruitless, you need to start 15 | shedding load by dropping messages. 16 | 17 | PO Box can help by shedding the load for you, and making sure you won't 18 | run out of memory. 19 | 20 | ## The Principles 21 | 22 | PO Box is a library that implements a buffer process. Erlang processes 23 | will receive their messages locally (at home), and may become overloaded 24 | because they have to both deal with their mailbox and day-to-day tasks: 25 | 26 | messages 27 | | 28 | V 29 | +-----[Pid or Name]-----+ 30 | | | | | 31 | | | mailbox | | 32 | | +---------+ | 33 | | | | 34 | | receive | 35 | +-----------------------+ 36 | 37 | A PO Box process will be where you will ask for your messages to go 38 | through. The PO Box process will implement a buffer (see *Types of 39 | Buffer* for details) that will do nothing but churn through messages and 40 | drop them when the buffer is full for you. 41 | 42 | Depending on how you use the API, the PO Box can tell you it received new data, 43 | so you can then ask for the data, or you can tell it to send the data to you 44 | directly, without notification: 45 | 46 | messages 47 | | 48 | V 49 | +---------[Pid]---------+ +--------[POBox]--------+ 50 | | |<-- got mail ---| | | | 51 | | | | | mailbox | | 52 | | |--- send it! -->| +---------+ | 53 | | | | | | 54 | | |<-----|<---buffer | 55 | +-----------------------+ +-----------------------+ 56 | 57 | To be more detailed, a PO Box is a state machine with an owner process 58 | (which it receives messages for), and it has 3 states: 59 | 60 | - Active 61 | - Notify 62 | - Passive 63 | 64 | The passive state basically does nothing but accumulate messages in the 65 | buffer and drop them when necessary. 66 | 67 | The notify state is enabled by the user by calling the PO Box. Its sole 68 | task is to verify if there is any message in the buffer. If there is, it 69 | will respond to the PO Box's owner with a `{mail, BoxPid, new_data}` message 70 | sent directly to the pid. If there is no message in the buffer, the 71 | process will wait in the notify state until it gets one. As soon as the 72 | notification is sent, it reverts back to the passive state. 73 | 74 | The active state is the only one that can send actual messages to the 75 | owner process. The user can call the PO Box to set it active, and if 76 | there are any messages in the buffer, all the messages it contains get 77 | sent as a list to the owner. If there are no messages, the PO Box waits 78 | until there is one to send it. After forwarding the messages, the PO Box 79 | reverts to the passive state. 80 | 81 | The FSM can be illustrated as crappy ASCII as: 82 | 83 | ,---->[passive]------(user makes active)----->[active] 84 | | | ^ | ^ | 85 | | | '---(sends message to user)--<-----' | | 86 | | (user makes notify) | | 87 | | | | | 88 | (user is notified) | | | 89 | | V | | 90 | '-----[notify]---------(user makes active)--------' | 91 | ^----------(user makes notify)<----------' 92 | 93 | ## Types of buffer 94 | 95 | Currently, there are three types of built-in buffers supported: queues 96 | and stacks, and `keep_old` queues. You can also provide your own 97 | buffer implementation using the `pobox_buf` behaviour. See 98 | `samples/pobox_queue_buf.erl` for an example implementation. 99 | 100 | Queues will keep messages in order, and drop oldest messages to make 101 | place for new ones. If you have a buffer of size 3 and receive messages 102 | a, b, c, d, e in that order, the buffer will contain messages `[c,d,e]`. 103 | 104 | `keep_old` queues will keep messages in order, but block newer messages 105 | from entering, favoring keeping old messages instead. If you have a 106 | buffer of size 3 and receive messages a, b, c, d, e in that order, the 107 | buffer will contain messages `[a,b,c]`. 108 | 109 | Stacks will not guarantee any message ordering, and will drop the top of 110 | the stack to make place for the new messages first. for the same 111 | messages, the stack buffer should contain the messages `[e,b,a]`. 112 | 113 | To choose between a queue and a stack buffer, you should consider the 114 | following criterias: 115 | 116 | - Do you need messages in order? Choose one of the queues. 117 | - Do you need the latest messages coming in to be kept, or the oldest 118 | ones? If so, pick `queue` and `keep_old`, respectively. 119 | - Do you need low latency? Then choose a stack. Stacks will give you 120 | many messages with low latency with a few with high latency. Queues 121 | will give you a higher overall latency, but less variance over time. 122 | 123 | More buffer types could be supported in the future, if people require 124 | them. 125 | 126 | ## How to build it 127 | 128 | ./rebar compile 129 | 130 | ## How to run tests 131 | 132 | ./rebar compile ct 133 | 134 | ## How to use it 135 | 136 | Start a buffer with any of the following: 137 | 138 | start_link(OwnerPid, MaxSize, BufferType) 139 | start_link(OwnerPid, MaxSize, BufferType, InitialState) 140 | start_link(Name, OwnerPid, MaxSize, BufferType) 141 | start_link(Name, OwnerPid, MaxSize, BufferType, InitialState) 142 | start_link(#{ 143 | name => Name, 144 | owner => OwnerPid, 145 | max => MaxSize, %% mandatory 146 | type => BufferType, 147 | initial_state => InitialState, 148 | heir => HeirPid, 149 | heir_data => HeirData 150 | }) 151 | start_link(Name, #{ 152 | owner => OwnerPid, 153 | max => MaxSize, %% mandatory 154 | type => BufferType, 155 | initial_state => InitialState, 156 | heir => Heir, 157 | heir_data => HeirData 158 | }) 159 | Where: 160 | 161 | - `Name` is any name a regular `gen_fsm` process can accept (including 162 | `{via,...}` tuples) 163 | - `OwnerPid` is the pid of the PO Box owner. It's the only one that can 164 | communicate with it in terms of setting state and reading messages. 165 | The `OwnerPid` can be either a pid or an atom. The PO Box will set up 166 | a link directly between itself and `OwnerPid`, and won't trap exits. 167 | If you're using named processes (atoms) and want to have the PO Box 168 | survive them individually, you should unlink the processes manually. 169 | This also means that processes that terminate normally won't kill the 170 | POBox. 171 | - `MaxSize` is the maximum number of messages in a buffer. Note that this 172 | is a mandatory property. 173 | - `BufferType` can be either `queue`, `stack` or `keep_old` and specifies 174 | which type is going to be used. You can also provide your buffer module 175 | using `{mod, Module}`. 176 | - `InitialState` can be either `passive` or `notify`. The default value 177 | is set to `notify`. Having the buffer passive is desirable when you 178 | start it during an asynchronous `init` and do not want to receive 179 | notifications right away. 180 | - `Heir` The name or pid of a process that will take over the PO Box if 181 | the owner dies. You can use a local registered name such as an atom or 182 | you can also use `{global, Name}` and `{via, Module, Name}` if you wish. 183 | If the Heir is a name the name is resolved as soon as the Owner dies. 184 | A message will be sent to the heir to notify it of the transfer and the 185 | PO Box will be put into passive state. The format of this message should 186 | look like `{pobox_transfer, BoxPid, PreviousOwnerPid, HeirData, Reason}`. 187 | - `HeirData` is data that should be sent as part of the pobox_transfer 188 | message to the heir when the owner dies. 189 | 190 | The buffer can be made active by calling: 191 | 192 | pobox:active(BoxPid, FilterFun, FilterState) 193 | 194 | The `FilterFun` is a function that will take messages one by one along 195 | with custom state and can return: 196 | 197 | - `{{ok, Message}, NewState}`: the message will be sent. 198 | - `{drop, NewState}`: the message will be dropped. 199 | - `skip`: the message is left in the buffer and whatever was filtered so 200 | far gets sent. 201 | 202 | A function that would blindly forward all messages could be written as: 203 | 204 | fun(Msg, _) -> {{ok,Msg},nostate} end 205 | 206 | A function that would limit binary messages by size could be written as: 207 | 208 | fun(Msg, Allowed) -> 209 | case Allowed - byte_size(Msg) of 210 | N when N < 0 -> skip; 211 | N -> {{ok, Msg}, N} 212 | end 213 | end 214 | 215 | Or you could drop messages that are empty binaries by doing: 216 | 217 | fun(<<>>, State) -> {drop, State}; 218 | (Msg, State) -> {{ok,Msg}, State} 219 | end. 220 | 221 | The resulting message sent will be: 222 | 223 | {mail, BoxPid, Messages, MessageCount, MessageDropCount} 224 | 225 | Finally, the PO Box can be forced to notify by calling: 226 | 227 | pobox:notify(BoxPid) 228 | 229 | Which is objectively much simpler. 230 | 231 | Messages can be sent to a PO Box by calling `pobox:post(BoxPid, Msg)` or 232 | sending a message directly to the process as `BoxPid ! {post, Msg}`. 233 | 234 | The ownership of the PO Box can be transfered to another process by calling: 235 | 236 | pobox:give_away(BoxPid, DestPid, DestData, Timeout) 237 | 238 | or 239 | 240 | pobox:give_away(BoxPid, DestPid, Timeout) 241 | 242 | which is equivalent to: 243 | 244 | pobox_give_away(BoxPid, DestPid, undefined, Timeout) 245 | 246 | The call should return `true` on success and `false` on failure. Note that 247 | you can only call this from within the owner process otherwise the call always fails. 248 | If `DestData` is not provided it will be sent as `undefined` in the `pobox_transfer` 249 | message. 250 | 251 | The destination process should receive a message of the following form: 252 | 253 | {pobox_transfer, BoxPid, PreviousOwnerPid, DestData | undefined, give_away} 254 | 255 | ## Example Session 256 | 257 | First start a PO Box for the current process: 258 | 259 | 1> {ok, Box} = pobox:start_link(self(), 10, queue). 260 | {ok,<0.39.0>} 261 | 262 | We'll also define a spammer function that will just keep mailing a bunch 263 | of messages: 264 | 265 | 2> Spam = fun(F,N) -> pobox:post(Box,N), F(F,N+1) end. 266 | #Fun 267 | 268 | Because we're in the shell, the function takes itself as an argument so 269 | it can both remain anonymous and loop. Each message is an increasing 270 | integer. 271 | 272 | I can start the process and wait for a while: 273 | 274 | 3> Spammer = spawn(fun() -> Spam(Spam,0) end). 275 | <0.42.0> 276 | 277 | Let's see if we have anything in our PO box: 278 | 279 | 4> flush(). 280 | Shell got {mail, <0.39.0>, new_data} 281 | ok 282 | 283 | Yes! Let's get that content: 284 | 285 | 5> pobox:active(Box, fun(X,ok) -> {{ok,X},ok} end, ok). 286 | ok 287 | 6> flush(). 288 | Shell got {mail,<0.39.0>, 289 | [778918,778919,778920,778921,778922,778923,778924,778925, 290 | 778926,778927], 291 | 10,778918} 292 | ok 293 | 294 | So we have 10 messages with seqential IDs (we used a queue buffer), and 295 | the process kindly dropped over 700,000 messages for us, keeping our 296 | node's memory safe. 297 | 298 | The spammer is still going and our PO Box is in passive mode. Let's cut 299 | to the chase and go directly to the active state: 300 | 301 | 7> pobox:active(Box, fun(X,ok) -> {{ok,X},ok} end, ok). 302 | ok 303 | 8> flush(). 304 | Shell got {mail,<0.39.0>, 305 | [1026883,1026884,1026885,1026886,1026887,1026888,1026889, 306 | 1026890,1026891,1026892], 307 | 10,247955} 308 | ok 309 | 310 | Nice. We can go back to notification mode too: 311 | 312 | 9> pobox:notify(Box). 313 | ok 314 | 10> flush(). 315 | Shell got {mail, <0.39.0>, new_data} 316 | ok 317 | 318 | And keep going on and on and on. 319 | 320 | ## Notes 321 | 322 | - Be careful to have a lightweight filter function if you expect constant 323 | overload from messages that keep coming very very fast. While the 324 | buffer filters out whatever messages you have, the new ones keep 325 | accumulating in the PO Box's own mailbox! 326 | - It is possible for a process to have multiple PO Boxes, although 327 | coordinating the multiple state machines together may get tricky. 328 | - The library is a generalization of ideas designed and implemented in 329 | logplex by Geoff Cant's (@archaelus). Props to him. 330 | - Using a `keep_old` buffer with a filter function that selects one message 331 | at a time would be equivalent to a naive bounded mailbox similar to what 332 | plenty of users asked for before. Tricking the filter function to 333 | forward the message (`self() ! Msg`) while dropping it will allow 334 | to do selective receives on bounded mailboxes. 335 | - When using `post_sync/3` keep in mind that full doesn't mean your message 336 | will be dropped unless you are using the `keep_old` buffer type or 337 | a custom buffer that behaves the same way as `keep_old`. 338 | 339 | ## Contributing 340 | 341 | Accepted contributions need to be non-aesthetic, and provide some new 342 | functionality, fix abstractions, improve performance or semantics, and 343 | so on. 344 | 345 | All changes received must be tested and not break existing tests. 346 | 347 | Changes to currently untested functionality should ideally first provide 348 | a separate commit that shows the current behaviour working with the new 349 | tests (or some of the new tests, if you expand on the functionality), 350 | and then your own feature (and additional tests if required) in its own 351 | commit so we can verify nothing breaks in unpredictable ways. 352 | 353 | Tests are written using Common Test. PropEr tests will be accepted, 354 | because they objectively rule. Ideally, you will wrap your PropEr tests 355 | in a Common Test suite so we can run everything with one command. 356 | 357 | If you need help, feel free to ask for it in issues or pull requests. 358 | These rules are strict, but we're nice people! 359 | 360 | ## Roadmap 361 | 362 | This is more a wishlist than a roadmap, in no particular order: 363 | 364 | - Provide default filter functions in a new module 365 | 366 | ## Changelog 367 | - 1.2.0: added heir and `give_away` functionality / fixed `keep_old` buffer size tracking 368 | - 1.1.0: added `pobox_buf` behaviour to add custom buffer implementations 369 | - 1.0.4: move to gen\_statem implementation to avoid OTP 21 compile errors and OTP 20 warnings 370 | - 1.0.3: fix typespecs to generate fewer errors 371 | - 1.0.2: explicitly specify `registered` to be `[]` for 372 | relx compatibility, switch to rebar3 373 | - 1.0.1: fixing bug where manually dropped messages (with the active filter) 374 | would result in wrong size values and crashes for queues. 375 | - 1.0.0: A PO Box links itself to the process that it receives data for. 376 | - 0.2.0: Added PO Box's pid in the `newdata` message so a process can own more 377 | than a PO Box. Changed internal queue and stack size monitoring to be 378 | O(1) in all cases. 379 | - 0.1.1: adding `keep_old` queue, which blocks new messages from entering 380 | a filled queue. 381 | - 0.1.0: initial commit 382 | 383 | ## Authors / Thanks 384 | 385 | - Fred Hebert / @ferd: library generalization and current implementation 386 | - Geoff Cant / @archaelus: design, original implementation 387 | - Jean-Samuel Bédard / @jsbed: adaptation to gen\_statem behaviour 388 | - Eric des Courtis / @edescourtis: added `pobox_buf` behaviour & heir/give\_away functionality 389 | -------------------------------------------------------------------------------- /test/pobox_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(pobox_SUITE). 2 | -include_lib("common_test/include/ct.hrl"). 3 | -compile(export_all). 4 | 5 | all() -> [{group, queue}, {group, stack}, {group, keep_old}, {group, pobox_queue_buf}]. 6 | groups() -> 7 | %% Nested groups for eternal glory. We use one group to declare all the 8 | %% tests ('all' group), and then nest it in 'stack' and 'queue' groups, 9 | %% which all repeat the 'all' test but with a different implementation. 10 | [{queue, [], [{group, all}]}, 11 | {stack, [], [{group, all}]}, 12 | {keep_old, [], [{group, all}]}, 13 | {pobox_queue_buf, [], [{group, all}]}, 14 | {all, [], [notify_to_active, notify_to_overflow, no_api_post, 15 | filter_skip, filter_drop, active_to_notify, 16 | passive_to_notify, passive_to_active, resize, 17 | linked_owner, drop_count, post_sync, post_sync_active]} 18 | ]. 19 | 20 | %%%%%%%%%%%%%% 21 | %%% MACROS %%% 22 | %%%%%%%%%%%%%% 23 | -define(wait_msg(PAT), 24 | (fun() -> 25 | receive 26 | PAT -> ok 27 | after 2000 -> 28 | error({wait_too_long}) 29 | end 30 | end)()). 31 | 32 | -define(wait_msg(PAT, RET), 33 | (fun() -> 34 | receive 35 | PAT -> RET 36 | after 2000 -> 37 | error({wait_too_long}) 38 | end 39 | end)()). 40 | 41 | %%%%%%%%%%%%%%%%%%%%%%% 42 | %%% INIT & TEARDOWN %%% 43 | %%%%%%%%%%%%%%%%%%%%%%% 44 | 45 | init_per_suite(Config) -> Config. 46 | end_per_suite(_Config) -> ok. 47 | 48 | init_per_group(queue, Config) -> 49 | [{type, queue} | Config]; 50 | init_per_group(stack, Config) -> 51 | [{type, stack} | Config]; 52 | init_per_group(keep_old, Config) -> 53 | [{type, keep_old} | Config]; 54 | init_per_group(pobox_queue_buf, Config) -> 55 | [{type, {mod, pobox_queue_buf}} | Config]; 56 | init_per_group(_, Config) -> 57 | Config. 58 | 59 | end_per_group(_, _Config) -> 60 | ok. 61 | 62 | init_per_testcase(linked_owner, Config) -> 63 | [{size, 3} | Config]; 64 | init_per_testcase(_, Config) -> 65 | Type = ?config(type, Config), 66 | Size = 3, 67 | {ok, Pid} = pobox:start_link(self(), Size, Type), 68 | [{pobox, Pid}, {size, Size} | Config]. 69 | 70 | end_per_testcase(linked_owner, _Config) -> 71 | ok; 72 | end_per_testcase(_, Config) -> 73 | Pid = ?config(pobox, Config), 74 | unlink(Pid), 75 | Ref = erlang:monitor(process, Pid), 76 | exit(Pid, shutdown), 77 | ?wait_msg({'DOWN', Ref, process, Pid, _}). 78 | 79 | %%%%%%%%%%%%% 80 | %%% TESTS %%% 81 | %%%%%%%%%%%%% 82 | notify_to_active(Config) -> 83 | %% Check that we can send messages to the POBox and it will notify us 84 | %% about it. We should then be able to set it to active and it should 85 | %% send us the messages back. 86 | Box = ?config(pobox, Config), 87 | Size = ?config(size, Config), 88 | Sent = lists:seq(1,Size), 89 | [pobox:post(Box, N) || N <- Sent], 90 | ?wait_msg({mail, Box, new_data}), 91 | pobox:active(Box, fun(X,State) -> {{ok,X}, State} end, no_state), 92 | Msgs = ?wait_msg({mail,Box,Msgs,Size,0}, Msgs), 93 | %% Based on the type, we have different guarantees 94 | case ?config(type, Config) of 95 | queue -> % queues are in order 96 | Sent = Msgs; 97 | {mod, pobox_queue_buf} -> % queues are in order 98 | Sent = Msgs; 99 | stack -> % We don't care for the order 100 | Sent = lists:sort(Msgs); 101 | keep_old -> % in order, no benefit for out-of-order 102 | Sent = Msgs 103 | end, 104 | %% messages are not repeated, and we get good state transitions 105 | Msg = Size+1, 106 | pobox:post(Box, Msg), 107 | pobox:active(Box, fun(X,State) -> {{ok,X}, State} end, no_state), 108 | ?wait_msg({mail,Box,[Msg],1,0}). 109 | 110 | notify_to_overflow(Config) -> 111 | %% Check that we can send messages to the POBox and it will notify 112 | %% us about it, but also overflow and tell us about how many messages 113 | %% overflowed. 114 | Box = ?config(pobox, Config), 115 | Size = ?config(size, Config), 116 | [pobox:post(Box, N) || N <- lists:seq(1,Size*2)], 117 | ?wait_msg({mail, Box, new_data}), 118 | pobox:active(Box, fun(X,State) -> {{ok,X}, State} end, no_state), 119 | Msgs = ?wait_msg({mail,Box,Msgs,Size,Size}, Msgs), 120 | %% Based on the type, we have different guarantees 121 | case ?config(type, Config) of 122 | queue -> % queues are in order. We expect to have lost the 1st msgs 123 | Msgs = lists:seq(Size+1, Size*2); % we dropped 1..Size 124 | {mod, pobox_queue_buf} -> % queues are in order. We expect to have lost the 1st msgs 125 | Msgs = lists:seq(Size+1, Size*2); % we dropped 1..Size 126 | stack -> % We don't care for the order. We have all oldest + 1 newest 127 | Kept = lists:sort([Size*2 | lists:seq(1,Size-1)]), 128 | Kept = lists:sort(Msgs); 129 | keep_old -> % In order. We expect to have lost the last messages 130 | Msgs = lists:seq(1,Size) 131 | end. 132 | 133 | no_api_post(Config) -> 134 | %% We want to support the ability to post directly without going through 135 | %% the API. This can be done by sending a message directly with the 136 | %% form {post, Msg}, for each message. 137 | Box = ?config(pobox, Config), 138 | Size = ?config(size, Config), 139 | Sent = lists:seq(1,Size), 140 | [Box ! {post,N} || N <- Sent], 141 | ?wait_msg({mail, Box, new_data}), 142 | pobox:active(Box, fun(X,State) -> {{ok,X}, State} end, no_state), 143 | Sent = lists:sort(?wait_msg({mail,Box,Msgs,Size,0}, Msgs)). 144 | 145 | filter_skip(Config) -> 146 | %% The custom function to deal with the buffer can be used to 147 | %% skip entries. We skip after one entry to make a message-per-message 148 | %% fetch. 149 | Box = ?config(pobox, Config), 150 | Size = ?config(size, Config), 151 | true = Size >= 3, % The test will fail with less than 3 messages 152 | [pobox:post(Box, N) || N <- lists:seq(1,3)], 153 | ?wait_msg({mail, Box, new_data}), 154 | Filter = fun(X,0) -> {{ok,X}, 1}; 155 | (_,_) -> skip 156 | end, 157 | pobox:active(Box, Filter, 0), 158 | [Msg1] = lists:sort(?wait_msg({mail,Box,Msgs,1,0}, Msgs)), 159 | pobox:active(Box, Filter, 0), 160 | [Msg2] = lists:sort(?wait_msg({mail,Box,Msgs,1,0}, Msgs)), 161 | pobox:active(Box, Filter, 0), 162 | [Msg3] = lists:sort(?wait_msg({mail,Box,Msgs,1,0}, Msgs)), 163 | case ?config(type, Config) of 164 | queue -> 165 | [1,2,3] = [Msg1, Msg2, Msg3]; 166 | {mod, pobox_queue_buf} -> 167 | [1,2,3] = [Msg1, Msg2, Msg3]; 168 | stack -> 169 | [3,2,1] = [Msg1, Msg2, Msg3]; 170 | keep_old -> 171 | [1,2,3] = [Msg1, Msg2, Msg3] 172 | end. 173 | 174 | filter_drop(Config) -> 175 | %% The custom function to deal with the buffer can be used to 176 | %% drop entries. We drop after one entry to make a message-per-message 177 | %% fetch that keeps no history. 178 | Box = ?config(pobox, Config), 179 | Size = ?config(size, Config), 180 | true = Size >= 3, % The test will fail with less than 3 messages 181 | [pobox:post(Box,N) || N <- lists:seq(1,3)], 182 | ?wait_msg({mail, Box, new_data}), 183 | Filter = fun(X,0) -> {{ok,X}, 1}; 184 | (_,_) -> {drop, 1} 185 | end, 186 | pobox:active(Box, Filter, 0), 187 | MsgList = lists:sort(?wait_msg({mail,Box,Msgs,1,2}, Msgs)), 188 | pobox:post(Box, 4), 189 | pobox:active(Box, Filter, 0), 190 | [MsgExtra] = lists:sort(?wait_msg({mail,Box,Msgs,1,0}, Msgs)), 191 | case ?config(type, Config) of 192 | queue -> 193 | [1,4] = MsgList ++ [MsgExtra]; 194 | {mod, pobox_queue_buf} -> 195 | [1,4] = MsgList ++ [MsgExtra]; 196 | stack -> 197 | [3,4] = MsgList ++ [MsgExtra]; 198 | keep_old -> 199 | [1,4] = MsgList ++ [MsgExtra] 200 | end. 201 | 202 | active_to_notify(Config) -> 203 | %% It should be possible to take an active box, and make it go to notify 204 | %% to receive notifications instead of data 205 | Box = ?config(pobox, Config), 206 | pobox:active(Box, fun(X,State) -> {{ok,X}, State} end, no_state), 207 | pobox:post(Box, 1), 208 | ?wait_msg({mail,Box,[1],1,0}), 209 | {_, 0} = process_info(self(), message_queue_len), % no 'new_data' 210 | %% We should be in passive mode. 211 | passive = get_statename(Box), 212 | pobox:active(Box, fun(X,State) -> {{ok,X}, State} end, no_state), 213 | wait_until(fun() -> active_s =:= get_statename(Box) end, 100, 10), 214 | pobox:notify(Box), 215 | wait_until(fun() -> notify =:= get_statename(Box) end, 100, 10), 216 | pobox:post(Box, 2), 217 | ?wait_msg({mail, Box, new_data}), 218 | pobox:active(Box, fun(X,State) -> {{ok,X}, State} end, no_state), 219 | ?wait_msg({mail,Box,[2],1,0}). 220 | 221 | passive_to_notify(Config) -> 222 | %% It should be possible to take a passive box, and make it 'notify' 223 | %% to receive notifications. 224 | Box = ?config(pobox, Config), 225 | pobox:active(Box, fun(X,State) -> {{ok,X}, State} end, no_state), 226 | pobox:post(Box, 1), 227 | ?wait_msg({mail,Box,[1],1,0}), 228 | {_, 0} = process_info(self(), message_queue_len), % no 'new_data' 229 | %% We should be in passive mode. 230 | passive = get_statename(Box), 231 | pobox:notify(Box), 232 | wait_until(fun() -> notify =:= get_statename(Box) end, 100, 10), 233 | %% Then we should be receiving notifications after a message 234 | pobox:post(Box, 2), 235 | ?wait_msg({mail, Box, new_data}), 236 | pobox:active(Box, fun(X,State) -> {{ok,X}, State} end, no_state), 237 | ?wait_msg({mail,Box,[2],1,0}), 238 | %% Back to passive. With a message in the mailbox, we should be able 239 | %% to notify then fall back to passive directly. 240 | passive = get_statename(Box), 241 | pobox:post(Box, 3), 242 | {_, 0} = process_info(self(), message_queue_len), % no 'new_data' 243 | pobox:notify(Box), 244 | ?wait_msg({mail, Box, new_data}), 245 | passive = get_statename(Box). 246 | 247 | passive_to_active(Config) -> 248 | %% It should be possible to take a passive box, and make it active 249 | %% to receive messages without notifications. 250 | Box = ?config(pobox, Config), 251 | Filter = fun(X,State) -> {{ok,X},State} end, 252 | pobox:active(Box, fun(X,State) -> {{ok,X}, State} end, no_state), 253 | pobox:post(Box, 1), 254 | ?wait_msg({mail,Box,[1],1,0}), 255 | {_, 0} = process_info(self(), message_queue_len), % no 'new_data' 256 | %% We should be in passive mode. 257 | passive = get_statename(Box), 258 | pobox:active(Box, Filter, no_state), 259 | wait_until(fun() -> active_s =:= get_statename(Box) end, 100, 10), 260 | %% Then we should be receiving mail after a post 261 | pobox:post(Box, 2), 262 | ?wait_msg({mail,Box,[2],1,0}), 263 | {_, 0} = process_info(self(), message_queue_len), % no 'new_data' 264 | %% Back to passive. With a message in the mailbox, we should be able 265 | %% to activate then fall back to passive directly. 266 | passive = get_statename(Box), 267 | pobox:post(Box, 3), 268 | {_, 0} = process_info(self(), message_queue_len), % no 'new_data' 269 | pobox:active(Box, Filter, no_state), 270 | ?wait_msg({mail,Box,[3],1,0}), 271 | passive = get_statename(Box). 272 | 273 | resize(Config) -> 274 | %% We should be able to resize the buffer up and down freely. 275 | Box = ?config(pobox, Config), 276 | Filter = fun(X,State) -> {{ok,X},State} end, 277 | pobox:resize(Box, 3), 278 | [pobox:post(Box, N) || N <- lists:seq(1,4)], 279 | ?wait_msg({mail, Box, new_data}), % POBox is full 280 | pobox:active(Box, Filter, no_state), 281 | ?wait_msg({mail,Box,_,3,1}), % then box is empty 282 | [pobox:post(Box, N) || N <- lists:seq(1,3)], 283 | pobox:resize(Box, 6), 284 | [pobox:post(Box, N) || N <- lists:seq(4,6)], 285 | pobox:active(Box, Filter, no_state), 286 | ?wait_msg({mail,Box,_,6,0}), % lost nothing 287 | [pobox:post(Box, N) || N <- lists:seq(1,6)], 288 | pobox:resize(Box, 3), 289 | pobox:active(Box, Filter, no_state), 290 | Kept = ?wait_msg({mail,Box,Msgs,3,3}, Msgs), % lost the surplus 291 | case ?config(type, Config) of 292 | queue -> 293 | Kept = [4,5,6]; 294 | {mod, pobox_queue_buf} -> 295 | Kept = [4,5,6]; 296 | stack -> 297 | Kept = [3,2,1]; 298 | keep_old -> 299 | Kept = [1,2,3] 300 | end. 301 | 302 | linked_owner(Config) -> 303 | Type = ?config(type, Config), 304 | Size = ?config(size, Config), 305 | Trap = process_flag(trap_exit, true), 306 | %% Can link to a regular process and catch the failure 307 | Owner0 = spawn_link(fun() -> timer:sleep(infinity) end), 308 | {ok,Box0} = pobox:start_link(Owner0, Size, Type), 309 | wait_until(fun() -> 2 =:= length(element(2, process_info(Box0, links))) end, 100, 10), 310 | wait_until(fun() -> 2 =:= length(element(2, process_info(Owner0, links))) end, 100, 10), 311 | exit(Owner0, shutdown), 312 | ?wait_msg({'EXIT', Owner0, _}), 313 | ?wait_msg({'EXIT', Box0, _}), 314 | %% Same thing works with named processes 315 | Owner1 = spawn_link(fun() -> timer:sleep(infinity) end), 316 | erlang:register(pobox_owner, Owner1), 317 | {ok,Box1} = pobox:start_link(pobox_owner, Size, Type), 318 | wait_until(fun() -> 2 =:= length(element(2, process_info(Box1, links))) end, 100, 10), 319 | wait_until(fun() -> 2 =:= length(element(2, process_info(Owner1, links))) end, 100, 10), 320 | exit(Owner1, shutdown), 321 | ?wait_msg({'EXIT', Owner1, _}), 322 | ?wait_msg({'EXIT', Box1, _}), 323 | %% Unlinking totally makes things work with multiple procs though 324 | Owner2 = spawn_link(fun() -> 325 | receive {From, pobox, Pid} -> 326 | unlink(Pid), 327 | From ! ok, 328 | timer:sleep(infinity) 329 | end 330 | end), 331 | erlang:register(pobox_owner, Owner2), 332 | {ok,Box2} = pobox:start_link(pobox_owner, Size, Type), 333 | wait_until(fun() -> 2 =:= length(element(2, process_info(Box2, links))) end, 100, 10), 334 | wait_until(fun() -> 2 =:= length(element(2, process_info(Owner2, links))) end, 100, 10), 335 | Owner2 ! {self(), pobox, Box2}, 336 | ?wait_msg(ok), 337 | exit(Owner2, shutdown), 338 | ?wait_msg({'EXIT', Owner2, _}), 339 | true = is_process_alive(Box2), 340 | exit(Box2, different_reason), 341 | ?wait_msg({'EXIT', Box2, different_reason}), 342 | process_flag(trap_exit, Trap). 343 | 344 | drop_count(Config) -> 345 | Type = ?config(type, Config), 346 | Size = ?config(size, Config), 347 | {ok, Box} = pobox:start_link(self(), Size, Type), 348 | %% post and manually drop 3 messages, should be able to do it again 349 | %% many times without a problem 350 | [pobox:post(Box, N) || N <- lists:seq(1,Size)], 351 | pobox:active(Box, fun(_, S) -> {drop, S} end, nostate), 352 | ?wait_msg({mail, Box, [], 0, Size}), 353 | [pobox:post(Box, N) || N <- lists:seq(1,Size)], 354 | pobox:active(Box, fun(_, S) -> {drop, S} end, nostate), 355 | ?wait_msg({mail, Box, [], 0, Size}), 356 | [pobox:post(Box, N) || N <- lists:seq(1,Size)], 357 | pobox:active(Box, fun(_, S) -> {drop, S} end, nostate), 358 | ?wait_msg({mail, Box, [], 0, Size}), 359 | [pobox:post(Box, N) || N <- lists:seq(1,Size)], 360 | pobox:active(Box, fun(_, S) -> {drop, S} end, nostate), 361 | ?wait_msg({mail, Box, [], 0, Size}), 362 | [pobox:post(Box, N) || N <- lists:seq(1,Size)], 363 | pobox:active(Box, fun(_, S) -> {drop, S} end, nostate), 364 | ?wait_msg({mail, Box, [], 0, Size}), 365 | true = is_process_alive(Box), 366 | unlink(Box), 367 | exit(Box, shutdown). 368 | 369 | post_sync(Config) -> 370 | Type = ?config(type, Config), 371 | Size = ?config(size, Config), 372 | {ok, Box} = pobox:start_link(self(), Size, Type), 373 | [full, ok | _] = lists:reverse( 374 | [pobox:post_sync(Box, N, 5000) || N <- lists:seq(1,Size + 1)] 375 | ), 376 | true = is_process_alive(Box), 377 | unlink(Box), 378 | exit(Box, shutdown). 379 | 380 | post_sync_active(Config) -> 381 | Type = ?config(type, Config), 382 | Size = ?config(size, Config), 383 | {ok, Box} = pobox:start_link(self(), Size, Type), 384 | pobox:active(Box, fun(_, _) -> skip end, nostate), 385 | [full, ok | _] = lists:reverse( 386 | [pobox:post_sync(Box, N, 5000) || N <- lists:seq(1,Size + 1)] 387 | ), 388 | true = is_process_alive(Box), 389 | unlink(Box), 390 | exit(Box, shutdown). 391 | 392 | 393 | %%%%%%%%%%%%%%% 394 | %%% HELPERS %%% 395 | %%%%%%%%%%%%%%% 396 | get_statename(Pid) -> 397 | {State, _} = sys:get_state(Pid), 398 | State. 399 | 400 | wait_until(Fun, _, 0) -> error({timeout, Fun}); 401 | wait_until(Fun, Interval, Tries) -> 402 | case Fun() of 403 | true -> ok; 404 | false -> 405 | timer:sleep(Interval), 406 | wait_until(Fun, Interval, Tries-1) 407 | end. 408 | 409 | -------------------------------------------------------------------------------- /src/pobox.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @copyright Fred Hebert, Geoff Cant 3 | %% @author Fred Hebert 4 | %% @author Geoff Cant 5 | %% @author Eric des Courtis 6 | %% @doc Generic process that acts as an external mailbox and a 7 | %% message buffer that will drop requests as required. For more 8 | %% information, see README.txt 9 | %% @end 10 | %%%------------------------------------------------------------------- 11 | -module(pobox). 12 | -behaviour(gen_statem). 13 | -compile({no_auto_import,[size/1]}). 14 | 15 | -ifdef(namespaced_types). 16 | -record(buf, {type = undefined :: undefined | stack | queue | keep_old | {mod, module()}, 17 | max = undefined :: undefined | max(), 18 | size = 0 :: non_neg_integer(), 19 | drop = 0 :: drop(), 20 | data = undefined :: undefined | queue:queue() | list()}). 21 | -else. 22 | -record(buf, {type = undefined :: undefined | stack | queue | keep_old | {mod, module()}, 23 | max = undefined :: undefined | max(), 24 | size = 0 :: non_neg_integer(), 25 | drop = 0 :: drop(), 26 | data = undefined :: undefined | queue() | list()}). 27 | -endif. 28 | 29 | -define( 30 | PROCESS_NAME_GUARD_VIA_OR_GLOBAL(V), 31 | ((tuple_size(V) == 2 andalso element(1, V) == global) orelse 32 | (tuple_size(V) == 3 andalso element(1, V) == via)) 33 | ). 34 | 35 | -define( 36 | PROCESS_NAME_GUARD_NO_PID(V), 37 | is_atom(V) orelse ?PROCESS_NAME_GUARD_VIA_OR_GLOBAL(V) 38 | ). 39 | 40 | -define( 41 | PROCESS_NAME_GUARD(V), 42 | is_pid(V) orelse ?PROCESS_NAME_GUARD_NO_PID(V) 43 | ). 44 | 45 | -define( 46 | PROCESS_NAME_GUARD_WITH_LOCAL_NO_PID(V), 47 | is_atom(V) orelse ( 48 | (tuple_size(V) == 2 andalso element(1, V) == local) orelse ?PROCESS_NAME_GUARD_VIA_OR_GLOBAL(V)) 49 | ). 50 | 51 | -define(POBOX_START_STATE_GUARD(V), V =:= notify orelse V =:= passive). 52 | -define(POBOX_BUFFER_TYPE_GUARD(V), V =:= queue orelse V =:= stack orelse V =:= keep_old orelse 53 | (tuple_size(V) == 2 andalso element(1, V) =:= mod andalso is_atom(element(2, V))) 54 | ). 55 | 56 | -type max() :: pos_integer(). 57 | -type drop() :: non_neg_integer(). 58 | -type buffer() :: #buf{}. 59 | -type filter() :: fun( (Msg::term(), State::term()) -> 60 | {{ok,NewMsg::term()} | drop , State::term()} | skip). 61 | 62 | -type in() :: {'post', Msg::term()}. 63 | -type note() :: {'mail', Self::pid(), new_data}. 64 | -type mail() :: {'mail', Self::pid(), Msgs::list(), 65 | Count::non_neg_integer(), Lost::drop()}. 66 | -type name() :: {local, atom()} | {global, term()} | atom() | pid() | {via, module(), term()}. 67 | 68 | -export_type([max/0, filter/0, in/0, mail/0, note/0]). 69 | 70 | -record(state, {buf :: buffer(), 71 | owner :: name(), 72 | filter :: undefined | filter(), 73 | filter_state :: undefined | term(), 74 | owner_pid :: pid(), 75 | owner_monitor_ref :: undefined | reference(), 76 | heir :: undefined | pid() | atom(), 77 | heir_data :: undefined | term(), 78 | name :: name()}). 79 | 80 | -record(pobox_opts, {name :: undefined | name(), 81 | owner = self() :: name(), 82 | max :: undefined | max(), 83 | type = queue :: stack | queue | keep_old | {mod, module()}, 84 | initial_state = notify :: notify | passive, 85 | heir :: undefined | name(), 86 | heir_data :: undefined | term()}). 87 | 88 | -export([start_link/1, start_link/2, start_link/3, start_link/4, start_link/5, 89 | resize/2, resize/3, usage/1, usage/2, active/3, notify/1, post/2, 90 | post_sync/2, post_sync/3, give_away/3, give_away/4]). 91 | -export([init/1, 92 | active_s/3, passive/3, notify/3, 93 | callback_mode/0, terminate/3, code_change/4]). 94 | 95 | 96 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 97 | %%% API Function Definitions %%% 98 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 99 | 100 | %% @doc Starts a new buffer process. The implementation can either 101 | %% be a stack or a queue, depending on which messages will be dropped 102 | %% (older ones or newer ones). Note that stack buffers do not guarantee 103 | %% message ordering. 104 | %% The initial state can be either passive or notify, depending on whether 105 | %% the user wants to get notifications of new messages as soon as possible. 106 | -spec start_link(name(), max(), stack | queue | keep_old | {mod, module()}) -> {ok, pid()}. 107 | start_link(Owner, MaxSize, Type) when ?PROCESS_NAME_GUARD(Owner), is_integer(MaxSize), MaxSize > 0 -> 108 | start_link(Owner, MaxSize, Type, notify). 109 | 110 | %% This one is messy because we have two clauses with 4 values, so we look them 111 | %% up based on guards. 112 | -spec start_link(name(), max(), stack | queue | keep_old | {mod, module()}, notify | passive) -> {ok, pid()}. 113 | start_link(Owner, MaxSize, Type, StateName) when ?PROCESS_NAME_GUARD(Owner), 114 | ?POBOX_START_STATE_GUARD(StateName), 115 | is_integer(MaxSize), MaxSize > 0 -> 116 | gen_statem:start_link(?MODULE, #pobox_opts{owner=Owner, max = MaxSize, type=Type, initial_state=StateName}, []); 117 | start_link(Name, Owner, MaxSize, Type) 118 | when MaxSize > 0, 119 | ?PROCESS_NAME_GUARD_WITH_LOCAL_NO_PID(Name), 120 | ?PROCESS_NAME_GUARD(Owner), 121 | ?POBOX_BUFFER_TYPE_GUARD(Type) -> 122 | start_link(Name, Owner, MaxSize, Type, notify). 123 | 124 | -spec start_link(name(), name(), max(), stack | queue | keep_old | {mod, module()}, 125 | 'notify'|'passive') -> {ok, pid()}. 126 | start_link(Name, Owner, MaxSize, Type, StateName) 127 | when MaxSize > 0, 128 | ?PROCESS_NAME_GUARD_WITH_LOCAL_NO_PID(Name), 129 | ?PROCESS_NAME_GUARD(Owner), 130 | ?POBOX_BUFFER_TYPE_GUARD(Type), 131 | ?POBOX_START_STATE_GUARD(StateName) -> 132 | gen_statem:start_link(Name, ?MODULE, #pobox_opts{ 133 | name = Name, 134 | owner = Owner, 135 | max = MaxSize, 136 | type = Type, 137 | initial_state = StateName 138 | }, []). 139 | 140 | default_opts() -> 141 | #pobox_opts{owner=self(), initial_state=notify, type=queue}. 142 | 143 | -spec(start_link(map() | list()) -> {ok, pid()}). 144 | start_link(Opts) when is_list(Opts) -> 145 | case validate_opts(proplist_to_pobox_opt_with_defaults(Opts)) of 146 | PoBoxOpts = #pobox_opts{name=undefined} -> 147 | gen_statem:start_link(?MODULE, PoBoxOpts, []); 148 | PoBoxOpts = #pobox_opts{name=Name} -> 149 | gen_statem:start_link(Name, ?MODULE, PoBoxOpts, []) 150 | end; 151 | start_link(Opts) when is_map(Opts) -> 152 | start_link(maps:to_list(Opts)). 153 | 154 | -spec(start_link(name(), map() | list()) -> {ok, pid()}). 155 | start_link(Name, Opts) when ?PROCESS_NAME_GUARD_WITH_LOCAL_NO_PID(Name), is_list(Opts) -> 156 | PoBoxOpts = validate_opts(proplist_to_pobox_opt_with_defaults([{name, Name} | Opts])), 157 | gen_statem:start_link(Name, ?MODULE, PoBoxOpts, []); 158 | start_link(Name, Opts) when ?PROCESS_NAME_GUARD_WITH_LOCAL_NO_PID(Name), is_map(Opts) -> 159 | start_link(Name, maps:to_list(Opts)). 160 | 161 | 162 | 163 | %% @doc Allows to take a given buffer, and make it larger or smaller. 164 | %% A buffer can be made larger without overhead, but it may take 165 | %% more work to make it smaller given there could be a 166 | %% need to drop messages that would now be considered overflow. 167 | -spec resize(name(), max()) -> ok. 168 | resize(Box, NewMaxSize) when NewMaxSize > 0 -> 169 | gen_statem:call(Box, {resize, NewMaxSize}). 170 | 171 | %% @doc Allows to take a given buffer, and make it larger or smaller. 172 | %% A buffer can be made larger without overhead, but it may take 173 | %% more work to make it smaller given there could be a 174 | %% need to drop messages that would now be considered overflow. 175 | -spec resize(name(), max(), timeout()) -> ok. 176 | resize(Box, NewMaxSize, Timeout) when NewMaxSize > 0 -> 177 | gen_statem:call(Box, {resize, NewMaxSize}, Timeout). 178 | 179 | %% @doc Get the number of items in the PO Box and the capacity. 180 | -spec usage(name()) -> {non_neg_integer(), pos_integer()}. 181 | usage(Box) -> 182 | gen_statem:call(Box, usage). 183 | 184 | %% @doc Get the number of items in the PO Box and the capacity. 185 | -spec usage(name(), timeout()) -> {non_neg_integer(), pos_integer()}. 186 | usage(Box, Timeout) -> 187 | gen_statem:call(Box, usage, Timeout). 188 | 189 | %% @doc Forces the buffer into an active state where it will 190 | %% send the data it has accumulated. The fun passed needs to have 191 | %% two arguments: A message, and a term for state. The function can return, 192 | %% for each element, a tuple of the form {Res, NewState}, where `Res' can be: 193 | %% - `{ok, Msg}' to receive the message in the block that gets shipped 194 | %% - `drop' to ignore the message 195 | %% - `skip' to stop removing elements from the stack, and keep them for later. 196 | -spec active(name(), filter(), State::term()) -> ok. 197 | active(Box, Fun, FunState) when is_function(Fun,2) -> 198 | gen_statem:cast(Box, {active_s, Fun, FunState}). 199 | 200 | %% @doc Forces the buffer into its notify state, where it will send a single 201 | %% message alerting the Owner of new messages before going back to the passive 202 | %% state. 203 | -spec notify(name()) -> ok. 204 | notify(Box) -> 205 | gen_statem:cast(Box, notify). 206 | 207 | %% @doc Sends a message to the PO Box, to be buffered. 208 | -spec post(name(), term()) -> ok. 209 | post(Box, Msg) -> 210 | gen_statem:cast(Box, {post, Msg}). 211 | 212 | %% @doc Sends a message to the PO Box, to be buffered. But give additional 213 | %% feedback about if PO Box is full. This is very useful when combined 214 | %% with the keep_old buffer type because it tells you the message will 215 | %% be dropped. 216 | -spec post_sync(name(), term()) -> ok | full. 217 | post_sync(Box, Msg) when ?PROCESS_NAME_GUARD(Box) -> 218 | gen_statem:call(Box, {post, Msg}). 219 | 220 | 221 | -spec post_sync(name(), term(), timeout()) -> ok | full. 222 | post_sync(Box, Msg, Timeout) when ?PROCESS_NAME_GUARD(Box) -> 223 | gen_statem:call(Box, {post, Msg}, Timeout). 224 | 225 | %% @doc Give away the PO Box ownership to another process. This will send a message in the following form to Dest: 226 | %% {pobox_transfer, BoxPid :: pid(), PreviousOwnerPid :: pid(), undefined, give_away} 227 | -spec give_away(name(), name(), timeout()) -> boolean(). 228 | give_away(Box, Dest, Timeout) when 229 | ?PROCESS_NAME_GUARD(Box), ?PROCESS_NAME_GUARD(Dest) -> 230 | give_away(Box, Dest, undefined, Timeout). 231 | 232 | %% @doc Give away the PO Box ownership to another process. This will send a message in the following form to Dest: 233 | %% {pobox_transfer, BoxPid :: pid(), PreviousOwnerPid :: pid(), DestData :: term(), give_away} 234 | -spec give_away(name(), name(), term(), timeout()) -> boolean(). 235 | give_away(Box, Dest, DestData, Timeout) when 236 | ?PROCESS_NAME_GUARD(Box), ?PROCESS_NAME_GUARD(Dest) -> 237 | gen_statem:call(Box, {give_away, Dest, DestData, self()}, Timeout). 238 | 239 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 240 | %%% gen_statem Function Definitions %%% 241 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 242 | 243 | callback_mode() -> 244 | state_functions. 245 | 246 | %% @private {Owner, Size, Type, StateName, {heir, Heir, HeirData}} 247 | init(#pobox_opts{ 248 | name = Name0, 249 | owner = Owner, 250 | max = MaxSize, 251 | type = Type, 252 | initial_state = StateName, 253 | heir = Heir, 254 | heir_data = HeirData 255 | }) -> 256 | Name1 = start_link_name_to_name(Name0), 257 | erlang:link(OwnerPid = where(Owner)), 258 | MaybeMonitorRef = case Heir of 259 | undefined -> undefined; 260 | HeirName when ?PROCESS_NAME_GUARD(HeirName) -> 261 | MonitorRef = erlang:monitor(process, OwnerPid), 262 | erlang:unlink(OwnerPid), 263 | MonitorRef 264 | end, 265 | {ok, StateName, #state{ 266 | buf=buf_new(Type, MaxSize), 267 | owner=Owner, 268 | owner_pid=OwnerPid, 269 | owner_monitor_ref=MaybeMonitorRef, 270 | heir=Heir, 271 | heir_data=HeirData, 272 | name=Name1 273 | }}. 274 | 275 | %% @private 276 | active_s(cast, {active_s, Fun, FunState}, S = #state{}) -> 277 | {next_state, active_s, S#state{filter=Fun, filter_state=FunState}}; 278 | active_s(cast, notify, S = #state{}) -> 279 | {next_state, notify, S#state{filter=undefined, filter_state=undefined}}; 280 | active_s(cast, {post, Msg}, S = #state{buf=Buf}) -> 281 | NewBuf = insert(Msg, Buf), 282 | send(S#state{buf=NewBuf}); 283 | active_s(cast, _Msg, _State) -> 284 | %% unexpected 285 | keep_state_and_data; 286 | active_s({call, From}, Msg, Data) -> 287 | handle_call(From, Msg, active_s, Data); 288 | active_s(info, Msg, Data) -> 289 | handle_info(Msg, active_s, Data). 290 | 291 | %% @private 292 | passive(cast, notify, State = #state{buf=Buf}) -> 293 | case size(Buf) of 294 | 0 -> {next_state, notify, State}; 295 | N when N > 0 -> send_notification(State) 296 | end; 297 | passive(cast, {active_s, Fun, FunState}, S = #state{buf=Buf}) -> 298 | NewState = S#state{filter=Fun, filter_state=FunState}, 299 | case size(Buf) of 300 | 0 -> {next_state, active_s, NewState}; 301 | N when N > 0 -> send(NewState) 302 | end; 303 | passive(cast, {post, Msg}, S = #state{buf=Buf}) -> 304 | {next_state, passive, S#state{buf=insert(Msg, Buf)}}; 305 | passive(cast, _Msg, _State) -> 306 | %% unexpected 307 | keep_state_and_data; 308 | passive({call, From}, Msg, Data) -> 309 | handle_call(From, Msg, passive, Data); 310 | passive(info, Msg, Data) -> 311 | handle_info(Msg, passive, Data). 312 | 313 | %% @private 314 | notify(cast, {active_s, Fun, FunState}, S = #state{buf=Buf}) -> 315 | NewState = S#state{filter=Fun, filter_state=FunState}, 316 | case size(Buf) of 317 | 0 -> {next_state, active_s, NewState}; 318 | N when N > 0 -> send(NewState) 319 | end; 320 | notify(cast, notify, S = #state{}) -> 321 | {next_state, notify, S}; 322 | notify(cast, {post, Msg}, S = #state{buf=Buf}) -> 323 | send_notification(S#state{buf=insert(Msg, Buf)}); 324 | notify(cast, _Msg, _State) -> 325 | %% unexpected 326 | keep_state_and_data; 327 | notify({call, From}, Msg, Data) -> 328 | handle_call(From, Msg, notify, Data); 329 | notify(info, Msg, Data) -> 330 | handle_info(Msg, notify, Data). 331 | 332 | 333 | %% @private 334 | handle_call(From, {post, Msg}, StateName, S=#state{buf=#buf{max=Size, size=Size}}) -> 335 | gen_statem:reply(From, full), 336 | ?MODULE:StateName(cast, {post, Msg}, S); 337 | handle_call(From, {post, Msg}, StateName, S) -> 338 | gen_statem:reply(From, ok), 339 | ?MODULE:StateName(cast, {post, Msg}, S); 340 | handle_call(From, usage, _State, #state{buf=#buf{size=Size, max=MaxSize}}) -> 341 | gen_statem:reply(From, {Size, MaxSize}), 342 | keep_state_and_data; 343 | handle_call(From, {resize, NewSize}, _StateName, S=#state{buf=Buf}) -> 344 | {keep_state, S#state{buf=resize_buf(NewSize,Buf)}, [{reply, From, ok}]}; 345 | handle_call(From, {give_away, Dest, DestData, Origin}, _StateName, S0=#state{ 346 | owner_pid=OwnerPid, owner_monitor_ref = MaybeOwnerMonitorRef, name=Name 347 | }) -> 348 | CanUsePid = case where(Dest) of 349 | DstPid when is_pid(DstPid) -> 350 | is_process_alive(DstPid) and (Origin =:= OwnerPid) and (DstPid =/= OwnerPid); 351 | _ -> false 352 | end, 353 | case CanUsePid of 354 | true -> 355 | case send_ownership_transfer(OwnerPid, Dest, DestData, Name, give_away) of 356 | {ok, DestPid} -> 357 | S1 = case MaybeOwnerMonitorRef of 358 | undefined -> 359 | true = erlang:link(DestPid), 360 | true = erlang:unlink(OwnerPid), 361 | S0#state{owner=Dest, owner_pid=DestPid}; 362 | OwnerMonitorRef when is_reference(OwnerMonitorRef) -> 363 | MaybeDestMonitorRef = erlang:monitor(process, DestPid), 364 | true = erlang:demonitor(OwnerMonitorRef), 365 | S0#state{owner=Dest, owner_pid=DestPid, owner_monitor_ref=MaybeDestMonitorRef} 366 | end, 367 | {next_state, passive, S1, [{reply, From, true}]}; 368 | {error, noproc} -> 369 | {keep_state, S0, [{reply, From, false}]} 370 | end; 371 | false -> 372 | {keep_state, S0, [{reply, From, false}]} 373 | end; 374 | handle_call(_From, _Msg, _StateName, _Data) -> 375 | %% die of starvation, caller! 376 | keep_state_and_data. 377 | 378 | %% @private 379 | handle_info( 380 | {'DOWN', OwnerMonitorRef, process, OwnerPid, Reason}, 381 | _StateName, 382 | S=#state{ 383 | owner_pid=OwnerPid, owner_monitor_ref=OwnerMonitorRef, heir=Heir, heir_data=HeirData, name=Name 384 | }) -> 385 | case send_ownership_transfer(OwnerPid, Heir, HeirData, Name, Reason) of 386 | {ok, HeirPid} -> 387 | erlang:link(HeirPid), 388 | erlang:demonitor(OwnerMonitorRef), 389 | {next_state, passive, S#state{ 390 | owner=Heir, 391 | owner_pid=HeirPid, 392 | owner_monitor_ref=undefined, 393 | heir=undefined, 394 | heir_data=undefined 395 | }}; 396 | {error, noproc} -> 397 | {stop, [{heir, noproc}, {owner, Reason}], S} 398 | end; 399 | handle_info({post, Msg}, StateName, State) -> 400 | %% We allow anonymous posting and redirect it to the internal form. 401 | ?MODULE:StateName(cast, {post, Msg}, State); 402 | 403 | handle_info(_Info, _StateName, _State) -> 404 | keep_state_and_data. 405 | 406 | %% @private 407 | terminate(_Reason, _StateName, _State) -> 408 | ok. 409 | 410 | %% @private 411 | code_change(_OldVsn, StateName, State, _Extra) -> 412 | {ok, StateName, State}. 413 | 414 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 415 | %%% Private Function Definitions %%% 416 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 417 | 418 | send(S=#state{buf=Buf, owner_pid=OwnerPid, filter=Fun, filter_state=FilterState}) -> 419 | {Msgs, Count, Dropped, NewBuf} = buf_filter(Buf, Fun, FilterState), 420 | OwnerPid ! {mail, self(), Msgs, Count, Dropped}, 421 | NewState = S#state{buf=NewBuf, filter=undefined, filter_state=undefined}, 422 | {next_state, passive, NewState}. 423 | 424 | send_notification(S = #state{owner_pid=OwnerPid}) -> 425 | OwnerPid ! {mail, self(), new_data}, 426 | {next_state, passive, S}. 427 | 428 | %%% Generic buffer ops 429 | -spec buf_new(queue | stack | keep_old | {mod, module()}, max()) -> buffer(). 430 | buf_new(queue, Size) -> #buf{type=queue, max=Size, data=queue:new()}; 431 | buf_new(stack, Size) -> #buf{type=stack, max=Size, data=[]}; 432 | buf_new(keep_old, Size) -> #buf{type=keep_old, max=Size, data=queue:new()}; 433 | buf_new(T={mod, Mod}, Size) -> #buf{type=T, max=Size, data=Mod:new()}. 434 | 435 | insert(Msg, B=#buf{type=T, max=Size, size=Size, drop=Drop, data=Data}) -> 436 | B#buf{drop=Drop+1, data=push_drop(T, Msg, Size, Data)}; 437 | insert(Msg, B=#buf{type=T, size=Size, data=Data}) -> 438 | B#buf{size=Size+1, data=push(T, Msg, Data)}. 439 | 440 | size(#buf{size=Size}) -> Size. 441 | 442 | resize_buf(NewMax, B=#buf{max=Max}) when Max =< NewMax -> 443 | B#buf{max=NewMax}; 444 | resize_buf(NewMax, B=#buf{type=T, size=Size, drop=Drop, data=Data}) -> 445 | if Size > NewMax -> 446 | ToDrop = Size - NewMax, 447 | B#buf{size=NewMax, max=NewMax, drop=Drop+ToDrop, 448 | data=drop(T, ToDrop, Size, Data)}; 449 | Size =< NewMax -> 450 | B#buf{max=NewMax} 451 | end. 452 | 453 | buf_filter(Buf=#buf{type=T, drop=D, data=Data, size=C}, Fun, State) -> 454 | {Msgs, Count, Dropped, NewData} = filter(T, Data, Fun, State), 455 | {Msgs, Count, Dropped+D, Buf#buf{drop=0, size=C-(Count+Dropped), data=NewData}}. 456 | 457 | filter(T, Data, Fun, State) -> 458 | filter(T, Data, Fun, State, [], 0, 0). 459 | 460 | filter(T, Data, Fun, State, Msgs, Count, Drop) -> 461 | case pop(T, Data) of 462 | {empty, NewData} -> 463 | {lists:reverse(Msgs), Count, Drop, NewData}; 464 | {{value,Msg}, NewData} -> 465 | case Fun(Msg, State) of 466 | {{ok, Term}, NewState} -> 467 | filter(T, NewData, Fun, NewState, [Term|Msgs], Count+1, Drop); 468 | {drop, NewState} -> 469 | filter(T, NewData, Fun, NewState, Msgs, Count, Drop+1); 470 | skip -> 471 | {lists:reverse(Msgs), Count, Drop, Data} 472 | end 473 | end. 474 | 475 | %% Specific buffer ops 476 | push_drop(T = {mod, Mod}, Msg, Size, Data) -> 477 | case erlang:function_exported(Mod, push_drop, 2) of 478 | true -> Mod:push_drop(Msg, Data); 479 | false -> push(T, Msg, drop(T, Size, Data)) 480 | end; 481 | push_drop(keep_old, _Msg, _Size, Data) -> Data; 482 | push_drop(T, Msg, Size, Data) -> push(T, Msg, drop(T, Size, Data)). 483 | 484 | drop(T, Size, Data) -> drop(T, 1, Size, Data). 485 | 486 | drop(_, 0, _Size, Data) -> Data; 487 | drop(queue, 1, _Size, Queue) -> queue:drop(Queue); 488 | drop(stack, 1, _Size, [_|T]) -> T; 489 | drop(keep_old, 1, _Size, Queue) -> queue:drop_r(Queue); 490 | drop(queue, N, Size, Queue) -> 491 | if Size > N -> element(2, queue:split(N, Queue)); 492 | Size =< N -> queue:new() 493 | end; 494 | drop(stack, N, Size, L) -> 495 | if Size > N -> lists:nthtail(N, L); 496 | Size =< N -> [] 497 | end; 498 | drop(keep_old, N, Size, Queue) -> 499 | if Size > N -> element(1, queue:split(Size - N, Queue)); 500 | Size =< N -> queue:new() 501 | end; 502 | drop({mod, Mod}, N, Size, Data) -> 503 | if Size > N -> Mod:drop(N, Data); 504 | Size =< N -> Mod:new() 505 | end. 506 | 507 | push(queue, Msg, Q) -> queue:in(Msg, Q); 508 | push(stack, Msg, L) -> [Msg|L]; 509 | push(keep_old, Msg, Q) -> queue:in(Msg, Q); 510 | push({mod, Mod}, Msg, Data) -> 511 | Mod:push(Msg, Data). 512 | 513 | pop(queue, Q) -> queue:out(Q); 514 | pop(stack, []) -> {empty, []}; 515 | pop(stack, [H|T]) -> {{value,H}, T}; 516 | pop(keep_old, Q) -> queue:out(Q); 517 | pop({mod, Mod}, Data) -> Mod:pop(Data). 518 | 519 | 520 | send_ownership_transfer(_PreviousOwnerPid, undefined, _HeirData, _BoxName, _Reason) -> {error, noproc}; 521 | send_ownership_transfer(PreviousOwnerPid, NewOwnerName, HeirData, BoxName, Reason) 522 | when ?PROCESS_NAME_GUARD_WITH_LOCAL_NO_PID(NewOwnerName), ?PROCESS_NAME_GUARD(BoxName) -> 523 | case where(NewOwnerName) of 524 | Pid when is_pid(Pid) -> 525 | Pid ! {pobox_transfer, self(), PreviousOwnerPid, HeirData, Reason}, 526 | {ok, Pid}; 527 | _ -> 528 | {error, noproc} 529 | end; 530 | send_ownership_transfer(PreviousOwnerPid, NewOwnerPid, HeirData, BoxName, Reason) 531 | when is_pid(NewOwnerPid), ?PROCESS_NAME_GUARD(BoxName) -> 532 | NewOwnerPid ! {pobox_transfer, self(), PreviousOwnerPid, HeirData, Reason}, 533 | {ok, NewOwnerPid}. 534 | 535 | where(Pid) when is_pid(Pid) -> Pid; 536 | where(Name) when is_atom(Name) -> erlang:whereis(Name); 537 | where({global, Name}) -> global:whereis_name(Name); 538 | where({via, Module, Name}) -> Module:whereis_name(Name). 539 | 540 | proplist_to_pobox_opt_with_defaults(Opts) when is_list(Opts) -> 541 | Fields = record_info(fields, pobox_opts), 542 | [Tag | Values] = tuple_to_list(default_opts()), 543 | Defaults = lists:zip(Fields, Values), 544 | L = lists:map(fun ({K,V}) -> proplists:get_value(K, Opts, V) end, Defaults), 545 | list_to_tuple([Tag | L]). 546 | 547 | 548 | validate_opts(Opts=#pobox_opts{ 549 | name=Name, 550 | owner=Owner, 551 | initial_state=StateName, 552 | max =MaxSize, 553 | type=Type, 554 | heir=Heir 555 | }) when 556 | is_integer(MaxSize), MaxSize > 0, 557 | ?POBOX_BUFFER_TYPE_GUARD(Type), 558 | ?POBOX_START_STATE_GUARD(StateName), 559 | ?PROCESS_NAME_GUARD(Owner), 560 | Name =:= undefined orelse ?PROCESS_NAME_GUARD_WITH_LOCAL_NO_PID(Name), 561 | Heir =:= undefined orelse ?PROCESS_NAME_GUARD(Heir) -> 562 | Opts; 563 | validate_opts(Opts) -> 564 | erlang:error(badarg, [Opts]). 565 | 566 | %% Normalize name to be used by gen:call gen:cast derived functions etc. 567 | start_link_name_to_name(Name0) -> 568 | case Name0 of 569 | {local, Name} -> Name; 570 | undefined -> self(); 571 | Name -> Name 572 | end. --------------------------------------------------------------------------------