├── LICENSE ├── README.markdown ├── doc ├── edoc-info ├── erlang.png ├── index.html ├── modules-frame.html ├── overview-summary.html ├── stylesheet.css ├── varpool.html └── varpool_sup.html ├── mix.exs ├── rebar.config ├── src ├── varpool.app.src ├── varpool.erl └── varpool_sup.erl └── test └── varpool_test_server.erl /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015-2023 Michael Truog 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | varpool: Erlang Process Pools as a Local Variable 2 | ================================================= 3 | 4 | "Let messages ride together to minimize computational costs" 5 | 6 | Purpose 7 | ------- 8 | 9 | There are two main approaches to process pooling in Erlang: 10 | 11 | 1. Treat the pool as shared data to operate on the pool in ways commonly used 12 | with imperative programming languages: A process is removed from the pool, 13 | the process is used and the process is added back to the same pool 14 | (interacting with the pool as mutable data). 15 | 2. Use the pool with Flow-Based-Programming (FBP) to use Erlang in a simpler 16 | way: The pool returns a process without modifying the pool and a 17 | message is sent to the process to utilize the process. Either an 18 | asynchronous or synchronous message is used with the process based on 19 | the requirements of the process and the effect of the request rate on the 20 | process message queue. 21 | 22 | The #1 approach is unfortunately very common in Erlang source code and is 23 | normally (erroneously) regarded as the correct approach when writing Erlang 24 | source code. A major negative aspect of the #1 approach is the impact on 25 | fault-tolerance. How is the process fault-tolerance handled when the process 26 | is removed from the pool (to be used)? If the process is still linked to the 27 | pool in a way where the pool will restart the process if it crashes while not 28 | being within the pool, it is impossible to isolate the cause of the crash 29 | in the implementation. The crash could have occurred due to the external 30 | usage of the process or it could have occurred due to an internal error 31 | within the pool source code. While you can easily argue that a stacktrace 32 | will show you the cause of the error, you will be unable to easily determine 33 | if the stacktrace occurred during or after the error on one side 34 | or the other. The ambiguity and complexity in the #1 approach makes it 35 | error-prone. 36 | 37 | A second major negative aspect of the #1 approach is the scalability impact 38 | when constantly mutating data to get a process from a pool. As the request 39 | rate increases, the latency associated with altering the pool becomes 40 | more significant. The extra latency helps to put artifical limits on any 41 | source code that depends on pooling with the #1 approach. 42 | 43 | The #2 approach is a more natural fit to the fault-tolerance and 44 | concurrency Erlang provides (the pool handles the process fault-tolerance 45 | and relying on the process message queue utilizes Erlang process concurrency). 46 | The pool does not become a scalability bottleneck while it remains 47 | immutable data. An additional benefit is that the implementation is simpler 48 | and easier to understand. 49 | 50 | The most common example of the #1 approach is probably 51 | [poolboy](https://github.com/devinus/poolboy). The varpool library 52 | provides the #2 approach as local variable data (for efficient access). 53 | 54 | Build 55 | ----- 56 | 57 | rebar compile 58 | 59 | Test 60 | ---- 61 | 62 | rebar eunit 63 | 64 | Author 65 | ------ 66 | 67 | Michael Truog (mjtruog at protonmail dot com) 68 | 69 | License 70 | ------- 71 | 72 | MIT License 73 | 74 | -------------------------------------------------------------------------------- /doc/edoc-info: -------------------------------------------------------------------------------- 1 | %% encoding: UTF-8 2 | {application,varpool}. 3 | {modules,[varpool,varpool_sup]}. 4 | -------------------------------------------------------------------------------- /doc/erlang.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okeuday/varpool/2b6dc565c2b9983db40cb89612092f6d11388af7/doc/erlang.png -------------------------------------------------------------------------------- /doc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The varpool application 5 | 6 | 7 | 8 | 9 | 10 | 11 | <h2>This page uses frames</h2> 12 | <p>Your browser does not accept frames. 13 | <br>You should go to the <a href="overview-summary.html">non-frame version</a> instead. 14 | </p> 15 | 16 | 17 | -------------------------------------------------------------------------------- /doc/modules-frame.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The varpool application 5 | 6 | 7 | 8 |

Modules

9 | 10 | 11 |
varpool
varpool_sup
12 | 13 | -------------------------------------------------------------------------------- /doc/overview-summary.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | The varpool application 6 | 7 | 8 | 9 | 10 |

The varpool application

11 | 12 |
13 | 14 |

Generated by EDoc

15 | 16 | 17 | -------------------------------------------------------------------------------- /doc/stylesheet.css: -------------------------------------------------------------------------------- 1 | /* standard EDoc style sheet */ 2 | body { 3 | font-family: Verdana, Arial, Helvetica, sans-serif; 4 | margin-left: .25in; 5 | margin-right: .2in; 6 | margin-top: 0.2in; 7 | margin-bottom: 0.2in; 8 | color: #000000; 9 | background-color: #ffffff; 10 | } 11 | h1,h2 { 12 | margin-left: -0.2in; 13 | } 14 | div.navbar { 15 | background-color: #add8e6; 16 | padding: 0.2em; 17 | } 18 | h2.indextitle { 19 | padding: 0.4em; 20 | background-color: #add8e6; 21 | } 22 | h3.function,h3.typedecl { 23 | background-color: #add8e6; 24 | padding-left: 1em; 25 | } 26 | div.spec { 27 | margin-left: 2em; 28 | background-color: #eeeeee; 29 | } 30 | a.module { 31 | text-decoration:none 32 | } 33 | a.module:hover { 34 | background-color: #eeeeee; 35 | } 36 | ul.definitions { 37 | list-style-type: none; 38 | } 39 | ul.index { 40 | list-style-type: none; 41 | background-color: #eeeeee; 42 | } 43 | 44 | /* 45 | * Minor style tweaks 46 | */ 47 | ul { 48 | list-style-type: square; 49 | } 50 | table { 51 | border-collapse: collapse; 52 | } 53 | td { 54 | padding: 3 55 | } 56 | -------------------------------------------------------------------------------- /doc/varpool.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Module varpool 6 | 7 | 8 | 9 | 10 |
11 | 12 |

Module varpool

13 | 14 |

Local Variable Pool

. 15 |

Copyright © 2015-2021 Michael Truog

16 | 17 |

Version: 2.0.1 May 26 2021 19:32:09 18 | ------------------------------------------------------------------------

19 |

Authors: Michael Truog (mjtruog at protonmail dot com).

20 | 21 |

Description

22 |

Local Variable Pool

23 | 24 |

Data Types

25 | 26 |

group()

27 |

group() = {Group::any(), {M::module(), F::atom(), A::list()}, Options::[{shutdown, pos_integer()} | {count_hash, pos_integer()} | {count_random, pos_integer()} | {hash, {module(), atom()} | fun((any(), pos_integer()) -> non_neg_integer())} | {random, {module(), atom()} | fun((pos_integer()) -> non_neg_integer())}]}

28 | 29 | 30 |

options()

31 |

options() = [{max_r, non_neg_integer()} | {max_t, pos_integer()} | {groups, [group(), ...]}, ...]

32 | 33 | 34 |

Function Index

35 | 36 | 37 | 38 | 39 | 40 |
destroy/1
get/2
get/3
new/1
update/2
41 | 42 |

Function Details

43 | 44 |

destroy/1

45 |
46 |

destroy(Varpool::#varpool{owner = pid(), supervisor = pid(), max_r = non_neg_integer(), max_t = pos_integer(), groups = #{any() := #group{count_hash = pos_integer(), count_random = pos_integer(), count_total = pos_integer(), hash = fun((any(), pos_integer()) -> non_neg_integer()), random = fun((pos_integer()) -> non_neg_integer()), processes = array:array(pid())}}, processes = #{pid() := {any(), non_neg_integer()}}, monitors = [reference()]}) -> ok

47 |

48 |
49 | 50 |

get/2

51 |
52 |

get(Group::any(), Varpool::#varpool{owner = pid(), supervisor = pid(), max_r = non_neg_integer(), max_t = pos_integer(), groups = #{any() := #group{count_hash = pos_integer(), count_random = pos_integer(), count_total = pos_integer(), hash = fun((any(), pos_integer()) -> non_neg_integer()), random = fun((pos_integer()) -> non_neg_integer()), processes = array:array(pid())}}, processes = #{pid() := {any(), non_neg_integer()}}, monitors = [reference()]}) -> pid() | undefined

53 |

54 |
55 | 56 |

get/3

57 |
58 |

get(Group::any(), Key::any(), Varpool::#varpool{owner = pid(), supervisor = pid(), max_r = non_neg_integer(), max_t = pos_integer(), groups = #{any() := #group{count_hash = pos_integer(), count_random = pos_integer(), count_total = pos_integer(), hash = fun((any(), pos_integer()) -> non_neg_integer()), random = fun((pos_integer()) -> non_neg_integer()), processes = array:array(pid())}}, processes = #{pid() := {any(), non_neg_integer()}}, monitors = [reference()]}) -> pid() | undefined

59 |

60 |
61 | 62 |

new/1

63 |
64 |

new(Options::options()) -> #varpool{owner = pid(), supervisor = pid(), max_r = non_neg_integer(), max_t = pos_integer(), groups = #{any() := #group{count_hash = pos_integer(), count_random = pos_integer(), count_total = pos_integer(), hash = fun((any(), pos_integer()) -> non_neg_integer()), random = fun((pos_integer()) -> non_neg_integer()), processes = array:array(pid())}}, processes = #{pid() := {any(), non_neg_integer()}}, monitors = [reference()]}

65 |

66 |
67 | 68 |

update/2

69 |
70 |

update(X1::any(), Varpool::#varpool{owner = pid(), supervisor = pid(), max_r = non_neg_integer(), max_t = pos_integer(), groups = #{any() := #group{count_hash = pos_integer(), count_random = pos_integer(), count_total = pos_integer(), hash = fun((any(), pos_integer()) -> non_neg_integer()), random = fun((pos_integer()) -> non_neg_integer()), processes = array:array(pid())}}, processes = #{pid() := {any(), non_neg_integer()}}, monitors = [reference()]}) -> {updated, #varpool{owner = pid(), supervisor = pid(), max_r = non_neg_integer(), max_t = pos_integer(), groups = #{any() := #group{count_hash = pos_integer(), count_random = pos_integer(), count_total = pos_integer(), hash = fun((any(), pos_integer()) -> non_neg_integer()), random = fun((pos_integer()) -> non_neg_integer()), processes = array:array(pid())}}, processes = #{pid() := {any(), non_neg_integer()}}, monitors = [reference()]}} | {ignored, #varpool{owner = pid(), supervisor = pid(), max_r = non_neg_integer(), max_t = pos_integer(), groups = #{any() := #group{count_hash = pos_integer(), count_random = pos_integer(), count_total = pos_integer(), hash = fun((any(), pos_integer()) -> non_neg_integer()), random = fun((pos_integer()) -> non_neg_integer()), processes = array:array(pid())}}, processes = #{pid() := {any(), non_neg_integer()}}, monitors = [reference()]}}

71 |

72 |
73 |
74 | 75 | 76 |

Generated by EDoc

77 | 78 | 79 | -------------------------------------------------------------------------------- /doc/varpool_sup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Module varpool_sup 6 | 7 | 8 | 9 | 10 |
11 | 12 |

Module varpool_sup

13 | 14 |

Local Variable Pool Supervisor

. 15 |

Copyright © 2015-2020 Michael Truog

16 | 17 |

Version: 2.0.1 May 26 2021 19:32:09 18 | ------------------------------------------------------------------------

19 |

Authors: Michael Truog (mjtruog at protonmail dot com).

20 | 21 |

Description

22 |

Local Variable Pool Supervisor

23 | 24 |

Function Index

25 | 26 | 28 | 30 |
init/1
start_link/4 27 |

Start the Pool supervisor.

.
stop_link/1 29 |

Stop the Pool supervisor.

.
31 | 32 |

Function Details

33 | 34 |

init/1

35 |
36 |

init(X1) -> any()

37 |

38 |
39 | 40 |

start_link/4

41 |
42 |

start_link(Parent::pid(), MaxR::non_neg_integer(), MaxT::pos_integer(), Pool::nonempty_list()) -> {ok, pid()} | {error, any()}

43 |

44 |

45 |

Start the Pool supervisor.

46 |

47 | 48 |

stop_link/1

49 |
50 |

stop_link(Supervisor::pid()) -> ok

51 |

52 |

53 |

Stop the Pool supervisor.

54 |

55 |
56 | 57 | 58 |

Generated by EDoc

59 | 60 | 61 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | #-*-Mode:elixir;coding:utf-8;tab-width:2;c-basic-offset:2;indent-tabs-mode:()-*- 2 | # ex: set ft=elixir fenc=utf-8 sts=2 ts=2 sw=2 et nomod: 3 | 4 | defmodule Varpool.Mixfile do 5 | use Mix.Project 6 | 7 | def project do 8 | [app: :varpool, 9 | version: "2.0.7", 10 | language: :erlang, 11 | erlc_options: [ 12 | :deterministic, 13 | :debug_info, 14 | :warn_export_vars, 15 | :warn_unused_import, 16 | #:warn_missing_spec, 17 | :warnings_as_errors], 18 | description: description(), 19 | package: package(), 20 | deps: deps()] 21 | end 22 | 23 | def application do 24 | [applications: [ 25 | :reltool_util]] 26 | end 27 | 28 | defp deps do 29 | [{:reltool_util, ">= 2.0.7"}] 30 | end 31 | 32 | defp description do 33 | "Erlang Process Pools as a Local Variable" 34 | end 35 | 36 | defp package do 37 | [files: ~w(src doc test rebar.config README.markdown LICENSE), 38 | maintainers: ["Michael Truog"], 39 | licenses: ["MIT"], 40 | links: %{"GitHub" => "https://github.com/okeuday/varpool"}] 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | %-*-Mode:erlang;coding:utf-8;tab-width:4;c-basic-offset:4;indent-tabs-mode:()-*- 2 | % ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et nomod: 3 | 4 | {deps, 5 | [{reltool_util, ".*", 6 | {git, "https://github.com/okeuday/reltool_util.git", 7 | {tag, "v2.0.7"}}}]}. 8 | 9 | {erl_opts, 10 | [warn_export_vars, 11 | warn_unused_import, 12 | %warn_missing_spec, 13 | warnings_as_errors]}. 14 | {cover_enabled, true}. 15 | {cover_print_enabled, true}. 16 | {cover_export_enabled, true}. 17 | 18 | -------------------------------------------------------------------------------- /src/varpool.app.src: -------------------------------------------------------------------------------- 1 | %-*-Mode:erlang;coding:utf-8;tab-width:4;c-basic-offset:4;indent-tabs-mode:()-*- 2 | % ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et nomod: 3 | 4 | {application, varpool, 5 | [{description, "Local Variable Pool"}, 6 | {vsn, "2.0.7"}, 7 | {modules, [ 8 | varpool, 9 | varpool_sup 10 | ]}, 11 | {registered, []}, 12 | {applications, [ 13 | reltool_util, 14 | stdlib, 15 | kernel 16 | ]}]}. 17 | 18 | -------------------------------------------------------------------------------- /src/varpool.erl: -------------------------------------------------------------------------------- 1 | %-*-Mode:erlang;coding:utf-8;tab-width:4;c-basic-offset:4;indent-tabs-mode:()-*- 2 | % ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et nomod: 3 | %%% 4 | %%%------------------------------------------------------------------------ 5 | %%% @doc 6 | %%% ==Local Variable Pool== 7 | %%% @end 8 | %%% 9 | %%% MIT License 10 | %%% 11 | %%% Copyright (c) 2015-2021 Michael Truog 12 | %%% 13 | %%% Permission is hereby granted, free of charge, to any person obtaining a 14 | %%% copy of this software and associated documentation files (the "Software"), 15 | %%% to deal in the Software without restriction, including without limitation 16 | %%% the rights to use, copy, modify, merge, publish, distribute, sublicense, 17 | %%% and/or sell copies of the Software, and to permit persons to whom the 18 | %%% Software is furnished to do so, subject to the following conditions: 19 | %%% 20 | %%% The above copyright notice and this permission notice shall be included in 21 | %%% all copies or substantial portions of the Software. 22 | %%% 23 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 28 | %%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 29 | %%% DEALINGS IN THE SOFTWARE. 30 | %%% 31 | %%% @author Michael Truog 32 | %%% @copyright 2015-2021 Michael Truog 33 | %%% @version 2.0.1 {@date} {@time} 34 | %%%------------------------------------------------------------------------ 35 | 36 | -module(varpool). 37 | -author('mjtruog at protonmail dot com'). 38 | 39 | %% external interface 40 | -export([new/1, 41 | destroy/1, 42 | update/2, 43 | get/2, 44 | get/3]). 45 | 46 | -record(group, 47 | { 48 | count_hash :: pos_integer(), % dimension 1 49 | count_random :: pos_integer(), % dimension 2 50 | count_total :: pos_integer(), 51 | hash :: fun((any(), pos_integer()) -> non_neg_integer()), 52 | random :: fun((pos_integer()) -> non_neg_integer()), 53 | processes :: array:array(pid()) 54 | }). 55 | 56 | -record(varpool, 57 | { 58 | owner :: pid(), 59 | supervisor :: pid(), 60 | max_r :: non_neg_integer(), 61 | max_t :: pos_integer(), 62 | groups :: #{any() := #group{}}, 63 | processes = #{} :: #{pid() := {any(), non_neg_integer()}}, 64 | monitors = [] :: list(reference()) 65 | }). 66 | 67 | -define(DEFAULT_MAX_R, 5). % max restart count 68 | -define(DEFAULT_MAX_T, 300). % max time in seconds 69 | -define(DEFAULT_HASH, fun erlang:phash2/2). 70 | -define(DEFAULT_RANDOM, 71 | fun(Count) -> 72 | % assuming exsplus/exsp for 58 bits, period 8.31e34 73 | rand:uniform(Count) - 1 74 | end). 75 | 76 | -type group() :: 77 | {Group :: any(), 78 | {M :: module(), F :: atom(), A :: list()}, 79 | Options :: list({shutdown, pos_integer()} | 80 | {count_hash, pos_integer()} | 81 | {count_random, pos_integer()} | 82 | {hash, 83 | {module(), atom()} | 84 | fun((any(), pos_integer()) -> non_neg_integer())} | 85 | {random, 86 | {module(), atom()} | 87 | fun((pos_integer()) -> non_neg_integer())})}. 88 | -type options() :: 89 | nonempty_list({max_r, non_neg_integer()} | 90 | {max_t, pos_integer()} | 91 | {groups, nonempty_list(group())}). 92 | -export_type([options/0]). 93 | 94 | %%%------------------------------------------------------------------------ 95 | %%% External interface functions 96 | %%%------------------------------------------------------------------------ 97 | 98 | -spec new(Options :: options()) -> 99 | #varpool{}. 100 | 101 | new([_ | _] = Options) -> 102 | Defaults = [ 103 | {max_r, ?DEFAULT_MAX_R}, 104 | {max_t, ?DEFAULT_MAX_T}, 105 | {groups, []}], 106 | [MaxR, MaxT, Groups] = take_values(Defaults, Options), 107 | true = is_integer(MaxR) andalso (MaxR >= 0), 108 | true = is_integer(MaxT) andalso (MaxT >= 1), 109 | true = is_list(Groups) andalso (length(Groups) > 0), 110 | ShutdownDefault = if 111 | MaxR >= 1 -> 112 | ((MaxT * 1000) div MaxR) - 100; 113 | MaxR == 0 -> 114 | (MaxT * 1000) - 100 115 | end, 116 | {GroupsData, GroupsSupervisor} = groups(Groups, ShutdownDefault), 117 | Owner = self(), 118 | {ok, Supervisor} = varpool_sup:start_link(Owner, MaxR, MaxT, 119 | GroupsSupervisor), 120 | #varpool{owner = Owner, 121 | supervisor = Supervisor, 122 | max_r = MaxR, 123 | max_t = MaxT, 124 | groups = GroupsData}. 125 | 126 | -spec destroy(#varpool{}) -> 127 | ok. 128 | 129 | destroy(#varpool{owner = Owner, 130 | supervisor = Supervisor, 131 | monitors = Monitors}) -> 132 | true = Owner =:= self(), 133 | [erlang:demonitor(MonitorRef, [flush]) || MonitorRef <- Monitors], 134 | ok = varpool_sup:stop_link(Supervisor), 135 | ok. 136 | 137 | -spec update(any(), #varpool{}) -> 138 | {updated, #varpool{}} | 139 | {ignored, #varpool{}}. 140 | 141 | update({'UP', Supervisor, process, Child, {Group, I} = Info}, 142 | #varpool{owner = Owner, 143 | supervisor = Supervisor, 144 | groups = Groups, 145 | processes = Processes, 146 | monitors = Monitors} = VarPool) -> 147 | true = Owner =:= self(), 148 | NewProcesses = maps:put(Child, Info, Processes), 149 | GroupState = maps:get(Group, Groups), 150 | #group{processes = GroupProcesses} = GroupState, 151 | NewGroupProcesses = array:set(I, Child, GroupProcesses), 152 | NewGroupState = GroupState#group{processes = NewGroupProcesses}, 153 | NewGroups = maps:put(Group, NewGroupState, Groups), 154 | NewMonitors = [erlang:monitor(process, Child) | Monitors], 155 | {updated, VarPool#varpool{groups = NewGroups, 156 | processes = NewProcesses, 157 | monitors = NewMonitors}}; 158 | update({'DOWN', MonitorRef, process, Child, _Info}, 159 | #varpool{owner = Owner, 160 | groups = Groups, 161 | processes = Processes, 162 | monitors = Monitors} = VarPool) -> 163 | true = Owner =:= self(), 164 | case maps:find(Child, Processes) of 165 | error -> 166 | {ignored, VarPool}; 167 | {ok, {Group, I}} -> 168 | NewProcesses = maps:remove(Child, Processes), 169 | GroupState = maps:get(Group, Groups), 170 | #group{processes = GroupProcesses} = GroupState, 171 | NewGroupProcesses = array:set(I, undefined, GroupProcesses), 172 | NewGroupState = GroupState#group{processes = NewGroupProcesses}, 173 | NewGroups = maps:put(Group, NewGroupState, Groups), 174 | NewMonitors = lists:delete(MonitorRef, Monitors), 175 | {updated, VarPool#varpool{groups = NewGroups, 176 | processes = NewProcesses, 177 | monitors = NewMonitors}} 178 | end; 179 | update(_, 180 | #varpool{owner = Owner} = VarPool) -> 181 | true = Owner =:= self(), 182 | {ignored, VarPool}. 183 | 184 | -spec get(Group :: any(), #varpool{}) -> 185 | pid() | undefined. 186 | 187 | get(Group, #varpool{groups = Groups}) -> 188 | #group{count_hash = 1, 189 | count_random = CountRandom, 190 | random = Random, 191 | processes = GroupProcesses} = maps:get(Group, Groups), 192 | IndexHash = 0, 193 | IndexRandom = if 194 | CountRandom > 1 -> 195 | Random(CountRandom); 196 | CountRandom =:= 1 -> 197 | 0 198 | end, 199 | I = IndexHash * CountRandom + IndexRandom, 200 | Child = array:get(I, GroupProcesses), 201 | if 202 | Child =:= undefined -> 203 | get_process(I, 204 | IndexHash * CountRandom, 205 | (IndexHash + 1) * CountRandom - 1, GroupProcesses); 206 | is_pid(Child) -> 207 | Child 208 | end. 209 | 210 | -spec get(Group :: any(), Key :: any(), #varpool{}) -> 211 | pid() | undefined. 212 | 213 | get(Group, Key, #varpool{groups = Groups}) -> 214 | #group{count_hash = CountHash, 215 | count_random = CountRandom, 216 | hash = Hash, 217 | random = Random, 218 | processes = GroupProcesses} = maps:get(Group, Groups), 219 | IndexHash = if 220 | CountHash > 1 -> 221 | Hash(Key, CountHash); 222 | CountHash =:= 1 -> 223 | 0 224 | end, 225 | IndexRandom = if 226 | CountRandom > 1 -> 227 | Random(CountRandom); 228 | CountRandom =:= 1 -> 229 | 0 230 | end, 231 | I = IndexHash * CountRandom + IndexRandom, 232 | Child = array:get(I, GroupProcesses), 233 | if 234 | Child =:= undefined -> 235 | get_process(I, 236 | IndexHash * CountRandom, 237 | (IndexHash + 1) * CountRandom - 1, GroupProcesses); 238 | is_pid(Child) -> 239 | Child 240 | end. 241 | 242 | %%%------------------------------------------------------------------------ 243 | %%% Private functions 244 | %%%------------------------------------------------------------------------ 245 | 246 | groups([], GroupsData, GroupsSupervisor, _) -> 247 | {GroupsData, lists:reverse(GroupsSupervisor)}; 248 | groups([{Group, {M, F, A}, Options} | Groups], 249 | GroupsData, GroupsSupervisor, ShutdownDefault) 250 | when is_atom(M), is_atom(F), is_list(A), is_list(Options) -> 251 | Defaults = [ 252 | {shutdown, ShutdownDefault}, 253 | {count_hash, 1}, 254 | {count_random, 1}, 255 | {hash, ?DEFAULT_HASH}, 256 | {random, ?DEFAULT_RANDOM}], 257 | [Shutdown, CountHash, CountRandom, 258 | Hash0, Random0] = take_values(Defaults, Options), 259 | true = is_integer(Shutdown) andalso 260 | (Shutdown >= 1) andalso (Shutdown =< ShutdownDefault), 261 | true = is_integer(CountHash) andalso (CountHash >= 1), 262 | true = is_integer(CountRandom) andalso (CountRandom >= 1), 263 | Hash1 = case Hash0 of 264 | {HashModule, HashFunction} 265 | when is_atom(HashModule), is_atom(HashFunction) -> 266 | true = erlang:function_exported(HashModule, HashFunction, 2), 267 | fun(HashArg1, HashArg2) -> 268 | HashModule:HashFunction(HashArg1, HashArg2) 269 | end; 270 | _ when is_function(Hash0, 2) -> 271 | Hash0 272 | end, 273 | Random1 = case Random0 of 274 | {RandomModule, RandomFunction} 275 | when is_atom(RandomModule), is_atom(RandomFunction) -> 276 | true = erlang:function_exported(RandomModule, RandomFunction, 1), 277 | fun(RandomArg1) -> 278 | RandomModule:RandomFunction(RandomArg1) 279 | end; 280 | _ when is_function(Random0, 1) -> 281 | Random0 282 | end, 283 | CountTotal = CountHash * CountRandom, 284 | GroupProcesses = array:new([{size, CountTotal}]), 285 | NewGroupsData = maps:put(Group, 286 | #group{count_hash = CountHash, 287 | count_random = CountRandom, 288 | count_total = CountTotal, 289 | hash = Hash1, 290 | random = Random1, 291 | processes = GroupProcesses}, 292 | GroupsData), 293 | NewGroupsSupervisor = [{Group, CountTotal, M, F, A, Shutdown} | 294 | GroupsSupervisor], 295 | groups(Groups, NewGroupsData, NewGroupsSupervisor, ShutdownDefault). 296 | 297 | groups(Groups, ShutdownDefault) -> 298 | groups(Groups, #{}, [], ShutdownDefault). 299 | 300 | get_process(Istop, Istop, _, _, _) -> 301 | undefined; 302 | get_process(I, Istop, Istart, Iend, GroupProcesses) -> 303 | Child = array:get(I, GroupProcesses), 304 | if 305 | Child =:= undefined -> 306 | if 307 | I == Iend -> 308 | get_process(Istart, Istop, Istart, Iend, GroupProcesses); 309 | true -> 310 | get_process(I + 1, Istop, Istart, Iend, GroupProcesses) 311 | end; 312 | is_pid(Child) -> 313 | Child 314 | end. 315 | 316 | get_process(Iend, Istart, Iend, GroupProcesses) -> 317 | get_process(Istart, Iend, Istart, Iend, GroupProcesses); 318 | get_process(I, Istart, Iend, GroupProcesses) -> 319 | get_process(I + 1, I, Istart, Iend, GroupProcesses). 320 | 321 | take_values(DefaultList, List) 322 | when is_list(DefaultList), is_list(List) -> 323 | take_values([], DefaultList, List). 324 | 325 | take_values(Result, [], List) -> 326 | lists:reverse(Result) ++ List; 327 | 328 | take_values(Result, [{Key, Default} | DefaultList], List) 329 | when is_atom(Key) -> 330 | case lists:keytake(Key, 1, List) of 331 | false -> 332 | take_values([Default | Result], DefaultList, List); 333 | {value, {Key, Value}, RemainingList} -> 334 | take_values([Value | Result], DefaultList, RemainingList) 335 | end. 336 | 337 | -------------------------------------------------------------------------------- /src/varpool_sup.erl: -------------------------------------------------------------------------------- 1 | %-*-Mode:erlang;coding:utf-8;tab-width:4;c-basic-offset:4;indent-tabs-mode:()-*- 2 | % ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et nomod: 3 | %%% 4 | %%%------------------------------------------------------------------------ 5 | %%% @doc 6 | %%% ==Local Variable Pool Supervisor== 7 | %%% @end 8 | %%% 9 | %%% MIT License 10 | %%% 11 | %%% Copyright (c) 2015-2020 Michael Truog 12 | %%% 13 | %%% Permission is hereby granted, free of charge, to any person obtaining a 14 | %%% copy of this software and associated documentation files (the "Software"), 15 | %%% to deal in the Software without restriction, including without limitation 16 | %%% the rights to use, copy, modify, merge, publish, distribute, sublicense, 17 | %%% and/or sell copies of the Software, and to permit persons to whom the 18 | %%% Software is furnished to do so, subject to the following conditions: 19 | %%% 20 | %%% The above copyright notice and this permission notice shall be included in 21 | %%% all copies or substantial portions of the Software. 22 | %%% 23 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 28 | %%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 29 | %%% DEALINGS IN THE SOFTWARE. 30 | %%% 31 | %%% @author Michael Truog 32 | %%% @copyright 2015-2020 Michael Truog 33 | %%% @version 2.0.1 {@date} {@time} 34 | %%%------------------------------------------------------------------------ 35 | 36 | -module(varpool_sup). 37 | -author('mjtruog at protonmail dot com'). 38 | 39 | %% external interface 40 | -export([start_link/4, 41 | stop_link/1]). 42 | 43 | %% internal callbacks 44 | -export([monitor_up/6, 45 | stop_link_process/0]). 46 | 47 | %% supervisor callbacks 48 | -export([init/1]). 49 | 50 | %%%------------------------------------------------------------------------ 51 | %%% External interface functions 52 | %%%------------------------------------------------------------------------ 53 | 54 | %%------------------------------------------------------------------------- 55 | %% @doc 56 | %% ===Start the Pool supervisor.=== 57 | %% @end 58 | %%------------------------------------------------------------------------- 59 | 60 | -spec start_link(Parent :: pid(), 61 | MaxR :: non_neg_integer(), 62 | MaxT :: pos_integer(), 63 | Pool :: nonempty_list()) -> 64 | {'ok', pid()} | 65 | {'error', any()}. 66 | 67 | start_link(Parent, MaxR, MaxT, [_ | _] = Pool) 68 | when is_pid(Parent), is_integer(MaxR), MaxR >= 0, 69 | is_integer(MaxT), MaxT >= 1 -> 70 | supervisor:start_link(?MODULE, [Parent, MaxR, MaxT, Pool]). 71 | 72 | %%------------------------------------------------------------------------- 73 | %% @doc 74 | %% ===Stop the Pool supervisor.=== 75 | %% @end 76 | %%------------------------------------------------------------------------- 77 | 78 | -spec stop_link(Supervisor :: pid()) -> 79 | 'ok'. 80 | 81 | stop_link(Supervisor) 82 | when is_pid(Supervisor) -> 83 | erlang:unlink(Supervisor), 84 | {ok, _} = supervisor:start_child(Supervisor, 85 | {shutdown, 86 | {?MODULE, stop_link_process, []}, 87 | permanent, brutal_kill, worker, []}), 88 | ok. 89 | 90 | %%------------------------------------------------------------------------- 91 | %% @hidden 92 | %% Internal callback 93 | %% @end 94 | %%------------------------------------------------------------------------- 95 | 96 | -spec monitor_up(Parent :: pid(), 97 | M :: module(), 98 | F :: atom(), 99 | A :: list(), 100 | Group :: any(), 101 | I :: non_neg_integer()) -> 102 | any(). 103 | 104 | monitor_up(Parent, M, F, A, Group, I) -> 105 | case erlang:apply(M, F, A) of 106 | {ok, Child} = Success -> 107 | ok = monitor_up_message(Parent, Child, Group, I), 108 | Success; 109 | {ok, Child, _Info} = Success -> 110 | ok = monitor_up_message(Parent, Child, Group, I), 111 | Success; 112 | ignore = Ignore -> 113 | Ignore; 114 | Error -> 115 | Error 116 | end. 117 | 118 | %%------------------------------------------------------------------------- 119 | %% @hidden 120 | %% Internal callback 121 | %% @end 122 | %%------------------------------------------------------------------------- 123 | 124 | -spec stop_link_process() -> 125 | {ok, pid()}. 126 | 127 | stop_link_process() -> 128 | Child = erlang:spawn_link(fun stop_link_process_kill/0), 129 | {ok, Child}. 130 | 131 | %%%------------------------------------------------------------------------ 132 | %%% Callback functions from supervisor 133 | %%%------------------------------------------------------------------------ 134 | 135 | init([Parent, MaxR, MaxT, Pool]) -> 136 | {ok, {{one_for_one, MaxR, MaxT}, child_specs(Pool, Parent)}}. 137 | 138 | %%%------------------------------------------------------------------------ 139 | %%% Private functions 140 | %%%------------------------------------------------------------------------ 141 | 142 | monitor_up_message(Parent, Child, Group, I) -> 143 | Parent ! {'UP', self(), process, Child, {Group, I}}, 144 | ok. 145 | 146 | child_specs([], ChildSpecs, _) -> 147 | ChildSpecs; 148 | child_specs([{Group, Count, M, F, A, Shutdown} | Pool], ChildSpecs, Parent) -> 149 | NewChildSpecs = case module_behaviour(M) of 150 | undefined -> 151 | child_spec(Count - 1, Group, Parent, M, F, A, 152 | Shutdown, worker, [M]); 153 | gen_event -> 154 | child_spec(Count - 1, Group, Parent, M, F, A, 155 | Shutdown, worker, dynamic); 156 | supervisor -> 157 | child_spec(Count - 1, Group, Parent, M, F, A, 158 | infinity, supervisor, [M]) 159 | end ++ ChildSpecs, 160 | child_specs(Pool, NewChildSpecs, Parent). 161 | 162 | child_specs(Pool, Parent) -> 163 | child_specs(Pool, [], Parent). 164 | 165 | child_spec_entry(I, Group, Parent, M, F, A, Shutdown, Type, Modules) -> 166 | {{Group, I}, {?MODULE, monitor_up, [Parent, M, F, A, Group, I]}, 167 | permanent, Shutdown, Type, Modules}. 168 | 169 | child_spec(0 = I, Group, Parent, M, F, A, Shutdown, Type, Modules) -> 170 | [child_spec_entry(I, Group, Parent, M, F, A, Shutdown, Type, Modules)]; 171 | child_spec(I, Group, Parent, M, F, A, Shutdown, Type, Modules) -> 172 | [child_spec_entry(I, Group, Parent, M, F, A, Shutdown, Type, Modules) | 173 | child_spec(I - 1, Group, Parent, M, F, A, Shutdown, Type, Modules)]. 174 | 175 | special_behaviour([]) -> 176 | undefined; 177 | special_behaviour([supervisor = Behaviour | _]) -> 178 | Behaviour; 179 | special_behaviour([gen_event = Behaviour | _]) -> 180 | Behaviour; 181 | special_behaviour([_ | L]) -> 182 | special_behaviour(L). 183 | 184 | module_behaviour(M) -> 185 | special_behaviour(reltool_util:module_behaviours(M)). 186 | 187 | -spec stop_link_process_kill() -> no_return(). 188 | 189 | stop_link_process_kill() -> 190 | erlang:exit(shutdown). 191 | -------------------------------------------------------------------------------- /test/varpool_test_server.erl: -------------------------------------------------------------------------------- 1 | %-*-Mode:erlang;coding:utf-8;tab-width:4;c-basic-offset:4;indent-tabs-mode:()-*- 2 | % ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et nomod: 3 | %%% 4 | %%%------------------------------------------------------------------------ 5 | %%% @doc 6 | %%% ==Local Variable Pool Test Server== 7 | %%% @end 8 | %%% 9 | %%% MIT License 10 | %%% 11 | %%% Copyright (c) 2015-2022 Michael Truog 12 | %%% 13 | %%% Permission is hereby granted, free of charge, to any person obtaining a 14 | %%% copy of this software and associated documentation files (the "Software"), 15 | %%% to deal in the Software without restriction, including without limitation 16 | %%% the rights to use, copy, modify, merge, publish, distribute, sublicense, 17 | %%% and/or sell copies of the Software, and to permit persons to whom the 18 | %%% Software is furnished to do so, subject to the following conditions: 19 | %%% 20 | %%% The above copyright notice and this permission notice shall be included in 21 | %%% all copies or substantial portions of the Software. 22 | %%% 23 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 28 | %%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 29 | %%% DEALINGS IN THE SOFTWARE. 30 | %%% 31 | %%% @author Michael Truog 32 | %%% @copyright 2015-2022 Michael Truog 33 | %%% @version 2.0.5 {@date} {@time} 34 | %%%------------------------------------------------------------------------ 35 | 36 | -module(varpool_test_server). 37 | -author('mjtruog at protonmail dot com'). 38 | 39 | %% external interface 40 | -export([start_link/0, 41 | ping/1]). 42 | 43 | %%%------------------------------------------------------------------------ 44 | %%% External interface functions 45 | %%%------------------------------------------------------------------------ 46 | 47 | start_link() -> 48 | {ok, erlang:spawn_link(fun loop/0)}. 49 | 50 | ping(Pid) -> 51 | Pid ! {self(), ping}, 52 | receive 53 | pong -> 54 | pong 55 | after 56 | 5000 -> 57 | pang 58 | end. 59 | 60 | %%%------------------------------------------------------------------------ 61 | %%% Private functions 62 | %%%------------------------------------------------------------------------ 63 | 64 | loop() -> 65 | receive 66 | {Pid, ping} -> 67 | Pid ! pong 68 | end, 69 | loop(). 70 | 71 | -ifdef(TEST). 72 | -include_lib("eunit/include/eunit.hrl"). 73 | 74 | -ifdef(CLOUDI_TEST_TIMEOUT). 75 | -define(TEST_TIMEOUT, ?CLOUDI_TEST_TIMEOUT). % seconds 76 | -else. 77 | -define(TEST_TIMEOUT, 10). % seconds 78 | -endif. 79 | 80 | module_test_() -> 81 | {timeout, ?TEST_TIMEOUT, [ 82 | {"internal tests", ?_assertEqual(ok, t_basic())} 83 | ]}. 84 | 85 | t_basic() -> 86 | application:start(varpool), 87 | P0 = varpool:new([{groups, 88 | [{group_0, {varpool_test_server, start_link, []}, 89 | [{count_hash, 4}, {count_random, 4}]} 90 | ]}]), 91 | P3 = lists:foldl(fun(_, P1) -> 92 | receive 93 | Up0 -> 94 | {'UP', _, process, _, {_, _}} = Up0, 95 | {updated, P2} = varpool:update(Up0, P1), 96 | P2 97 | end 98 | end, P0, lists:seq(1, 16)), 99 | Child0 = varpool:get(group_0, 0, P3), 100 | Child1 = varpool:get(group_0, 1, P3), 101 | true = (Child0 /= Child1), 102 | true = is_pid(Child0), 103 | true = is_pid(Child1), 104 | pong = varpool_test_server:ping(Child0), 105 | pong = varpool_test_server:ping(Child1), 106 | erlang:exit(Child0, kill), 107 | P5 = receive 108 | Down0 -> 109 | {'DOWN', _, process, _, _} = Down0, 110 | {updated, P4} = varpool:update(Down0, P3), 111 | P4 112 | end, 113 | PN = receive 114 | Up1 -> 115 | {'UP', _, process, _, {_, _}} = Up1, 116 | {updated, P6} = varpool:update(Up1, P5), 117 | P6 118 | end, 119 | Child0New = varpool:get(group_0, 0, PN), 120 | true = (Child0 /= Child0New), 121 | true = is_pid(Child0New), 122 | ok = varpool:destroy(PN), 123 | ok. 124 | 125 | -endif. 126 | --------------------------------------------------------------------------------