├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── conf ├── sys.config └── vm.args ├── include ├── log.hrl └── mx.hrl ├── rebar.config ├── rebar.lock ├── rebar3 ├── src ├── mx.app.src ├── mx.erl ├── mx_app.erl ├── mx_broker.erl ├── mx_broker_sup.erl ├── mx_mnesia.erl ├── mx_queue.erl └── mx_sup.erl └── test ├── ct └── mx_SUITE.erl └── mx_tests.erl /.gitignore: -------------------------------------------------------------------------------- 1 | deps 2 | ebin 3 | log 4 | .rebar* 5 | .eunit 6 | *.o 7 | *.beam 8 | _* 9 | erl_crash.dump 10 | Mnesia* 11 | .dialyzer_plt 12 | 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Taras Halturin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | 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 FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REBAR = $(shell pwd)/rebar3 2 | DIALYZER ?= dialyzer 3 | 4 | DIALYZER_WARNINGS = -Wunmatched_returns -Werror_handling \ 5 | -Wrace_conditions -Wunderspecs 6 | 7 | .PHONY: deps compile 8 | 9 | all: compile 10 | 11 | compile: clean 12 | @$(REBAR) compile 13 | 14 | test: compile 15 | @$(REBAR) as test eunit 16 | 17 | ct: 18 | @$(REBAR) ct --setcookie devcook --name mxtest001@127.0.0.1 19 | 20 | clean: 21 | @$(REBAR) clean 22 | 23 | deps: 24 | @$(REBAR) deps 25 | 26 | build-plt: 27 | @$(DIALYZER) --build_plt --output_plt .dialyzer_plt \ 28 | --apps kernel stdlib 29 | 30 | dialyze: build-plt 31 | @$(DIALYZER) --src src --plt .dialyzer_plt $(DIALYZER_WARNINGS) 32 | 33 | rel: compile 34 | $(REBAR) release 35 | 36 | release: compile 37 | $(REBAR) as prod release -n mx 38 | 39 | run: release 40 | $(REBAR) run 41 | 42 | tar: 43 | $(REBAR) as prod tar release -n mx 44 | 45 | demo_run: compile 46 | # only for demonstration purposes 47 | erl -name $(node_name) -pa _build/default/lib/mx/ebin \ 48 | _build/default/lib/gproc/ebin \ 49 | _build/default/lib/lager/ebin \ 50 | _build/default/lib/goldrush/ebin \ 51 | -config "conf/sys.config" \ 52 | -eval "lists:map(fun(App) -> application:start(App) end, [gproc, syntax_tools, compiler, goldrush, lager])" 53 | 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OTP Message Broker 2 | 3 | ## Overview 4 | 5 | Universal OTP message broker features: 6 | * create channels (pub/sub) 7 | * pools (workers queue) 8 | * mixing it (pool of channels, channel of pools... etc.) 9 | * send messages with specify priority of delivering messages (range: 1..10) 10 | * pool has 3 balance methods: rr(round robin), hash (by erlang:phash(Message, lenth(Pool))), random 11 | * defer message delivering in case of 12 | - exceed the queue limit (10000) and receiver has the 'true' in 'defer' option. 13 | - client has 'offline' state and the 'defer' option is set to 'true' 14 | * 'async' Client option allows you control the delivery process. Default value of this option is 'true'. 15 | 16 | ## Install 17 | For **dev** purposes simple run: 18 | ``` 19 | git clone https://github.com/halturin/mx && cd mx && make compile && echo "MX is installed in $(pwd)" 20 | ``` 21 | For **production** use: 22 | ``` 23 | % rebar.config -- add mx in deps section 24 | {deps, [ 25 | {mx, {git, "git://github.com/halturin/mx.git", {branch, "master"}}} 26 | ]}. 27 | %% and add mx to the relx section 28 | {relx, [{release, {your_rel, "0.0.1"}, 29 | [..., 30 | mx]} 31 | ]}. 32 | ``` 33 | 34 | You can specifying a **queue limits** in sys.config: 35 | ``` 36 | [ 37 | {mx, [ 38 | {queue_length_limit, 100000}, 39 | {queue_low_threshold, 0.6}, % 60% 40 | {queue_high_threshold, 0.8} % 80% 41 | ]} 42 | ] 43 | ``` 44 | 45 | ## Run 46 | 47 | ``` 48 | make run 49 | ``` 50 | 51 | ## Distributed mode 52 | 53 | Run the first node: 54 | ```erlang 55 | make demo_run node_name='mxnode01@127.0.0.1' 56 | 57 | (mxnode01@127.0.0.1)1> application:start(mx). 58 | ``` 59 | 60 | and the second one: 61 | 62 | ```erlang 63 | make demo_run node_name='mxnode02@127.0.0.1' 64 | 65 | (mxnode02@127.0.0.1)1> application:load(mx). 66 | %% Set via environment value of **'master'** to run it in slave mode. 67 | (mxnode02@127.0.0.1)2> application:set_env(mx, master, 'mxnode01@127.0.0.1'). 68 | (mxnode02@127.0.0.1)3> application:start(mx). 69 | ``` 70 | 71 | Call **mx:nodes()** to get the list of mx cluster nodes. 72 | 73 | ```erlang 74 | (mxnode01@127.0.0.1)2> mx:nodes(). 75 | ['mxnode01@127.0.0.1','mxnode02@127.0.0.1'] 76 | ``` 77 | 78 | ## Mnesia custom directory (optional) 79 | 80 | Create dir, for example, **/usr/local/var/lib/mx/mnesia/** with correct (Read/Write) permissions. 81 | 82 | Set option `mnesia_base_dir` with this **directory** in `sys.config`: 83 | 84 | ``` 85 | {mx, [ 86 | {mnesia_base_dir, "/usr/local/var/lib/mx/mnesia/"} 87 | ]}, 88 | ``` 89 | 90 | Or set the value of configuration parameter `mnesia_base_dir` for **mx**: 91 | 92 | ```erlang 93 | make demo_run node_name='mxnode01@127.0.0.1' 94 | 95 | (mxnode01@127.0.0.1)1> application:load(mx). 96 | (mxnode01@127.0.0.1)1> application:set_env(mx, mnesia_base_dir, "/usr/local/var/lib/mx/mnesia/"). 97 | (mxnode01@127.0.0.1)1> application:start(mx). 98 | ``` 99 | 100 | So, mnesia data will be located: 101 | ``` 102 | /usr/local/var/lib/mx/mnesia/mxnode01@127.0.0.1 %% node name 103 | ``` 104 | 105 | ## Examples 106 | 107 | ```erlang 108 | % Client has higest priority by default (priority = 1) 109 | {clientkey, Client1Key} = mx:register(client, "Client1"), 110 | {clientkey, Client2Key} = mx:register(client, "Client2", [{priority, 8}]), 111 | {clientkey, Client3Key} = mx:register(client, "Client3", [{async, false}, {defer, true}]), 112 | {clientkey, Client4Key} = mx:register(client, "Client4"), 113 | 114 | % register channel with default priority (5) 115 | {channelkey, Channel1Key} = mx:register(channel, "Channel1", Client4Key), 116 | ok = mx:subscribe(Client1Key, Channel1Key), 117 | % just for example try to subscribe one more time 118 | {already_subscribed, Client1Key} = mx:subscribe(Client1Key, Channel1Key), 119 | 120 | ok = mx:subscribe(Client2Key, Channel1Key), 121 | ok = mx:subscribe(Client3Key, Channel1Key), 122 | 123 | mx:send(Channel1Key, "Hello, Channel1!"). 124 | 125 | % register pool with default balance method is 'rr' - round robin 126 | % default priority (5) 127 | {poolkey, Pool1Key} = mx:register(pool, "Pool1", Client4Key), 128 | mx:join(Client1Key, Pool1Key), 129 | mx:join(Client2Key, Pool1Key), 130 | mx:join(Client3Key, Pool1Key), 131 | 132 | % create lowest priority channel and pool by Client2 133 | {channelkey, LPCh1} = mx:register(channel, "LP Channel1", Client2Key, [{priority, 10}]), 134 | {poolkey, LPPl1} = mx:register(pool, "LP Pool1", Client2Key, [{priority, 10}]), 135 | 136 | % create highest priority channel and pool by Client3 137 | {channelkey, HPCh1} = mx:register(channel, "HP Channel1", Client2Key, [{priority, 1}]), 138 | {poolkey, HPPl1} = mx:register(pool, "HP Pool1", Client2Key, [{priority, 1}]), 139 | 140 | % high priority pool with 'hash' balance 141 | {poolkey, HP_Hash_Pl} = mx:register(pool, "HP Pool (hash)", Client2Key, [{priority, 1}, {balance, hash}]), 142 | 143 | % pool with random balance 144 | {poolkey, Rand_Pl} = mx:register(pool, "Pool (random)", Client2Key, [{balance, random}]), 145 | 146 | ``` 147 | 148 | ## API 149 | 150 | ### local usage 151 | 152 | * Create client/channel/pool 153 | 154 | ```erlang 155 | mx:register(client, Name) 156 | mx:register(client, Name, Opts) 157 | ``` 158 | Name - list or binary 159 | Opts - proplists 160 | 161 | returns: `{clientkey, Key} | {duplicate, Key}` 162 | 163 | Key - binary 164 | 165 | ```erlang 166 | mx:register(channel, Name, ClientKey) 167 | mx:register(channel, Name, ClientKey, Opts) 168 | ``` 169 | Name - list or binary 170 | Opts - proplists 171 | ClientKey - binary 172 | 173 | returns: `{channelkey, Key} | {duplicate, Key}` 174 | 175 | Key - binary 176 | 177 | ```erlang 178 | mx:register(pool, Name, ClientKey)** 179 | mx:register(pool, Name, ClientKey, Opts)** 180 | ``` 181 | Name - list or binary 182 | Opts - proplists 183 | ClientKey - binary 184 | 185 | returns: `{poolkey, Key} | {duplicate, Key}` 186 | 187 | Key - binary 188 | 189 | * Delete client/channel/pool 190 | ```erlang 191 | mx:unregister(Key) 192 | ``` 193 | 194 | * Set online/offline state 195 | ```erlang 196 | mx:online(ClientKey, Pid) 197 | mx:offline(ClientKey) 198 | ``` 199 | 200 | * Work with channel/pool 201 | ```erlang 202 | mx:subscribe(Key, Channel) 203 | mx:unsubscribe(Key, Channel) 204 | ``` 205 | Key - binary (ClientKey, ChannelKey, PoolKey) 206 | Channel - channel name or channel key 207 | 208 | ```erlang 209 | mx:join(Key, Pool) 210 | mx:leave(Key, Pool) 211 | ``` 212 | Key - binary (ClientKey, ChannelKey, PoolKey) 213 | Pool - pool name or pool key 214 | 215 | * Set options for client/channel/pool 216 | ```erlang 217 | mx:set(Key, Opts) 218 | ``` 219 | Key - binary (ClientKey, ChannelKey, PoolKey) 220 | Opts - proplists 221 | 222 | * Sending message 223 | ```erlang 224 | mx:send(ClientKey, Message) 225 | mx:send(ChannelKey, Message) 226 | mx:send(PoolKey, Message) 227 | ``` 228 | 229 | * Owning Pool/Channel 230 | ```erlang 231 | mx:own(Key, ClientKey) 232 | ``` 233 | Key - binary (ChannelKey, PoolKey) 234 | 235 | orphan Pool/Channel will unregister automaticaly 236 | 237 | ```erlang 238 | mx:abandon(Key, ClientKey) 239 | ``` 240 | Key - binary (ChannelKey, PoolKey) 241 | 242 | 243 | * Clear deferred messages 244 | ```erlang 245 | mx:flush(Key) 246 | ``` 247 | Key - binary (ClientKey, ChannelKey, PoolKey) 248 | Key = all - truncate the 'deferred' table 249 | 250 | * Clear all MX tables 251 | ```erlang 252 | mx:clear_all_tables() 253 | ``` 254 | 255 | * Info 256 | 257 | show MX cluster nodes 258 | ```erlang 259 | mx:nodes() 260 | ``` 261 | 262 | show full information about the client 263 | ```erlang 264 | mx:info(Key) 265 | ``` 266 | Key - binary (ClientKey, ChannelKey, PoolKey) 267 | 268 | show the only `Name` property from the information list about the client 269 | ```erlang 270 | mx:info(Key, Name) 271 | ``` 272 | Key - binary (ClientKey, ChannelKey, PoolKey) 273 | Name - field name 274 | 275 | show the list of Clients are subscribed/joined to. 276 | ```erlang 277 | mx:relation(Key) 278 | ``` 279 | Key - binary (ChannelKey, PoolKey) 280 | 281 | 282 | ### remote usage 283 | 284 | ```erlang 285 | gen_server:call(MX, Message) 286 | ``` 287 | where the `Message` is one of the listed values below: 288 | 289 | - `{register_client, Client}` 290 | - `{register_client, Client, Opts}` 291 | - `{register_channel, ChannelName, ClientKey}` 292 | - `{register_channel, ChannelName, ClientKey, Opts}` 293 | - `{register_pool, PoolName, ClientKey}` 294 | - `{register_pool, PoolName, ClientKey, Opts}` 295 | - `{unregister, Key}` 296 | - `{online, ClientKey, Pid}` 297 | - `{offline, ClientKey}` 298 | - `{subscribe, Client, To}` 299 | - `{unsubscribe, Client, From}` 300 | - `{join, Client, To}` 301 | - `{leave, Client, From}` 302 | - `{send, To, Message}` 303 | - `{own, Key, ClientKey}` 304 | - `{abandon, Key, ClientKey}` 305 | - `{info, Key}` 306 | - `{info, {Key, Name}}` 307 | - `{relation, Key}` 308 | - `{set, Key, Opts}` 309 | - `nodes` 310 | - `clear_all_tables` 311 | 312 | ```erlang 313 | > (mxnode02@127.0.0.1)2> gen_server:call({mx, 'mxnode01@127.0.0.1'}, nodes). 314 | ['mxnode02@127.0.0.1','mxnode01@127.0.0.1'] 315 | ``` 316 | 317 | ## Testing 318 | 319 | There are only common tests (CT) are implemented with some limited set of cases 320 | 321 | ### Direct sending tests. 322 | Sequentualy running: 323 | 324 | * [x] 1 reciever - 1 message 325 | * [x] 1 reciever - 1000 messages 326 | * [x] 1000 recievers - 1 messages 327 | * [x] 1000 recievers - 100 messages 328 | 329 | Parallel: 330 | 331 | * [x] 1 reciever - 1000 messages (3 processes) 332 | * [x] 1000 recievers - 1 messages (3 processes) 333 | 334 | 335 | ### Channel tests (pub/sub). 336 | 337 | Sequentualy running: 338 | 339 | * [x] 1 subscriber (subscriber) recieves 1 messages 340 | * [x] 1 subscriber - 1000 messages 341 | * [x] 1000 subscribers - 1 messages 342 | * [x] 1000 subscribers - 10 messages 343 | 344 | Parallel: 345 | 346 | * [ ] 1000 subscribers - 10 messages (10 processes) 347 | 348 | Priority delivering: 349 | * 1 subscriber recieves 1000 messages with 10 different priorities: 350 | * [ ] 100 messages with priority 1 351 | * [ ] 100 messages with priority 2 352 | * [ ] ... 353 | * [ ] 100 messages with priority 10 354 | 355 | 356 | ### Pool tests (worker queue) 357 | 358 | Sequentualy running: 359 | 360 | * [ ] 1 client sends 1 messages to 1 worker 361 | * [ ] 1 client - 1000 messages - 2 workers (round robin) 362 | * [ ] 1 client - 1000 messages - 2 workers (hash) 363 | * [ ] 1 client - 1000 messages - 2 workers (random) 364 | * [ ] 1000 clients - 1 messages - 4 workers 365 | * [ ] 1000 clients - 1000 messages - 4 workers 366 | 367 | Parallel: _(not implemented)_ 368 | * [ ] 1000 clients - 10 messages (10 processes) 369 | 370 | ### Run the testing 371 | 1. Run MX application as standalone application 372 | 373 | ```shell 374 | $ make run 375 | ``` 376 | 377 | 2. Run "Common Tests" 378 | 379 | ```shell 380 | $ make ct 381 | ``` 382 | -------------------------------------------------------------------------------- /conf/sys.config: -------------------------------------------------------------------------------- 1 | [ 2 | {mx, [ 3 | % queue limits 4 | {queue_length_limit, 100000}, 5 | {queue_low_threshold, 0.6}, % 60% 6 | {queue_high_threshold, 0.8} % 80% 7 | %, {mnesia_base_dir, "/usr/local/var/lib/mx/mnesia/"} 8 | %, {master, "mxnode01@127.0.0.1"} %% or "${MASTER_NODE}" 9 | ]}, 10 | {lager, [ 11 | {log_root, "/tmp/"}, 12 | {handlers, [ 13 | {lager_console_backend, [{level, info}]}, 14 | {lager_file_backend, [{file, "mx.error.log"}, {level, error}]}, 15 | {lager_file_backend, [{file, "mx.console.log"}, {level, debug}]} 16 | ]} 17 | ]}, 18 | {mnesia, [ 19 | {dump_log_write_threshold, 50000} 20 | ]} 21 | ]. 22 | -------------------------------------------------------------------------------- /conf/vm.args: -------------------------------------------------------------------------------- 1 | ## Name of the node 2 | -name mxnode01@127.0.0.1 3 | 4 | ## Cookie for distributed erlang 5 | -setcookie devcook 6 | 7 | #+pc unicode 8 | 9 | ## Enable kernel poll and a few async threads 10 | +K true 11 | +A 5 12 | 13 | ## Increase number of concurrent ports/sockets 14 | ##-env ERL_MAX_PORTS 4096 15 | 16 | ## Tweak GC to run more often 17 | ##-env ERL_FULLSWEEP_AFTER 10 -------------------------------------------------------------------------------- /include/log.hrl: -------------------------------------------------------------------------------- 1 | -ifndef(LOG_HRL). 2 | -define(LOG_HRL, true). 3 | 4 | -define(CURRENT_FUN_NAME, element(2, element(2, process_info(self(), current_function)))). 5 | -define(CURRENT_FUN_NAME_STR, atom_to_list(?CURRENT_FUN_NAME)). 6 | 7 | -define(DBG(Format), lager:log(debug, self(), "~p:~p: " ++ Format, [?MODULE, ?LINE])). 8 | -define(DBG(Format, Data), lager:log(debug, self(), "~p:~p: " ++ Format, [?MODULE, ?LINE | Data])). 9 | 10 | -define(INFO(Format), lager:log(info, self(), "~p:~p: " ++ Format, [?MODULE, ?LINE])). 11 | -define(INFO(Format, Data), lager:log(info, self(), "~p:~p: " ++ Format, [?MODULE, ?LINE | Data])). 12 | -define(LOG(Format), lager:log(info, self(), "~p:~p: " ++ Format, [?MODULE, ?LINE])). 13 | -define(LOG(Format, Data), lager:log(info, self(), "~p:~p: " ++ Format, [?MODULE, ?LINE | Data])). 14 | 15 | -define(NOTICE(Format), lager:log(notice, self(), "~p:~p: " ++ Format, [?MODULE, ?LINE])). 16 | -define(NOTICE(Format, Data), lager:log(notice, self(), "~p:~p: " ++ Format, [?MODULE, ?LINE | Data])). 17 | 18 | -define(WRN(Format), lager:log(warning, self(), "~p:~p: " ++ Format, [?MODULE, ?LINE])). 19 | -define(WRN(Format, Data), lager:log(warning, self(), "~p:~p: " ++ Format, [?MODULE, ?LINE | Data])). 20 | 21 | -define(ERR(Format), lager:log(error, self(), "~p:~p: " ++ Format, [?MODULE, ?LINE])). 22 | -define(ERR(Format, Data), lager:log(error, self(), "~p:~p: " ++ Format, [?MODULE, ?LINE | Data])). 23 | 24 | -define(CRITICAL(Format), lager:log(critical, self(), "~p:~p: " ++ Format, [?MODULE, ?LINE])). 25 | -define(CRITICAL(Format, Data), lager:log(critical, self(), "~p:~p: " ++ Format, [?MODULE, ?LINE | Data])). 26 | 27 | -define(ALERT(Format), lager:log(alert, self(), "~p:~p: " ++ Format, [?MODULE, ?LINE])). 28 | -define(ALERT(Format, Data), lager:log(alert, self(), "~p:~p: " ++ Format, [?MODULE, ?LINE | Data])). 29 | 30 | -define(PANIC(Format), lager:log(emergency, self(), "~p:~p: " ++ Format, [?MODULE, ?LINE])). 31 | -define(PANIC(Format, Data), lager:log(emergency, self(), "~p:~p: " ++ Format, [?MODULE, ?LINE | Data])). 32 | 33 | -define(LOG_STACK, ?LOG("Stack: ~p", [try throw(42) catch 42 -> erlang:get_stacktrace() end])). 34 | 35 | -define(FIXME, lager:log(emergency, self(), "FIXME[at ~p:~p]!!!", [?MODULE, ?LINE])). 36 | -define(FIXME(Format), lager:log(emergency, self(), "FIXME[at ~p:~p]: " ++ Format ++ "!!!", [?MODULE, ?LINE])). 37 | -define(FIXME(Format, Data), lager:log(emergency, self(), "FIXME[at ~p:~p]: " ++ Format ++ "!!!", [?MODULE, ?LINE | Data])). 38 | 39 | -endif. 40 | -------------------------------------------------------------------------------- /include/mx.hrl: -------------------------------------------------------------------------------- 1 | %% Copyright (C) 2015 Taras Halturin 2 | %% 3 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 4 | %% of this software and associated documentation files (the "Software"), to deal 5 | %% in the Software without restriction, including without limitation the rights 6 | %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | %% copies of the Software, and to permit persons to whom the Software is 8 | %% furnished to do so, subject to the following conditions: 9 | %% 10 | %% The above copyright notice and this permission notice shall be included in 11 | %% all copies or substantial portions of the Software. 12 | %% 13 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | %% THE SOFTWARE. 20 | %% 21 | 22 | -ifndef(MX_HRL). 23 | -define(MX_HRL, true). 24 | 25 | -define(MXQUEUE_PRIO_SRT, 1). % soft realtime 26 | -define(MXQUEUE_PRIO_NORMAL, 5). 27 | -define(MXQUEUE_PRIO_LOW, 10). 28 | 29 | -define(MXCLIENT, mx_table_client). 30 | -define(MXCHANNEL, mx_table_channel). 31 | -define(MXPOOL, mx_table_pool). 32 | -define(MXDEFER, mx_table_defer). 33 | -define(MXRELATION, mx_table_relation). 34 | -define(MXKV, mx_table_kv). 35 | 36 | % '%system' 37 | -define(MXSYSTEM_CHANNEL, <<35,1,205,204,237,249,21,234,116,63,125,148,219,82,19,237,212>>). 38 | % '%clients' 39 | -define(MXSYSTEM_CLIENTS_CHANNEL, <<35,218,100,92,158,250,171,65,140,165,196,6,41,174,67,121,214>>). 40 | % '%queues' 41 | -define(MXSYSTEM_QUEUES_CHANNEL, <<35,44,214,227,18,198,49,63,136,180,212,33,133,149,223,21,136>>). 42 | 43 | 44 | -define(MX_SEND_TIMEOUT, 5000). % sync sending timeout 45 | 46 | -define(MXTABLES, 47 | [{?MXCLIENT, [{type, set}, 48 | {disc_copies, [node()]}, 49 | {record_name, ?MXCLIENT}, 50 | {attributes, record_info(fields, ?MXCLIENT)} ]}, 51 | 52 | {?MXCHANNEL,[{type, set}, 53 | {disc_copies, [node()]}, 54 | {record_name, ?MXCHANNEL}, 55 | {attributes, record_info(fields, ?MXCHANNEL)} ]}, 56 | 57 | {?MXPOOL,[{type, set}, 58 | {disc_copies, [node()]}, 59 | {record_name, ?MXPOOL}, 60 | {attributes, record_info(fields, ?MXPOOL)} ]}, 61 | 62 | {?MXRELATION,[{type, set}, 63 | {disc_copies, [node()]}, 64 | {record_name, ?MXRELATION}, 65 | {attributes, record_info(fields, ?MXRELATION)} ]}, 66 | 67 | {?MXDEFER, [{type, bag}, 68 | {disc_copies, [node()]}, 69 | {record_name, ?MXDEFER}, 70 | {attributes, record_info(fields, ?MXDEFER)} ]}, 71 | 72 | {?MXKV,[{type, set}, 73 | {disc_copies, [node()]}, 74 | {record_name, ?MXKV}, 75 | {attributes, record_info(fields, ?MXKV)} ]} ]). 76 | 77 | -record(?MXCLIENT, { 78 | key :: binary(), % <<$*,Md5Hash/binary>> (<<42,...>>) 79 | name :: binary(), 80 | related :: list(), % subscribed/joined to 81 | ownerof :: list(), % list of keys (channels, pools) 82 | handler :: pid() | offline, % who manage the client (for recieving messages) 83 | async = true :: boolean(), % send async or wait for 'ok' message 84 | defer = false :: boolean(), % defer message when the handler is not available (offline) 85 | monitor = false :: boolean(), % generate 'on/off' event message to the $system/$clients channel if its 'true' 86 | comment = "Client info" :: list() 87 | }). 88 | 89 | -record(?MXCHANNEL, { 90 | key :: binary(), % <<$#,Md5Hash/binary>> (<<35,...>>) 91 | name :: binary(), 92 | related :: list(), % subscribed/joined to. in case of tree-like subscriptions (example: pool of channels) 93 | owners :: list(), % owners (who can publish here) 94 | priority = 5 :: non_neg_integer(), % priority 95 | defer = true :: boolean(), % deferrable. defer message when exceed the queue limit 96 | comment = "Channel info" :: list() 97 | }). 98 | 99 | -record(?MXPOOL, { 100 | key :: binary(), % <<$@,Md5Hash/binary>> (<<64,...>>) 101 | name :: binary(), 102 | related :: list(), % subscribed/joined to. in case of tree-like pooling (example: channel of pools) 103 | owners :: list(), 104 | balance = rr :: rr | hash | random, % balance type 105 | priority = 5 :: non_neg_integer(), 106 | defer = true :: boolean(), % deferrable 107 | comment = "Pool info" :: list() 108 | }). 109 | 110 | -record(?MXRELATION, { 111 | key :: binary(), % key of channel|pool 112 | related = [] :: list() % list of client|pool|channel keys 113 | }). 114 | 115 | -record(?MXDEFER, { 116 | priority :: non_neg_integer(), 117 | to :: binary(), 118 | message, 119 | parent = none, 120 | fails = 0 :: non_neg_integer() % count of sending fails 121 | }). 122 | 123 | -record(?MXKV, { 124 | key, 125 | value 126 | }). 127 | 128 | -endif. % MX_HRL 129 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {minimum_otp_vsn, "18.0"}. 2 | 3 | {validate_app_modules, true}. 4 | {cover_enabled, true}. 5 | {reset_after_eunit, true}. 6 | 7 | {deps, [ 8 | {gproc, {git, "git://github.com/uwiger/gproc.git", {tag, "0.8.0"}}}, 9 | {lager, {git, "git://github.com/erlang-lager/lager.git", {tag, "3.6.8"}}}, 10 | {sync, {git, "git://github.com/rustyio/sync.git", {branch, "master"}}} 11 | ]}. 12 | 13 | {erl_opts, [{parse_transform, lager_transform}]}. 14 | 15 | {ct_opts, []}. 16 | 17 | % Surefire reports for EUnit (Format used by Maven and Atlassian Bamboo for example 18 | % to integrate test results). 19 | {eunit_opts, [{report, {eunit_surefire, [{dir, "_build/test"}]}}]}. 20 | 21 | {edoc_opts, [{dir, "_build/edoc"}]}. 22 | 23 | 24 | {relx, [ 25 | {default_release, {mx_dev, "1.0.0"}}, 26 | 27 | {release, {mx_dev, "1.0.0"}, [ 28 | sync, 29 | observer, 30 | wx, 31 | runtime_tools, 32 | debugger, 33 | inets, 34 | {mnesia, load}, 35 | lager, 36 | gproc, 37 | mx 38 | ]}, 39 | 40 | {release, {mx, "1.0.0"}, [ 41 | inets, 42 | {mnesia, load}, 43 | lager, 44 | gproc, 45 | mx 46 | ]}, 47 | 48 | {dev_mode, true}, 49 | {include_erts, false}, 50 | {extended_start_script, true}, 51 | {vm_args, "conf/vm.args"}, 52 | {sys_config, "conf/sys.config"} 53 | ]}. 54 | 55 | {profiles, [ 56 | {prod, [ 57 | {erl_opts, [ 58 | no_debug_info, 59 | warnings_as_errors 60 | ]}, 61 | {relx, [{dev_mode, false}, 62 | {include_erts, true}, 63 | {extended_start_script, true}, 64 | {include_src, false}]} 65 | ]}, 66 | {test, [ 67 | {erl_opts, [ 68 | debug_info, 69 | {parse_transform, lager_transform}, 70 | nowarn_unused_vars 71 | ]} 72 | ]} 73 | ]}. 74 | 75 | {plugins, [ 76 | {rebar3_run, {git, "git://github.com/tsloughter/rebar3_run.git", {branch, "master"}}}, 77 | {pc, {git, "git://github.com/blt/port_compiler.git", {tag, "1.6.0"}}} 78 | ]}. 79 | 80 | % {commands, [ 81 | % {distclean, "rm -rf .rebar3 _build _checkouts/*/ebin ebin log"}, 82 | % {sync, "git fetch upstream && git merge upstream/master"} 83 | % ]}. 84 | 85 | 86 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | [{<<"goldrush">>, 2 | {git,"git://github.com/basho/goldrush.git", 3 | {ref,"8f1b715d36b650ec1e1f5612c00e28af6ab0de82"}}, 4 | 0}, 5 | {<<"gproc">>, 6 | {git,"git://github.com/uwiger/gproc.git", 7 | {ref,"ce7397809aca0d6eb3aac6db65953752e47fb511"}}, 8 | 0}, 9 | {<<"lager">>, 10 | {git,"git://github.com/erlang-lager/lager.git", 11 | {ref,"1c0ab772df0535d46a4467857954ee4dcf7f3078"}}, 12 | 0}, 13 | {<<"sync">>, 14 | {git,"git://github.com/rustyio/sync.git", 15 | {ref,"d7ded2d221fb542b3417cffe77163206613daacb"}}, 16 | 0}]. 17 | -------------------------------------------------------------------------------- /rebar3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/halturin/mx/4ccce0f7233aea255576b9d01c0114d5ab4b199f/rebar3 -------------------------------------------------------------------------------- /src/mx.app.src: -------------------------------------------------------------------------------- 1 | {application, mx, 2 | [ 3 | {description, "OTP Message Broker"}, 4 | {vsn, git}, 5 | {registered, []}, 6 | {applications, [ 7 | kernel, 8 | stdlib, 9 | gproc 10 | ]}, 11 | {mod, { mx_app, []}}, 12 | {modules, []}, 13 | {included_applications, []}, 14 | {maintainers, []}, 15 | {licenses, []}, 16 | {links, []} 17 | ]}. 18 | -------------------------------------------------------------------------------- /src/mx.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (C) 2015 Taras Halturin 2 | %% 3 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 4 | %% of this software and associated documentation files (the "Software"), to deal 5 | %% in the Software without restriction, including without limitation the rights 6 | %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | %% copies of the Software, and to permit persons to whom the Software is 8 | %% furnished to do so, subject to the following conditions: 9 | %% 10 | %% The above copyright notice and this permission notice shall be included in 11 | %% all copies or substantial portions of the Software. 12 | %% 13 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | %% THE SOFTWARE. 20 | %% 21 | 22 | -module(mx). 23 | 24 | -behaviour(gen_server). 25 | 26 | -compile({no_auto_import,[register/2, nodes/0, monitor_node/2]}). 27 | 28 | -export([start_link/0]). 29 | 30 | -export([init/1, 31 | handle_call/3, 32 | handle_cast/2, 33 | handle_info/2, 34 | terminate/2, 35 | code_change/3]). 36 | 37 | -export([register/2, 38 | register/3, 39 | register/4, 40 | unregister/1, 41 | subscribe/2, 42 | unsubscribe/2, 43 | offline/1, 44 | online/2, 45 | join/2, 46 | leave/2, 47 | send/2, 48 | send/3, 49 | info/1, 50 | info/2, 51 | relation/1, 52 | set/2, 53 | own/2, 54 | abandon/2, 55 | flush/1, 56 | nodes/0, 57 | clear_all_tables/0 58 | ]). 59 | 60 | %% records 61 | -record(state, {config :: list()}). 62 | 63 | %% includes 64 | -include_lib("include/log.hrl"). 65 | -include_lib("include/mx.hrl"). 66 | 67 | register(client, Client, Opts) when is_binary(Client) -> 68 | case call({register_client, Client, Opts}) of 69 | {M, ClientKey} when M == clientkey; M == duplicate -> 70 | Node = proplists:get_value(handler, Opts, self()), 71 | case catch erlang:node(Node) of 72 | {'EXIT', _} -> 73 | pass; 74 | N -> 75 | monitor_node(N, ClientKey) 76 | end, 77 | {M, ClientKey}; 78 | E -> 79 | E 80 | end; 81 | 82 | register(X, Y, Opts) when is_list(Y) -> 83 | register(X, list_to_binary(Y), Opts); 84 | 85 | register(channel, X, <<$*,_/binary>> = Y) -> 86 | register(channel, X, Y, []); 87 | register(pool, X, <<$*,_/binary>> = Y) -> 88 | register(pool, X, Y, []). 89 | 90 | register(channel, Channel, <<$*,_/binary>> = ClientKey, Opts) when is_binary(Channel) -> 91 | call({register_channel, Channel, ClientKey, Opts}); 92 | register(pool, Pool, <<$*,_/binary>> = ClientKey, Opts) when is_binary(Pool) -> 93 | call({register_pool, Pool, ClientKey, Opts}); 94 | register(X, Y, <<$*,_/binary>> = Z, Opts) when is_list(Y) -> 95 | register(X, list_to_binary(Y), Z, Opts). 96 | 97 | register(client, X) -> 98 | register(client, X, []). 99 | 100 | 101 | unregister(Key) -> 102 | call({unregister, Key}). 103 | 104 | offline(<<$*,_/binary>> = ClientKey) -> 105 | cast({offline, ClientKey}); 106 | offline(_Key) -> 107 | unknown_client. 108 | 109 | online(<<$*,_/binary>> = ClientKey, Pid) when is_pid(Pid) -> 110 | cast({online, ClientKey}); 111 | online(_Key, _Pid) -> 112 | unknown_client. 113 | 114 | subscribe(Key, Channel) when is_list(Channel) -> 115 | ChannelHash = erlang:md5(list_to_binary(Channel)), 116 | subscribe(Key, <<$#, ChannelHash/binary>>); 117 | 118 | subscribe(Key, <<$#, _/binary>> = ChannelKey) -> 119 | case mnesia:dirty_read(?MXCHANNEL, ChannelKey) of 120 | [] -> 121 | unknown_channel; 122 | [_Ch] -> 123 | call({relate, Key, ChannelKey}) 124 | end; 125 | 126 | subscribe(Key, Channel) when is_binary(Channel) -> 127 | ChannelHash = erlang:md5(Channel), 128 | subscribe(Key, <<$#, ChannelHash/binary>>). 129 | 130 | unsubscribe(Key, Channel) when is_list(Channel) -> 131 | ChannelHash = erlang:md5(list_to_binary(Channel)), 132 | unsubscribe(Key, <<$#, ChannelHash/binary>>); 133 | 134 | unsubscribe(Key, <<$#, _/binary>> = ChannelKey) -> 135 | case mnesia:dirty_read(?MXCHANNEL, ChannelKey) of 136 | [] -> 137 | unknown_channel; 138 | [_Ch] -> 139 | call({unrelate, Key, ChannelKey}) 140 | end; 141 | 142 | unsubscribe(Key, Channel) when is_binary(Channel) -> 143 | ChannelHash = erlang:md5(Channel), 144 | unsubscribe(Key, <<$#, ChannelHash/binary>>). 145 | 146 | 147 | join(Key, Pool) when is_list(Pool) -> 148 | PoolHash = erlang:md5(list_to_binary(Pool)), 149 | join(Key, <<$@, PoolHash/binary>>); 150 | 151 | join(Key, <<$@, _/binary>> = PoolKey) -> 152 | case mnesia:dirty_read(?MXPOOL, PoolKey) of 153 | [] -> 154 | unknown_pool; 155 | [_P] -> 156 | call({relate, Key, PoolKey}) 157 | end; 158 | 159 | join(Key, Pool) when is_binary(Pool) -> 160 | PoolHash = erlang:md5(Pool), 161 | join(Key, <<$@, PoolHash/binary>>). 162 | 163 | leave(Key, Pool) when is_list(Pool) -> 164 | PoolHash = erlang:md5(list_to_binary(Pool)), 165 | leave(Key, <<$@, PoolHash/binary>>); 166 | 167 | leave(Key, <<$@, _/binary>> = PoolKey) -> 168 | case mnesia:dirty_read(?MXPOOL, PoolKey) of 169 | [] -> 170 | unknown_pool; 171 | [_P] -> 172 | call({unrelate, Key, PoolKey}) 173 | end; 174 | 175 | leave(Key, Pool) when is_binary(Pool) -> 176 | PoolHash = erlang:md5(Pool), 177 | leave(Key, <<$@, PoolHash/binary>>). 178 | 179 | info(Key) -> 180 | call({info, Key}). 181 | 182 | info(Key, Name) -> 183 | call({info, {Key, Name}}). 184 | 185 | relation(Key) -> 186 | call({relation, Key}). 187 | 188 | set(Key, Opts) -> 189 | call({set, Key, Opts}). 190 | 191 | send(To, Message) -> 192 | send(To, Message, []). 193 | 194 | send(<<$*, _/binary>> = ClientKeyTo, Message, Opts) -> 195 | case mnesia:dirty_read(?MXCLIENT, ClientKeyTo) of 196 | [] -> 197 | unknown_client; 198 | [ClientTo] -> 199 | cast({send, ClientTo, Message, Opts}) 200 | end; 201 | 202 | send(<<$#, _/binary>> = ChannelKeyTo, Message, Opts) -> 203 | case mnesia:dirty_read(?MXCHANNEL, ChannelKeyTo) of 204 | [] -> 205 | unknown_channel; 206 | [ChannelTo] -> 207 | cast({send, ChannelTo, Message, Opts}) 208 | end; 209 | 210 | send(<<$@, _/binary>> = PoolKeyTo, Message, Opts) -> 211 | case mnesia:dirty_read(?MXPOOL, PoolKeyTo) of 212 | [] -> 213 | unknown_pool; 214 | [PoolTo] -> 215 | cast({send, PoolTo, Message, Opts}) 216 | end; 217 | 218 | send(_, _, _) -> 219 | unknown_receiver. 220 | 221 | own(Key, <<$*, _/binary>> = ClientKey) -> 222 | case mnesia:dirty_read(?MXCLIENT, ClientKey) of 223 | [] -> 224 | unknown_client; 225 | [Client] -> 226 | call({own, Key, Client}) 227 | end. 228 | 229 | abandon(Key, <<$*, _/binary>> = ClientKey) -> 230 | case mnesia:dirty_read(?MXCLIENT, ClientKey) of 231 | [] -> 232 | unknown_client; 233 | [Client] -> 234 | call({abandon, Key, Client}) 235 | end. 236 | 237 | 238 | flush(all) -> 239 | mnesia:clear_table(?MXDEFER); 240 | 241 | flush(Key) -> 242 | cast({flush, Key}). 243 | 244 | nodes() -> 245 | mx_mnesia:nodes(). 246 | 247 | clear_all_tables() -> 248 | mx_mnesia:clear_all_tables(). 249 | 250 | %%-------------------------------------------------------------------- 251 | %% @doc 252 | %% Starts the server 253 | %% 254 | %% @spec start_link() -> {ok, Pid} | ignore | {error, Error} 255 | %% @end 256 | %%-------------------------------------------------------------------- 257 | start_link() -> 258 | gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). 259 | 260 | %%% gen_server callbacks 261 | %%%=================================================================== 262 | 263 | %%-------------------------------------------------------------------- 264 | %% @private 265 | %% @doc 266 | %% Initializes the server 267 | %% 268 | %% @spec init(Args) -> {ok, State} | 269 | %% {ok, State, Timeout} | 270 | %% ignore | 271 | %% {stop, Reason} 272 | %% @end 273 | %%-------------------------------------------------------------------- 274 | init([]) -> 275 | process_flag(trap_exit, true), 276 | ok = wait_for_mnesia(5000), % wait for mnesia 5 sec 277 | erlang:send_after(0, self(), {'$gen_cast', requeue}), 278 | 279 | % handle monitoring remote nodes by mx 280 | mnesia:subscribe(system), 281 | 282 | init_mxscheme(), 283 | 284 | {ok, #state{config = []}}. 285 | 286 | %%-------------------------------------------------------------------- 287 | %% @private 288 | %% @doc 289 | %% Handling call messages 290 | %% 291 | %% @spec handle_call(Request, From, State) -> 292 | %% {reply, Reply, State} | 293 | %% {reply, Reply, State, Timeout} | 294 | %% {noreply, State} | 295 | %% {noreply, State, Timeout} | 296 | %% {stop, Reason, Reply, State} | 297 | %% {stop, Reason, State} 298 | %% @end 299 | %%-------------------------------------------------------------------- 300 | handle_call({register_client, Client, Opts}, {Pid, _}, State) -> 301 | case proplists:is_defined(handler, Opts) of 302 | true -> 303 | R = register(client, Client, Opts); 304 | false -> 305 | Opts1 = [{handler, Pid} | Opts], 306 | R = register(client, Client, Opts1) 307 | end, 308 | {reply, R, State}; 309 | 310 | handle_call({register_client, Client}, {Pid, _}, State) -> 311 | R = register(client, Client, [{handler, Pid}]), 312 | {reply, R, State}; 313 | 314 | handle_call({register_channel, ChannelName, ClientKey, Opts}, _From, State) -> 315 | R = register(channel, ChannelName, ClientKey, Opts), 316 | {reply, R, State}; 317 | handle_call({register_channel, ChannelName, ClientKey}, _From, State) -> 318 | R = register(channel, ChannelName, ClientKey), 319 | {reply, R, State}; 320 | 321 | handle_call({register_pool, PoolName, ClientKey, Opts}, _From, State) -> 322 | R = register(pool, PoolName, ClientKey, Opts), 323 | {reply, R, State}; 324 | handle_call({register_pool, PoolName, ClientKey}, _From, State) -> 325 | R = register(pool, PoolName, ClientKey), 326 | {reply, R, State}; 327 | 328 | handle_call({unregister, Key}, _From, State) -> 329 | R = call({unregister, Key}), 330 | {reply, R, State}; 331 | 332 | handle_call({offline, ClientKey}, _From, State) -> 333 | R = offline(ClientKey), 334 | {reply, R, State}; 335 | 336 | handle_call({online, ClientKey, Pid}, _From, State) -> 337 | R = online(ClientKey, Pid), 338 | {reply, R, State}; 339 | 340 | handle_call({subscribe, Client, To}, _From, State) -> 341 | R = subscribe(Client, To), 342 | {reply, R, State}; 343 | 344 | handle_call({unsubscribe, Client, From}, _From, State) -> 345 | R = unsubscribe(Client, From), 346 | {reply, R, State}; 347 | 348 | handle_call({join, Client, To}, _From, State) -> 349 | R = join(Client, To), 350 | {reply, R, State}; 351 | 352 | handle_call({leave, Client, From}, _From, State) -> 353 | R = leave(Client, From), 354 | {reply, R, State}; 355 | 356 | handle_call({info, {Key, Name}}, _From, State) -> 357 | R = info(Key, Name), 358 | {reply, R, State}; 359 | 360 | handle_call({info, Key}, _From, State) -> 361 | R = info(Key), 362 | {reply, R, State}; 363 | 364 | handle_call({relation, Key}, _From, State) -> 365 | R = relation(Key), 366 | {reply, R, State}; 367 | 368 | handle_call({set, Key, Opts}, _From, State) -> 369 | R = set(Key, Opts), 370 | {reply, R, State}; 371 | 372 | handle_call({own, Key, ClientKey}, _From, State) -> 373 | R = own(Key, ClientKey), 374 | {reply, R, State}; 375 | 376 | handle_call({abandon, Key, ClientKey}, _From, State) -> 377 | R = abandon(Key, ClientKey), 378 | {reply, R, State}; 379 | 380 | handle_call(nodes, _From, State) -> 381 | R = nodes(), 382 | {reply, R, State}; 383 | 384 | handle_call({send, To, Message}, _From, State) -> 385 | R = send(To, Message, []), 386 | {reply, R, State}; 387 | 388 | handle_call({send, To, Message, Opts}, _From, State) -> 389 | R = send(To, Message, Opts), 390 | {reply, R, State}; 391 | 392 | handle_call(Request, _From, State) -> 393 | ?ERR("unhandled call: ~p", [Request]), 394 | {reply, unknown_request, State}. 395 | %%-------------------------------------------------------------------- 396 | %% @private 397 | %% @doc 398 | %% Handling cast messages 399 | %% 400 | %% @spec handle_cast(Msg, State) -> {noreply, State} | 401 | %% {noreply, State, Timeout} | 402 | %% {stop, Reason, State} 403 | %% @end 404 | %%-------------------------------------------------------------------- 405 | handle_cast(requeue, State) -> 406 | Timeout = requeue(), 407 | erlang:send_after(Timeout, self(), {'$gen_cast', requeue}), 408 | {noreply, State}; 409 | 410 | handle_cast(Message, State) -> 411 | ?ERR("unhandled cast: ~p", [Message]), 412 | {noreply, State}. 413 | 414 | %%-------------------------------------------------------------------- 415 | %% @private 416 | %% @doc 417 | %% Handling all non call/cast messages 418 | %% 419 | %% @spec handle_info(Info, State) -> {noreply, State} | 420 | %% {noreply, State, Timeout} | 421 | %% {stop, Reason, State} 422 | %% @end 423 | %%-------------------------------------------------------------------- 424 | handle_info({mnesia_system_event,{mnesia_down,Node}}, State) -> 425 | 426 | % mnesia:transaction(fun() -> 427 | % case mnesia:read(?MXKV, Node, read) of 428 | % [] -> 429 | % % unregistered client 430 | % pass; 431 | % [Client] when Client#?MXCLIENT.monitor =:= true -> 432 | % mx:send(?MXSYSTEM_CLIENTS_CHANNEL, {offline, Client#?MXCLIENT.name}), 433 | % C = Client#?MXCLIENT{handler = offline}, 434 | % mnesia:write(C); 435 | % [Client] when Client#?MXCLIENT.monitor =:= false -> 436 | % C = Client#?MXCLIENT{handler = offline}, 437 | % mnesia:write(C) 438 | % end 439 | % end 440 | 441 | ?FIXME("Set offline state for all monitored ClientKeys by MX node ~p", [Node]), 442 | 443 | % update mx node list 444 | {noreply, State}; 445 | 446 | 447 | handle_info({mnesia_system_event, _}, State) -> 448 | {noreply, State}; 449 | 450 | handle_info({nodedown, Node}, State) -> 451 | demonitor_node(Node), 452 | {noreply, State}; 453 | 454 | handle_info(Info, State) -> 455 | ?ERR("unhandled info: ~p", [Info]), 456 | {noreply, State}. 457 | 458 | %%-------------------------------------------------------------------- 459 | %% @private 460 | %% @doc 461 | %% This function is called by a gen_server when it is about to 462 | %% terminate. It should be the opposite of Module:init/1 and do any 463 | %% necessary cleaning up. When it returns, the gen_server terminates 464 | %% with Reason. The return value is ignored. 465 | %% 466 | %% @spec terminate(Reason, State) -> void() 467 | %% @end 468 | %%-------------------------------------------------------------------- 469 | terminate(_Reason, _State) -> 470 | ok. 471 | 472 | %%-------------------------------------------------------------------- 473 | %% @private 474 | %% @doc 475 | %% Convert process state when code is changed 476 | %% 477 | %% @spec code_change(OldVsn, State, Extra) -> {ok, NewState} 478 | %% @end 479 | %%-------------------------------------------------------------------- 480 | code_change(_OldVsn, State, _Extra) -> 481 | {ok, State}. 482 | 483 | %%%=================================================================== 484 | %%% Internal functions 485 | %%%=================================================================== 486 | wait_for_mnesia(T) when T > 0 -> 487 | case mx_mnesia:status() of 488 | running -> 489 | ok; 490 | starting = X -> 491 | ?ERR("waiting ~p~n", [X]), 492 | timer:sleep(100), 493 | wait_for_mnesia(T - 100) 494 | end; 495 | 496 | wait_for_mnesia(_T) -> 497 | timeout. 498 | 499 | init_mxscheme() -> 500 | {_, RootKey} = register(client, "%root"), 501 | {_, SystemChannel} = register(channel, "%system", RootKey, [{priority, 1}]), 502 | {_, SystemClientsChannel} = register(channel, "%clients", RootKey, [{priority, 1}]), 503 | {_, SystemQueuesChannel} = register(channel, "%queues", RootKey, [{priority, 1}]), 504 | subscribe(SystemClientsChannel, SystemChannel), 505 | subscribe(SystemQueuesChannel, SystemChannel), 506 | ok. 507 | 508 | 509 | call(M) -> 510 | case gproc_pool:pick_worker(mx_pubsub) of 511 | false -> 512 | broker_unavailable; 513 | Pid -> 514 | gen_server:call(Pid, M) 515 | end. 516 | 517 | cast(M) -> 518 | case gproc_pool:pick_worker(mx_pubsub) of 519 | false -> 520 | broker_unavailable; 521 | Pid -> 522 | gen_server:cast(Pid, M) 523 | end. 524 | 525 | requeue(_, 0, HasDeferred) -> 526 | HasDeferred; 527 | requeue(P, N, HasDeferred) -> 528 | % ?FIXME("we have to check queue utilization here - do not requeue if its exceed the 'threshold_high' limit"), 529 | case 530 | mnesia:transaction(fun() -> 531 | case mnesia:read(?MXDEFER, N, read) of 532 | [] -> 533 | pass; 534 | [Deferred|_] -> 535 | mnesia:delete_object(?MXDEFER, Deferred, write), 536 | Deferred 537 | end 538 | end) of 539 | 540 | {atomic, Deferred} when is_record(Deferred, ?MXDEFER) -> 541 | #?MXDEFER{to = To, message = Message, priority = Priority} = Deferred, 542 | send(To, Message, [{priority, Priority}]), 543 | requeue(P, N - 1, true); 544 | 545 | {atomic, pass} -> 546 | requeue(P, 0, HasDeferred) 547 | end. 548 | 549 | 550 | requeue() -> 551 | case lists:foldl(fun(P, HasDeferredAcc) -> 552 | case requeue(P, 11 - P, false) of 553 | true -> 554 | true; 555 | false -> 556 | HasDeferredAcc 557 | end 558 | end, false, lists:seq(1, 10)) of 559 | true -> 560 | 0; % cast 'dispatch' immediately 561 | false -> 562 | 5000 % FIXME later. wait 50 ms before 'dispatch requeue' 563 | end. 564 | 565 | 566 | monitor_node(Node, ClientKey) -> 567 | mnesia:transaction(fun() -> 568 | case mnesia:read(?MXKV, {monitor, erlang:node(), Node}, read) of 569 | [] -> 570 | % ?DBG("monitor node ~p", [Node]), 571 | erlang:monitor_node(Node, true), 572 | KV = #?MXKV{key = {monitor, erlang:node(), Node}, value = [ClientKey]}, 573 | mnesia:write(KV); 574 | [#?MXKV{key = K, value = V}] -> 575 | % ?DBG("monitor node: already"), 576 | case lists:member(ClientKey, V) of 577 | false -> 578 | mnesia:write(#?MXKV{key = K, value = [ClientKey | V]}); 579 | true -> 580 | pass 581 | end 582 | end 583 | end). 584 | 585 | demonitor_node(Node) -> 586 | case mnesia:transaction(fun() -> 587 | case mnesia:read(?MXKV, {monitor, erlang:node(), Node}, read) of 588 | [] -> 589 | []; 590 | [#?MXKV{key = K, value = V}] -> 591 | ?DBG("Demonitor node (~p): set offline - ~p", [Node,V]), 592 | mnesia:transaction(fun() -> mnesia:delete({?MXKV, K}) end), 593 | V 594 | end 595 | end) of 596 | {atomic, []} -> 597 | pass; 598 | {atomic, ClientKeys} -> 599 | % set client handle to 'offline' 600 | [cast({offline, C}) || C <- ClientKeys]; 601 | _ -> 602 | pass 603 | end. 604 | -------------------------------------------------------------------------------- /src/mx_app.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (C) 2015 Taras Halturin 2 | %% 3 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 4 | %% of this software and associated documentation files (the "Software"), to deal 5 | %% in the Software without restriction, including without limitation the rights 6 | %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | %% copies of the Software, and to permit persons to whom the Software is 8 | %% furnished to do so, subject to the following conditions: 9 | %% 10 | %% The above copyright notice and this permission notice shall be included in 11 | %% all copies or substantial portions of the Software. 12 | %% 13 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | %% THE SOFTWARE. 20 | %% 21 | 22 | 23 | 24 | -module(mx_app). 25 | 26 | -behaviour(application). 27 | 28 | -export([start/2, stop/1]). 29 | 30 | -spec start(_, _) -> {ok, pid()}. 31 | start(_, _) -> 32 | mx_sup:start_link(). 33 | 34 | -spec stop(_) -> {ok}. 35 | stop(_) -> 36 | ok. 37 | -------------------------------------------------------------------------------- /src/mx_broker.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (C) 2015 Taras Halturin 2 | %% 3 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 4 | %% of this software and associated documentation files (the "Software"), to deal 5 | %% in the Software without restriction, including without limitation the rights 6 | %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | %% copies of the Software, and to permit persons to whom the Software is 8 | %% furnished to do so, subject to the following conditions: 9 | %% 10 | %% The above copyright notice and this permission notice shall be included in 11 | %% all copies or substantial portions of the Software. 12 | %% 13 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | %% THE SOFTWARE. 20 | %% 21 | 22 | -module(mx_broker). 23 | 24 | -behaviour(gen_server). 25 | 26 | -compile({no_auto_import,[unregister/1]}). 27 | 28 | -export([start_link/2]). 29 | 30 | -export([init/1, 31 | handle_call/3, 32 | handle_cast/2, 33 | handle_info/2, 34 | terminate/2, 35 | code_change/3]). 36 | 37 | %% records 38 | -record(state, { 39 | id :: non_neg_integer, 40 | config :: list(), 41 | queues 42 | }). 43 | 44 | %% includes 45 | -include_lib("include/log.hrl"). 46 | -include_lib("include/mx.hrl"). 47 | 48 | %%% API 49 | %%%=================================================================== 50 | 51 | 52 | %%-------------------------------------------------------------------- 53 | %% @doc 54 | %% Starts the server 55 | %% 56 | %% @spec start_link() -> {ok, Pid} | ignore | {error, Error} 57 | %% @end 58 | %%-------------------------------------------------------------------- 59 | start_link(I, Opts) -> 60 | gen_server:start_link(?MODULE, [I, Opts], []). 61 | 62 | %%% gen_server callbacks 63 | %%%=================================================================== 64 | 65 | %%-------------------------------------------------------------------- 66 | %% @private 67 | %% @doc 68 | %% Initializes the server 69 | %% 70 | %% @spec init(Args) -> {ok, State} | 71 | %% {ok, State, Timeout} | 72 | %% ignore | 73 | %% {stop, Reason} 74 | %% @end 75 | %%-------------------------------------------------------------------- 76 | init([I, _Opts]) -> 77 | process_flag(trap_exit, true), 78 | gproc_pool:connect_worker(mx_pubsub, {mx_broker, I}), 79 | QueuesTable = list_to_atom("mx_broker_" ++ integer_to_list(I) ++ "_queues"), 80 | ets:new(QueuesTable, [named_table, ordered_set]), 81 | lists:map(fun(X) -> 82 | Q = mx_queue:new(X), 83 | ets:insert(QueuesTable,{X, Q}) 84 | end, lists:seq(1, 10)), 85 | State = #state{ 86 | id = I, 87 | config = [], 88 | queues = QueuesTable 89 | }, 90 | erlang:send_after(0, self(), {'$gen_cast', dispatch}), 91 | {ok, State}. 92 | 93 | %%-------------------------------------------------------------------- 94 | %% @private 95 | %% @doc 96 | %% Handling call messages 97 | %% 98 | %% @spec handle_call(Request, From, State) -> 99 | %% {reply, Reply, State} | 100 | %% {reply, Reply, State, Timeout} | 101 | %% {noreply, State} | 102 | %% {noreply, State, Timeout} | 103 | %% {stop, Reason, Reply, State} | 104 | %% {stop, Reason, State} 105 | %% @end 106 | %%-------------------------------------------------------------------- 107 | 108 | handle_call({register_client, Client, Opts}, {Pid, _}, State) -> 109 | ClientHash = erlang:md5(Client), 110 | ClientKey = <<$*, ClientHash/binary>>, 111 | case mnesia:dirty_read(?MXCLIENT, ClientKey) of 112 | [] -> 113 | C = #?MXCLIENT{ 114 | name = Client, 115 | key = ClientKey, 116 | related = [], 117 | ownerof = [], 118 | handler = proplists:get_value(handler, Opts, Pid), 119 | async = proplists:get_value(async, Opts, true), 120 | defer = proplists:get_value(defer, Opts, false), 121 | monitor = proplists:get_value(monitor, Opts, false), 122 | comment = proplists:get_value(comment, Opts, "Client info") 123 | }, 124 | 125 | case mnesia:transaction(fun() -> mnesia:write(C) end) of 126 | {aborted, E} -> 127 | {reply, E, State}; 128 | _ when C#?MXCLIENT.monitor =:= true, is_pid(C#?MXCLIENT.handler) -> 129 | mx:send(?MXSYSTEM_CLIENTS_CHANNEL, {'$clients', online, Client, ClientKey}), 130 | {reply, {clientkey, ClientKey}, State}; 131 | _ -> 132 | {reply, {clientkey, ClientKey}, State} 133 | end; 134 | [_Client] -> 135 | {reply, {duplicate, ClientKey}, State} 136 | end; 137 | 138 | handle_call({register_channel, Channel, ClientKey, Opts}, _From, State) -> 139 | case mnesia:dirty_read(?MXCLIENT, ClientKey) of 140 | [] -> 141 | {reply, unknown_client, State}; 142 | 143 | [Client] -> 144 | ChannelHash = erlang:md5(Channel), 145 | ChannelKey = <<$#, ChannelHash/binary>>, 146 | case mnesia:dirty_read(?MXCHANNEL, ChannelKey) of 147 | [] -> 148 | Ch = #?MXCHANNEL{ 149 | key = ChannelKey, 150 | name = Channel, 151 | related = [], 152 | owners = [ClientKey], 153 | priority = proplists:get_value(priority, Opts, ?MXQUEUE_PRIO_NORMAL), 154 | defer = proplists:get_value(defer, Opts, true), 155 | comment = proplists:get_value(comment, Opts, "Channel info") 156 | }, 157 | 158 | Transaction = fun() -> 159 | mnesia:write(Ch), 160 | Cl = Client#?MXCLIENT{ownerof = [ChannelKey| Client#?MXCLIENT.ownerof]}, 161 | mnesia:write(Cl) 162 | end, 163 | 164 | case mnesia:transaction(Transaction) of 165 | {aborted, E} -> 166 | {reply, E, State}; 167 | _ -> 168 | {reply, {channelkey, ChannelKey}, State} 169 | end; 170 | 171 | [_Channel] -> 172 | case proplists:get_value(own, Opts, false) of 173 | true -> 174 | own(ChannelKey, Client), 175 | {reply, {duplicate, ChannelKey}, State}; 176 | false -> 177 | {reply, {duplicate, ChannelKey}, State} 178 | end 179 | end 180 | end; 181 | 182 | handle_call({register_pool, Pool, ClientKey, Opts}, _From, State) -> 183 | case mnesia:dirty_read(?MXCLIENT, ClientKey) of 184 | [] -> 185 | {reply, unknown_client, State}; 186 | [Client] -> 187 | PoolHash = erlang:md5(Pool), 188 | PoolKey = <<$@, PoolHash/binary>>, 189 | case mnesia:dirty_read(?MXPOOL, PoolKey) of 190 | [] -> 191 | Pl = #?MXPOOL{ 192 | key = PoolKey, 193 | name = Pool, 194 | related = [], 195 | owners = [ClientKey], 196 | balance = proplists:get_value(balance, Opts, rr), 197 | priority = proplists:get_value(priority, Opts, ?MXQUEUE_PRIO_NORMAL), 198 | defer = proplists:get_value(defer, Opts, true), 199 | comment = proplists:get_value(comment, Opts, "Pool info") 200 | }, 201 | Transaction = fun() -> 202 | mnesia:write(Pl), 203 | Cl = Client#?MXCLIENT{ownerof = [PoolKey| Client#?MXCLIENT.ownerof]}, 204 | mnesia:write(Cl) 205 | end, 206 | 207 | case mnesia:transaction(Transaction) of 208 | {aborted, E} -> 209 | {reply, E, State}; 210 | _ -> 211 | {reply, {poolkey, PoolKey}, State} 212 | end; 213 | 214 | [_Pool] -> 215 | case proplists:get_value(own, Opts, false) of 216 | true -> 217 | own(PoolKey, Client), 218 | {reply, {duplicate, PoolKey}, State}; 219 | false -> 220 | {reply, {duplicate, PoolKey}, State} 221 | end 222 | end 223 | end; 224 | 225 | handle_call({info, <<$*,_/binary>> = ClientKey}, _From, State) -> 226 | case mnesia:dirty_read(?MXCLIENT, ClientKey) of 227 | [] -> 228 | {reply, unknown_client, State}; 229 | [Client] -> 230 | R = lists:zip(record_info(fields, ?MXCLIENT), tl(tuple_to_list(Client))), 231 | {reply, R, State} 232 | end; 233 | 234 | handle_call({info, <<$#, _/binary>> = ChannelKey}, _From, State) -> 235 | case mnesia:dirty_read(?MXCHANNEL, ChannelKey) of 236 | [] -> 237 | {reply, unknown_channel, State}; 238 | [Channel] -> 239 | R = lists:zip(record_info(fields, ?MXCHANNEL), tl(tuple_to_list(Channel))), 240 | {reply, R, State} 241 | end; 242 | 243 | handle_call({info, <<$@, _/binary>> = PoolKey}, _From, State) -> 244 | case mnesia:dirty_read(?MXPOOL, PoolKey) of 245 | [] -> 246 | {reply, unknown_pool, State}; 247 | [Pool] -> 248 | R = lists:zip(record_info(fields, ?MXPOOL), tl(tuple_to_list(Pool))), 249 | {reply, R, State} 250 | end; 251 | 252 | handle_call({info, {Key, Name}}, From, State) -> 253 | case handle_call({info, Key}, From, State) of 254 | {_,R,_} when is_list(R) -> 255 | R1 = proplists:get_value(Name, R), 256 | {reply, R1, State}; 257 | Value -> 258 | Value 259 | end; 260 | 261 | handle_call({relation, Key}, _From, State) -> 262 | case mnesia:dirty_read(?MXRELATION, Key) of 263 | [] -> 264 | {reply, unknown_key, State}; 265 | [Related] -> 266 | R = lists:zip(record_info(fields, ?MXRELATION), tl(tuple_to_list(Related))), 267 | {reply, R, State} 268 | end; 269 | 270 | 271 | handle_call({relate, Key, To}, _From, State) -> 272 | R = action(relate, Key, To), 273 | {reply, R, State}; 274 | 275 | handle_call({unrelate, Key, From}, _From, State) -> 276 | R = action(unrelate, Key, From), 277 | {reply, R, State}; 278 | 279 | handle_call({unregister, Key}, _From, State) -> 280 | R = unregister(Key), 281 | {reply, R, State}; 282 | 283 | handle_call({set, <<$*, _/binary>> = ClientKey, Opts}, _From, State) -> 284 | case mnesia:dirty_read(?MXCLIENT, ClientKey) of 285 | [] -> 286 | {reply, unknown_client, State}; 287 | [Client] -> 288 | Client1 = Client#?MXCLIENT{ 289 | handler = proplists:get_value(handler, Opts, Client#?MXCLIENT.handler), 290 | async = proplists:get_value(async, Opts, Client#?MXCLIENT.async), 291 | defer = proplists:get_value(defer, Opts, Client#?MXCLIENT.defer), 292 | monitor = proplists:get_value(monitor, Opts, Client#?MXCLIENT.monitor), 293 | comment = proplists:get_value(comment, Opts, Client#?MXCLIENT.comment) 294 | }, 295 | mnesia:transaction(fun() -> mnesia:write(Client1) end), 296 | 297 | case Client#?MXCLIENT.handler =:= Client1#?MXCLIENT.handler of 298 | false when Client1#?MXCLIENT.monitor =:= true, is_pid(Client1#?MXCLIENT.handler) -> 299 | % handler has been changed. notify 'online' if its monitored 300 | mx:send(?MXSYSTEM_CLIENTS_CHANNEL, {'$clients', online, Client#?MXCLIENT.name, Client#?MXCLIENT.key}); 301 | false when Client1#?MXCLIENT.monitor =:= true -> 302 | mx:send(?MXSYSTEM_CLIENTS_CHANNEL, {'$clients', offline, Client#?MXCLIENT.name, Client#?MXCLIENT.key}); 303 | _ -> 304 | pass 305 | end, 306 | {reply, ok, State} 307 | end; 308 | 309 | handle_call({set, <<$#, _/binary>> = ChannelKey, Opts}, _From, State) -> 310 | case mnesia:dirty_read(?MXCHANNEL, ChannelKey) of 311 | [] -> 312 | {reply, unknown_channel, State}; 313 | [Channel] -> 314 | Channel1 = Channel#?MXCHANNEL{ 315 | defer = proplists:get_value(defer, Opts, Channel#?MXCHANNEL.defer), 316 | priority = proplists:get_value(priority, Opts, Channel#?MXCHANNEL.priority), 317 | comment = proplists:get_value(comment, Opts, Channel#?MXCHANNEL.comment) 318 | }, 319 | mnesia:transaction(fun() -> mnesia:write(Channel1) end), 320 | {reply, ok, State} 321 | end; 322 | 323 | handle_call({set, <<$@, _/binary>> = PoolKey, Opts}, _From, State) -> 324 | case mnesia:dirty_read(?MXPOOL, PoolKey) of 325 | [] -> 326 | {reply, unknown_pool, State}; 327 | [Pool] -> 328 | Pool1 = Pool#?MXPOOL{ 329 | balance = proplists:get_value(balance, Opts, Pool#?MXPOOL.balance), 330 | defer = proplists:get_value(defer, Opts, Pool#?MXPOOL.defer), 331 | priority = proplists:get_value(priority, Opts, Pool#?MXPOOL.priority), 332 | comment = proplists:get_value(comment, Opts, Pool#?MXPOOL.comment) 333 | }, 334 | mnesia:transaction(fun() -> mnesia:write(Pool1) end), 335 | {reply, ok, State} 336 | end; 337 | 338 | handle_call({own, Key, Client}, _From, State) -> 339 | case own(Key, Client) of 340 | ok -> 341 | Client1 = Client#?MXCLIENT{ownerof = [Key | Client#?MXCLIENT.ownerof]}, 342 | mnesia:transaction(fun() -> mnesia:write(Client1) end), 343 | {reply, ok, State}; 344 | E -> 345 | {reply, E, State} 346 | end; 347 | 348 | handle_call({abandon, Key, Client}, _From, State) -> 349 | case abandon(Key, Client) of 350 | ok -> 351 | OwnerOf = lists:delete(Key, Client#?MXCLIENT.ownerof), 352 | Client1 = Client#?MXCLIENT{ownerof = OwnerOf}, 353 | mnesia:transaction(fun() -> mnesia:write(Client1) end), 354 | {reply, ok, State}; 355 | E -> 356 | {reply, E, State} 357 | end; 358 | 359 | handle_call(Request, _From, State) -> 360 | ?ERR("unhandled call: ~p", [Request]), 361 | {reply, ok, State}. 362 | 363 | %%-------------------------------------------------------------------- 364 | %% @private 365 | %% @doc 366 | %% Handling cast messages 367 | %% 368 | %% @spec handle_cast(Msg, State) -> {noreply, State} | 369 | %% {noreply, State, Timeout} | 370 | %% {stop, Reason, State} 371 | %% @end 372 | %%-------------------------------------------------------------------- 373 | handle_cast({send, To, Message, Opts}, State) when is_record(To, ?MXCLIENT) -> 374 | ?DBG("Send to client: ~p", [To]), 375 | #state{queues = QueuesTable} = State, 376 | #?MXCLIENT{key = Key} = To, 377 | 378 | % peering message priority is 1, but can be overrided via options 379 | P = proplists:get_value(priority, Opts, 1), 380 | Parent = proplists:get_value(parent, Opts, none), 381 | [{P,Q}|_] = ets:lookup(QueuesTable, P), 382 | 383 | case mx_queue:put({Key, {To, Message, Parent}}, Q) of 384 | {defer, Q1} -> 385 | defer(1, Key, Message, Parent); 386 | Q1 -> 387 | pass 388 | end, 389 | ets:insert(QueuesTable, {P, Q1}), 390 | {noreply, State}; 391 | 392 | handle_cast({send, To, Message, Opts}, State) when is_record(To, ?MXCHANNEL) -> 393 | ?DBG("Send to channel: ~p", [To]), 394 | #state{queues = QueuesTable} = State, 395 | #?MXCHANNEL{key = Key, priority = ChannelP, defer = Deferrable} = To, 396 | 397 | P = proplists:get_value(priority, Opts, ChannelP), 398 | Parent = proplists:get_value(parent, Opts, none), 399 | [{P,Q}|_] = ets:lookup(QueuesTable, P), 400 | 401 | case mx_queue:put({Key, {To, Message, Parent}}, Q) of 402 | {defer, Q1} when P =:= 1, Deferrable =:= true -> 403 | defer(1, Key, Message, Parent); 404 | {defer, Q1} when P > 1, Deferrable =:= true -> 405 | defer(P - 1, Key, Message, Parent); 406 | Q1 -> 407 | pass 408 | end, 409 | ets:insert(QueuesTable, {P, Q1}), 410 | {noreply, State}; 411 | 412 | handle_cast({send, To, Message, Opts}, State) when is_record(To, ?MXPOOL) -> 413 | ?DBG("Send to pool: ~p", [To]), 414 | #state{queues = QueuesTable} = State, 415 | #?MXPOOL{key = Key, priority = PoolP, defer = Deferrable} = To, 416 | 417 | P = proplists:get_value(priority, Opts, PoolP), 418 | Parent = proplists:get_value(parent, Opts, none), 419 | [{P,Q}|_] = ets:lookup(QueuesTable, P), 420 | 421 | case mx_queue:put({Key, {To, Message, Parent}}, Q) of 422 | {defer, Q1} when P =:= 1, Deferrable =:= true -> 423 | defer(P, Key, Message, Parent); 424 | {defer, Q1} when P > 1, Deferrable =:= true -> 425 | defer(P, Key, Message, Parent); 426 | Q1 -> 427 | pass 428 | end, 429 | ets:insert(QueuesTable, {P, Q1}), 430 | {noreply, State}; 431 | 432 | handle_cast(dispatch, State) -> 433 | #state{queues = QueuesTable} = State, 434 | Timeout = dispatch(QueuesTable), 435 | erlang:send_after(Timeout, self(), {'$gen_cast', dispatch}), 436 | {noreply, State}; 437 | 438 | handle_cast({flush, Key}, State) -> 439 | mnesia:delete({?MXDEFER, Key}), 440 | {noreply, State}; 441 | 442 | handle_cast({offline, ClientKey}, State) -> 443 | client_offline(ClientKey), 444 | {noreply, State}; 445 | 446 | handle_cast({online, ClientKey, Pid}, State) -> 447 | client_online(ClientKey, Pid), 448 | {noreply, State}; 449 | 450 | handle_cast(Msg, State) -> 451 | ?ERR("unhandled cast: ~p", [Msg]), 452 | {noreply, State}. 453 | 454 | %%-------------------------------------------------------------------- 455 | %% @private 456 | %% @doc 457 | %% Handling all non call/cast messages 458 | %% 459 | %% @spec handle_info(Info, State) -> {noreply, State} | 460 | %% {noreply, State, Timeout} | 461 | %% {stop, Reason, State} 462 | %% @end 463 | %%-------------------------------------------------------------------- 464 | 465 | handle_info(Info, State) -> 466 | ?ERR("unhandled info: ~p", [Info]), 467 | {noreply, State}. 468 | 469 | %%-------------------------------------------------------------------- 470 | %% @private 471 | %% @doc 472 | %% This function is called by a gen_server when it is about to 473 | %% terminate. It should be the opposite of Module:init/1 and do any 474 | %% necessary cleaning up. When it returns, the gen_server terminates 475 | %% with Reason. The return value is ignored. 476 | %% 477 | %% @spec terminate(Reason, State) -> void() 478 | %% @end 479 | %%-------------------------------------------------------------------- 480 | terminate(_Reason, #state{id = ID}) -> 481 | gproc_pool:disconnect_worker(mx_pubsub, {mx_broker, ID}), 482 | ok. 483 | 484 | 485 | %%-------------------------------------------------------------------- 486 | %% @private 487 | %% @doc 488 | %% Convert process state when code is changed 489 | %% 490 | %% @spec code_change(OldVsn, State, Extra) -> {ok, NewState} 491 | %% @end 492 | %%-------------------------------------------------------------------- 493 | code_change(_OldVsn, State, _Extra) -> 494 | {ok, State}. 495 | 496 | %%%=================================================================== 497 | %%% Internal functions 498 | %%%=================================================================== 499 | unregister(<<$*,_/binary>> = ClientKey) -> 500 | case mnesia:dirty_read(?MXCLIENT, ClientKey) of 501 | [] -> 502 | unknown_client; 503 | [Client] when Client#?MXCLIENT.monitor =:= true -> 504 | [unrelate(ClientKey, Ch) || Ch <- Client#?MXCLIENT.related], 505 | [abandon(I, Client) || I <- Client#?MXCLIENT.ownerof], 506 | mnesia:transaction(fun() -> mnesia:delete({?MXCLIENT, ClientKey}) end), 507 | mx:send(?MXSYSTEM_CLIENTS_CHANNEL, 508 | {'$clients', offline, Client#?MXCLIENT.name, ClientKey}), 509 | ok; 510 | [Client] -> 511 | [unrelate(ClientKey, Ch) || Ch <- Client#?MXCLIENT.related], 512 | [abandon(I, Client) || I <- Client#?MXCLIENT.ownerof], 513 | mnesia:transaction(fun() -> mnesia:delete({?MXCLIENT, ClientKey}) end), 514 | ok 515 | end; 516 | 517 | unregister(<<$#, _/binary>> = Key) -> % channel 518 | case mnesia:dirty_read(?MXCHANNEL, Key) of 519 | [] -> 520 | unknown_channel; 521 | [R] -> 522 | remove_relations(R#?MXCHANNEL.related), 523 | remove_owning(R#?MXCHANNEL.owners), 524 | mnesia:transaction(fun() -> mnesia:delete({?MXCHANNEL, Key}) end), 525 | ok 526 | end; 527 | 528 | unregister(<<$@, _/binary>> = Key) -> % channel 529 | case mnesia:dirty_read(?MXPOOL, Key) of 530 | [] -> 531 | unknown_pool; 532 | [R] -> 533 | remove_relations(R#?MXPOOL.related), 534 | remove_owning(R#?MXPOOL.owners), 535 | mnesia:transaction(fun() -> 536 | mnesia:delete({?MXPOOL, Key}), 537 | mnesia:delete(?MXKV, {rrpool,Key}) 538 | end), 539 | ok 540 | end; 541 | 542 | unregister(_Key) -> 543 | unknown_key. 544 | 545 | unrelate(Key, <<$#, _/binary>> = From) -> 546 | unrelate(Key, From, checked); 547 | 548 | unrelate(Key, <<$@, _/binary>> = From) -> 549 | unrelate(Key, From, checked). 550 | 551 | unrelate(Key, From, checked) -> 552 | case mnesia:dirty_read(?MXRELATION, From) of 553 | [] -> 554 | unknown_relation; 555 | [Relation] -> 556 | case lists:member(Key, Relation#?MXRELATION.related) of 557 | false -> 558 | unknown_relation; 559 | true -> 560 | Related = lists:delete(Key, Relation#?MXRELATION.related), 561 | UpdatedRelation = Relation#?MXRELATION{related = Related}, 562 | mnesia:transaction(fun() -> mnesia:write(UpdatedRelation) end), 563 | ok 564 | end 565 | end. 566 | 567 | relate(Key, <<$#, _/binary>> = To) -> 568 | relate(Key, To, checked); 569 | 570 | relate(Key, <<$@, _/binary>> = To) -> 571 | relate(Key, To, checked). 572 | 573 | relate(Key, To, checked) -> 574 | case mnesia:dirty_read(?MXRELATION, To) of 575 | [] -> 576 | Relation = #?MXRELATION{ 577 | key = To, 578 | related = [] 579 | }; 580 | [Relation] -> 581 | ok 582 | end, 583 | case lists:member(Key, Relation#?MXRELATION.related) of 584 | false -> 585 | Related = [Key | Relation#?MXRELATION.related], 586 | UpdatedRelation = Relation#?MXRELATION{related = Related}, 587 | mnesia:transaction(fun() -> mnesia:write(UpdatedRelation) end), 588 | ok; 589 | true -> 590 | related 591 | end. 592 | 593 | remove_relations(Relations) -> 594 | ?DBG("REMOVE RELATIONS: ~p", [Relations]), 595 | lists:foldl( 596 | fun(<<$*,_/binary>> = RelationKey, _) -> 597 | % client relation 598 | [Client] = mnesia:dirty_read(?MXCLIENT, RelationKey), 599 | Related = lists:delete(RelationKey, Client#?MXCLIENT.related), 600 | UpdatedClient = Client#?MXCLIENT{related = Related}, 601 | mnesia:transaction(fun() -> mnesia:write(UpdatedClient) end), 602 | ok; 603 | 604 | (<<$#,_/binary>> = RelationKey, _) -> 605 | % channel relation 606 | [Channel] = mnesia:dirty_read(?MXCHANNEL, RelationKey), 607 | Related = lists:delete(RelationKey, Channel#?MXCHANNEL.related), 608 | UpdatedChannel = Channel#?MXCHANNEL{related = Related}, 609 | mnesia:transaction(fun() -> mnesia:write(UpdatedChannel) end), 610 | ok; 611 | 612 | (<<$@,_/binary>> = RelationKey, _) -> 613 | % pool relation 614 | [Channel] = mnesia:dirty_read(?MXCHANNEL, RelationKey), 615 | Related = lists:delete(RelationKey, Channel#?MXCHANNEL.related), 616 | UpdatedChannel = Channel#?MXCHANNEL{related = Related}, 617 | mnesia:transaction(fun() -> mnesia:write(UpdatedChannel) end), 618 | ok 619 | end, ok, Relations). 620 | 621 | remove_owning(Owners) -> 622 | lists:foldl(fun(Key, _) -> 623 | [Client] = mnesia:dirty_read(?MXCLIENT, Key), 624 | ClientOwnerOf = lists:delete(Key, Client#?MXCLIENT.ownerof), 625 | UpdatedClient = Client#?MXCLIENT{ownerof = ClientOwnerOf}, 626 | mnesia:transaction(fun() -> mnesia:write(UpdatedClient) end), 627 | ok 628 | end, ok, Owners). 629 | 630 | own(<<$@,_/binary>> = PoolKey, Client) when is_record(Client, ?MXCLIENT) -> 631 | case mnesia:dirty_read(?MXPOOL, PoolKey) of 632 | [] -> 633 | unknown_pool; 634 | [Pool] -> 635 | case lists:member(Client#?MXCLIENT.key, Pool#?MXPOOL.owners) of 636 | false -> 637 | Owners = [Client#?MXCLIENT.key | Pool#?MXPOOL.owners], 638 | UpdatedPool = Pool#?MXPOOL{owners = Owners}, 639 | mnesia:transaction(fun() ->mnesia:write(UpdatedPool) end), 640 | ok; 641 | true -> 642 | already_owned 643 | end 644 | end; 645 | 646 | own(<<$#,_/binary>> = ChannelKey, Client) when is_record(Client, ?MXCLIENT) -> 647 | case mnesia:dirty_read(?MXCHANNEL, ChannelKey) of 648 | [] -> 649 | unknown_channel; 650 | [Channel] -> 651 | case lists:member(Client#?MXCLIENT.key, Channel#?MXCHANNEL.owners) of 652 | false -> 653 | Owners = [Client#?MXCLIENT.key | Channel#?MXCHANNEL.owners], 654 | UpdatedChannel = Channel#?MXCHANNEL{owners = Owners}, 655 | mnesia:transaction(fun() ->mnesia:write(UpdatedChannel) end), 656 | ok; 657 | true -> 658 | already_owned 659 | end 660 | end; 661 | 662 | own(_Key, _Client) -> 663 | unknown_key. 664 | 665 | abandon(<<$@,_/binary>> = PoolKey, Client) when is_record(Client, ?MXCLIENT) -> 666 | case mnesia:dirty_read(?MXPOOL, PoolKey) of 667 | [] -> 668 | unknown_pool; 669 | [Pool] -> 670 | case lists:delete(Client#?MXCLIENT.key, Pool#?MXPOOL.owners) of 671 | [] -> 672 | unregister(PoolKey); 673 | Owners -> 674 | UpdatedPool = Pool#?MXPOOL{owners = Owners}, 675 | mnesia:transaction(fun() ->mnesia:write(UpdatedPool) end), 676 | ok 677 | end 678 | end; 679 | 680 | abandon(<<$#,_/binary>> = ChannelKey, Client) when is_record(Client, ?MXCLIENT) -> 681 | case mnesia:dirty_read(?MXCHANNEL, ChannelKey) of 682 | [] -> 683 | ok; 684 | [Channel] -> 685 | case lists:delete(Client#?MXCLIENT.key, Channel#?MXCHANNEL.owners) of 686 | [] -> 687 | unregister(ChannelKey); 688 | Owners -> 689 | UpdatedChannel = Channel#?MXCHANNEL{owners = Owners}, 690 | mnesia:transaction(fun() ->mnesia:write(UpdatedChannel) end), 691 | ok 692 | end 693 | end; 694 | 695 | abandon(_key, _Client) -> 696 | unknown_key. 697 | 698 | dispatch(Q, 0, HasMessages) -> 699 | {Q, HasMessages}; 700 | dispatch(Q, N, HasMessages) -> 701 | case mx_queue:get(Q) of 702 | % has no message 703 | {empty, Q1} -> 704 | {Q1, HasMessages}; 705 | % async dispatch 706 | {{value, {_, {To, Message, _}}}, Q1} when is_record(To, ?MXCLIENT), 707 | is_pid(To#?MXCLIENT.handler), 708 | To#?MXCLIENT.async =:= true -> 709 | ?DBG("Dispatch (async) to the client: ~p [MESSAGE: ~p]", [To, Message]), 710 | To#?MXCLIENT.handler ! {mx, Message}, 711 | dispatch(Q1, N - 1, true); 712 | 713 | % sync dispatch 714 | {{value, {_, {To, Message, Parent}}}, Q1} when is_record(To, ?MXCLIENT), 715 | is_pid(To#?MXCLIENT.handler) -> 716 | ?DBG("Dispatch to (sync) the client: ~p [MESSAGE: ~p]", [To, Message]), 717 | Sync = fun() -> 718 | To#?MXCLIENT.handler ! {mxs, self(), Message}, 719 | receive 720 | ok -> pass 721 | after 722 | ?MX_SEND_TIMEOUT -> 723 | client_offline(To#?MXCLIENT.key), 724 | case To#?MXCLIENT.defer of 725 | true when Parent == none -> 726 | P = mx_queue:name(Q1), 727 | defer(P+1, To#?MXCLIENT.key, Message, none); 728 | 729 | _ when is_binary(Parent) -> 730 | ?DBG("remove offline/broken ClientKey from the pool: ~p", [To#?MXCLIENT.key]), 731 | mx:leave(To#?MXCLIENT.key, Parent), 732 | case mx_queue:name(Q1) of 733 | 10 -> 734 | mx:send(Parent, Message, [{priority, 10}]); 735 | P -> 736 | mx:send(Parent, Message, [{priority, P+1}]) 737 | end; 738 | _ -> 739 | pass 740 | end 741 | end 742 | end, 743 | erlang:spawn(Sync), 744 | dispatch(Q1, N - 1, true); 745 | 746 | % offline client with parent. return it to the parent. 747 | {{value, {_, {To, Message, Parent}}}, Q1} when is_binary(Parent) -> 748 | ?DBG("remove offline/broken ClientKey from the pool: ~p", [To#?MXCLIENT.key]), 749 | mx:leave(To#?MXCLIENT.key, Parent), 750 | case mx_queue:name(Q1) of 751 | 10 -> 752 | mx:send(Parent, Message, [{priority, 10}]); 753 | P -> 754 | mx:send(Parent, Message, [{priority, P+1}]) 755 | end, 756 | dispatch(Q1, N - 1, true); 757 | 758 | % offline client with deferring 759 | {{value, {_, {To, Message, _}}}, Q1} when is_record(To, ?MXCLIENT), 760 | To#?MXCLIENT.defer == true -> 761 | ?DBG("Client is offline: ~p. Defer this message.", [To#?MXCLIENT.name]), 762 | defer(1, To#?MXCLIENT.key, Message, none), 763 | dispatch(Q1, N - 1, true); 764 | 765 | % offline client with disabled deferring (drop messages) 766 | {{value, {_, {To, _Message, _}}}, Q1} when is_record(To, ?MXCLIENT) -> 767 | ?DBG("Client is offline [~p]. Deferring is disabled. Drop this message.", [To#?MXCLIENT.name]), 768 | dispatch(Q1, N - 1, true); 769 | 770 | % message for channel 771 | {{value, {_, {To, Message, _}}}, Q1} when is_record(To, ?MXCHANNEL) -> 772 | case mnesia:dirty_read(?MXRELATION, To#?MXCHANNEL.key) of 773 | [] -> 774 | pass; % have no receivers 775 | [Relations] when Relations#?MXRELATION.related == [] -> 776 | pass; % have no receivers 777 | [Relations] -> 778 | ?DBG("Dispatch message to the channel [~p]. Send times [~p] ", 779 | [To#?MXCHANNEL.name, length(Relations#?MXRELATION.related)]), 780 | P = mx_queue:name(Q1), 781 | [mx:send(X, Message, [{priority, P}]) || X <- Relations#?MXRELATION.related] 782 | end, 783 | dispatch(Q1, N - 1, true); 784 | 785 | % message for pool 786 | {{value, {_, {To, Message, _}}}, Q1} when is_record(To, ?MXPOOL) -> 787 | case mnesia:dirty_read(?MXRELATION, To#?MXPOOL.key) of 788 | [] -> 789 | pass; % have no receivers 790 | [Relations] when Relations#?MXRELATION.related == [] -> 791 | pass; % have no receivers 792 | [Relations] when To#?MXPOOL.balance == rr -> 793 | mnesia:transaction(fun() -> 794 | L = length(Relations#?MXRELATION.related), 795 | case mnesia:wread({?MXKV, {rrpool, To#?MXPOOL.key}}) of 796 | [#?MXKV{value = V}] when V < L -> 797 | I = V + 1; 798 | _ -> 799 | I = 1 800 | end, 801 | KV = #?MXKV{key = {rrpool, To#?MXPOOL.key}, value = I}, 802 | mnesia:write(KV), 803 | X = lists:nth(I, Relations#?MXRELATION.related), 804 | ?DBG("Dispatch message to the pool [~p]. Send to [~p] of [~p] ", 805 | [To#?MXPOOL.name, I, length(Relations#?MXRELATION.related)]), 806 | P = mx_queue:name(Q1), 807 | mx:send(X, Message, [{priority, P}, {parent, To#?MXPOOL.key}]) 808 | end); 809 | 810 | [Relations] when To#?MXPOOL.balance == random -> 811 | N = rand:uniform(length(Relations#?MXRELATION.related)), 812 | X = lists:nth(N, Relations#?MXRELATION.related), 813 | ?DBG("Dispatch message to the pool [~p]. Send to [~p] of [~p] ", 814 | [To#?MXPOOL.name, N, length(Relations#?MXRELATION.related)]), 815 | mx:send(X, Message, [{priority, To#?MXPOOL.priority}, {parent, To#?MXPOOL.key}]); 816 | [Relations] when To#?MXPOOL.balance == hash -> 817 | N = erlang:phash(Message, length(Relations#?MXRELATION.related)), 818 | X = lists:nth(N, Relations#?MXRELATION.related), 819 | ?DBG("Dispatch message to the pool [~p]. Send to [~p] of [~p] ", 820 | [To#?MXPOOL.name, N, length(Relations#?MXRELATION.related)]), 821 | P = mx_queue:name(Q1), 822 | mx:send(X, Message, [{priority, P}, {parent, To#?MXPOOL.key}]) 823 | end, 824 | dispatch(Q1, N - 1, true); 825 | {FIXME, Q1} -> 826 | ?DBG("FIXME: ~p", [FIXME]), 827 | dispatch(Q1, N - 1, true) 828 | end. 829 | 830 | % queue name = 1 (priority 1) - process 10 messages from queue 831 | % ... 832 | % 10 (priority 10) - process 1 message 833 | dispatch(QueuesTable) -> 834 | case lists:foldl(fun(P, HasMessagesAcc) -> 835 | [{P,Q}|_] = ets:lookup(QueuesTable, P), 836 | case dispatch(Q, 11 - P, false) of 837 | {Q1, true} -> 838 | ets:insert(QueuesTable, {P, Q1}), 839 | true; 840 | {_, false} -> 841 | HasMessagesAcc; 842 | 843 | FIXME -> 844 | ?DBG("FIXME: ~p", [FIXME]), 845 | false 846 | end 847 | end, false, lists:seq(1, 10)) of 848 | true -> 849 | 0; % cast 'dispatch' immediately 850 | false -> 851 | 100 % FIXME later. wait 50 ms before 'dispatch casting' 852 | end. 853 | 854 | defer(Priority, _To, _Message, _Parent) when Priority > 10 -> 855 | pass; 856 | defer(Priority, To, Message, Parent) when Priority < 1 -> 857 | defer(1, To, Message, Parent); 858 | defer(Priority, To, Message, Parent) -> 859 | Defer = #?MXDEFER{ 860 | priority = Priority, 861 | to = To, 862 | message = Message, 863 | parent = Parent 864 | }, 865 | mnesia:transaction(fun() -> mnesia:write(Defer) end). 866 | 867 | action(relate, Key, ToKey) -> 868 | case relate(Key, <> = ToKey) of 869 | related when X == 35 -> % '#' - channel 870 | {already_subscribed, Key}; 871 | related -> 872 | {already_joined, Key}; 873 | ok -> 874 | action(relate, Key, ToKey, related) 875 | end; 876 | 877 | action(unrelate, Key, FromKey) -> 878 | case unrelate(Key, <> = FromKey) of 879 | unknown_relation when X == 35 -> % '#' - channel 880 | {not_subscribed, Key}; 881 | unknown_relation -> 882 | {not_joined, Key}; 883 | ok -> 884 | action(unrelate, Key, FromKey, unrelated) 885 | end. 886 | 887 | action(relate, <<$*, _/binary>> = Key, ToKey, related) -> 888 | case mnesia:dirty_read(?MXCLIENT, Key) of 889 | [] -> 890 | unknown_client; 891 | [Client|_] -> 892 | Related = [ToKey | Client#?MXCLIENT.related], 893 | UpdatedClient = Client#?MXCLIENT{related = Related}, 894 | mnesia:transaction(fun() -> mnesia:write(UpdatedClient) end), 895 | ok 896 | end; 897 | 898 | action(relate, <<$#, _/binary>> = Key, ToKey, related) -> 899 | case mnesia:dirty_read(?MXCHANNEL, Key) of 900 | [] -> 901 | unknown_channel_client; 902 | [Channel|_] -> 903 | Related = [ToKey | Channel#?MXCHANNEL.related], 904 | UpdatedChannel = Channel#?MXCHANNEL{related = Related}, 905 | mnesia:transaction(fun() -> mnesia:write(UpdatedChannel) end), 906 | ok 907 | end; 908 | 909 | action(relate, <<$@, _/binary>> = Key, ToKey, related) -> 910 | case mnesia:dirty_read(?MXPOOL, Key) of 911 | [] -> 912 | unknown_pool_client; 913 | [Pool|_] -> 914 | Related = [ToKey | Pool#?MXPOOL.related], 915 | UpdatedPool = Pool#?MXPOOL{related = Related}, 916 | mnesia:transaction(fun() -> mnesia:write(UpdatedPool) end), 917 | ok 918 | end; 919 | 920 | action(unrelate, <<$*, _/binary>> = Key, FromKey, unrelated) -> 921 | case mnesia:dirty_read(?MXCLIENT, Key) of 922 | [] -> 923 | unknown_client; 924 | [Client|_] -> 925 | Related = lists:delete(FromKey, Client#?MXCLIENT.related), 926 | UpdatedClient = Client#?MXCLIENT{related = Related}, 927 | mnesia:transaction(fun() -> mnesia:write(UpdatedClient) end), 928 | ok 929 | end; 930 | 931 | action(unrelate, <<$#, _/binary>> = Key, FromKey, unrelated) -> 932 | case mnesia:dirty_read(?MXCHANNEL, Key) of 933 | [] -> 934 | unknown_channel_client; 935 | [Channel|_] -> 936 | Related = Related = lists:delete(FromKey, Channel#?MXCHANNEL.related), 937 | UpdatedChannel = Channel#?MXCHANNEL{related = Related}, 938 | mnesia:transaction(fun() -> mnesia:write(UpdatedChannel) end), 939 | ok 940 | end; 941 | 942 | action(unrelate, <<$@, _/binary>> = Key, FromKey, unrelated) -> 943 | case mnesia:dirty_read(?MXPOOL, Key) of 944 | [] -> 945 | unknown_pool_client; 946 | [Pool|_] -> 947 | Related = Related = lists:delete(FromKey, Pool#?MXPOOL.related), 948 | UpdatedPool = Pool#?MXPOOL{related = Related}, 949 | mnesia:transaction(fun() -> mnesia:write(UpdatedPool) end), 950 | ok 951 | end. 952 | 953 | client_offline(<<$*, _/binary>> = ClientKey) -> 954 | mnesia:transaction(fun() -> 955 | case mnesia:wread({?MXCLIENT, ClientKey}) of 956 | [] -> 957 | % unregistered client 958 | pass; 959 | [Client] when Client#?MXCLIENT.monitor =:= true -> 960 | mx:send(?MXSYSTEM_CLIENTS_CHANNEL, {'$clients', offline, Client#?MXCLIENT.name, ClientKey}), 961 | C = Client#?MXCLIENT{handler = offline}, 962 | mnesia:write(C); 963 | [Client] when Client#?MXCLIENT.monitor =:= false -> 964 | C = Client#?MXCLIENT{handler = offline}, 965 | mnesia:write(C) 966 | end 967 | end). 968 | 969 | client_online(<<$*, _/binary>> = ClientKey, Pid) -> 970 | mnesia:transaction(fun() -> 971 | case mnesia:wread({?MXCLIENT, ClientKey}) of 972 | [] -> 973 | % unregistered client 974 | pass; 975 | [Client] when Client#?MXCLIENT.monitor =:= true -> 976 | mx:send(?MXSYSTEM_CLIENTS_CHANNEL, {'$clients', online, Client#?MXCLIENT.name, ClientKey}), 977 | C = Client#?MXCLIENT{handler = Pid}, 978 | mnesia:write(C); 979 | [Client] when Client#?MXCLIENT.monitor =:= false -> 980 | C = Client#?MXCLIENT{handler = Pid}, 981 | mnesia:write(C) 982 | end 983 | end). 984 | -------------------------------------------------------------------------------- /src/mx_broker_sup.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (C) 2015 Taras Halturin 2 | %% 3 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 4 | %% of this software and associated documentation files (the "Software"), to deal 5 | %% in the Software without restriction, including without limitation the rights 6 | %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | %% copies of the Software, and to permit persons to whom the Software is 8 | %% furnished to do so, subject to the following conditions: 9 | %% 10 | %% The above copyright notice and this permission notice shall be included in 11 | %% all copies or substantial portions of the Software. 12 | %% 13 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | %% THE SOFTWARE. 20 | %% 21 | 22 | -module(mx_broker_sup). 23 | 24 | -behaviour(supervisor). 25 | 26 | -export([start_link/0]). 27 | 28 | %% Supervisor callbacks 29 | -export([init/1]). 30 | 31 | 32 | -define(CHILD(I, Type, Args), {{N,I}, {N, start_link, [Args]}, permanent, 5000, Type, [N]}). 33 | 34 | %% API functions 35 | %% =================================================================== 36 | 37 | start_link() -> 38 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 39 | 40 | 41 | %% Supervisor callbacks 42 | %% =================================================================== 43 | 44 | init([]) -> 45 | % {ok, Opts} = application:get_env(mx, broker), 46 | Opts = [], 47 | Workers = erlang:system_info(schedulers), 48 | gproc_pool:new(mx_pubsub, round_robin, [{size, Workers}]), 49 | 50 | Children = lists:map( 51 | fun(I) -> 52 | Worker = {mx_broker, I}, 53 | gproc_pool:add_worker(mx_pubsub, Worker, I), 54 | {Worker, {mx_broker, start_link, [I, Opts]}, 55 | permanent, 5000, worker, [mx_broker]} 56 | end, lists:seq(1, Workers)), 57 | 58 | {ok, { {one_for_one, 5, 10}, Children } }. 59 | 60 | -------------------------------------------------------------------------------- /src/mx_mnesia.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (C) 2015 Taras Halturin 2 | %% 3 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 4 | %% of this software and associated documentation files (the "Software"), to deal 5 | %% in the Software without restriction, including without limitation the rights 6 | %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | %% copies of the Software, and to permit persons to whom the Software is 8 | %% furnished to do so, subject to the following conditions: 9 | %% 10 | %% The above copyright notice and this permission notice shall be included in 11 | %% all copies or substantial portions of the Software. 12 | %% 13 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | %% THE SOFTWARE. 20 | %% 21 | 22 | -module(mx_mnesia). 23 | 24 | -behaviour(gen_server). 25 | 26 | -export([start_link/0]). 27 | 28 | -export([init/1, 29 | handle_call/3, 30 | handle_cast/2, 31 | handle_info/2, 32 | terminate/2, 33 | code_change/3]). 34 | 35 | -export([nodes/0, 36 | status/0, 37 | clear_all_tables/0]). 38 | 39 | -include_lib("include/mx.hrl"). 40 | -include_lib("include/log.hrl"). 41 | 42 | -record(state, { 43 | status :: starting | running 44 | }). 45 | 46 | %%%=================================================================== 47 | %%% API 48 | %%%=================================================================== 49 | 50 | nodes() -> 51 | gen_server:call(?MODULE, nodes). 52 | 53 | status() -> 54 | gen_server:call(?MODULE, status). 55 | 56 | %%-------------------------------------------------------------------- 57 | %% @doc 58 | %% Starts the server 59 | %% 60 | %% @spec start_link() -> {ok, Pid} | ignore | {error, Error} 61 | %% @end 62 | %%-------------------------------------------------------------------- 63 | start_link() -> 64 | gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). 65 | 66 | %%% gen_server callbacks 67 | %%%=================================================================== 68 | 69 | %%-------------------------------------------------------------------- 70 | %% @private 71 | %% @doc 72 | %% Initializes the server 73 | %% 74 | %% @spec init(Args) -> {ok, State} | 75 | %% {ok, State, Timeout} | 76 | %% ignore | 77 | %% {stop, Reason} 78 | %% @end 79 | %%-------------------------------------------------------------------- 80 | init([]) -> 81 | process_flag(trap_exit, true), 82 | 83 | case application:get_env(mx, mnesia_base_dir, undefined) of 84 | Dir when Dir == ""; Dir == undefined -> 85 | pass; 86 | MnesiaBaseDir when is_list(MnesiaBaseDir) -> 87 | NodeDir = MnesiaBaseDir ++ node(), 88 | ok = filelib:ensure_dir(NodeDir), 89 | application:set_env(mnesia, dir, NodeDir) 90 | end, 91 | 92 | case application:get_env(mx, master, undefined) of 93 | X when X == ""; X == undefined -> 94 | run_as_master(); 95 | Master when is_atom(Master); is_list(Master) -> 96 | run_as_slave(Master) 97 | end, 98 | 99 | {ok, #state{status = running}}. 100 | 101 | %%-------------------------------------------------------------------- 102 | %% @private 103 | %% @doc 104 | %% Handling call messages 105 | %% 106 | %% @spec handle_call(Request, From, State) -> 107 | %% {reply, Reply, State} | 108 | %% {reply, Reply, State, Timeout} | 109 | %% {noreply, State} | 110 | %% {noreply, State, Timeout} | 111 | %% {stop, Reason, Reply, State} | 112 | %% {stop, Reason, State} 113 | %% @end 114 | %%-------------------------------------------------------------------- 115 | handle_call(status, _, #state{status = S} = State) -> 116 | {reply, S, State}; 117 | 118 | handle_call(nodes, _From, State) -> 119 | Nodes = mnesia:system_info(running_db_nodes), 120 | {reply, Nodes, State}; 121 | 122 | handle_call(Request, _From, State) -> 123 | ?ERR("unhandled call: ~p", [Request]), 124 | {reply, ok, State}. 125 | 126 | %%-------------------------------------------------------------------- 127 | %% @private 128 | %% @doc 129 | %% Handling cast messages 130 | %% 131 | %% @spec handle_cast(Msg, State) -> {noreply, State} | 132 | %% {noreply, State, Timeout} | 133 | %% {stop, Reason, State} 134 | %% @end 135 | %%-------------------------------------------------------------------- 136 | handle_cast(Msg, State) -> 137 | ?ERR("unhandled cast: ~p", [Msg]), 138 | {noreply, State}. 139 | 140 | %%-------------------------------------------------------------------- 141 | %% @private 142 | %% @doc 143 | %% Handling all non call/cast messages 144 | %% 145 | %% @spec handle_info(Info, State) -> {noreply, State} | 146 | %% {noreply, State, Timeout} | 147 | %% {stop, Reason, State} 148 | %% @end 149 | %%-------------------------------------------------------------------- 150 | 151 | handle_info(Info, State) -> 152 | ?ERR("unhandled info: ~p", [Info]), 153 | {noreply, State}. 154 | 155 | %%-------------------------------------------------------------------- 156 | %% @private 157 | %% @doc 158 | %% This function is called by a gen_server when it is about to 159 | %% terminate. It should be the opposite of Module:init/1 and do any 160 | %% necessary cleaning up. When it returns, the gen_server terminates 161 | %% with Reason. The return value is ignored. 162 | %% 163 | %% @spec terminate(Reason, State) -> void() 164 | %% @end 165 | %%-------------------------------------------------------------------- 166 | terminate(_Reason, _State) -> 167 | ok. 168 | 169 | %%-------------------------------------------------------------------- 170 | %% @private 171 | %% @doc 172 | %% Convert process state when code is changed 173 | %% 174 | %% @spec code_change(OldVsn, State, Extra) -> {ok, NewState} 175 | %% @end 176 | %%-------------------------------------------------------------------- 177 | code_change(_OldVsn, State, _Extra) -> 178 | {ok, State}. 179 | 180 | %%%=================================================================== 181 | %%% Internal functions 182 | %%%=================================================================== 183 | run_as_master() -> 184 | ?DBG("Run mnesia as MASTER on [~p]", [node()]), 185 | case mnesia:create_schema([node()]) of 186 | ok -> 187 | ok; 188 | {error, {_, {already_exists, _}}} -> 189 | ok; 190 | {error, E} -> 191 | ?ERR("failed to create Mnesia schema: ~p", [E]) 192 | end, 193 | ok = mnesia:start(), 194 | 195 | [create_table(T,A) || {T,A} <- ?MXTABLES], 196 | mnesia:wait_for_tables(mnesia:system_info(local_tables), infinity), 197 | clear_all_tables(). 198 | 199 | run_as_slave(Master) when is_list(Master) -> 200 | run_as_slave(list_to_atom(Master)); 201 | 202 | run_as_slave(Master) when is_atom(Master) -> 203 | ?DBG("Run mnesia as SLAVE on [~p] and link to the master [~p]", [node(), Master]), 204 | ok = stop(), 205 | ok = mnesia:delete_schema([node()]), 206 | ok = mnesia:start(), 207 | 208 | case mnesia:change_config(extra_db_nodes, [Master]) of 209 | [] -> 210 | throw({error, "failed to start Mnesia in slave mode"}); 211 | {ok, Cluster} -> 212 | % FIXME. 213 | ?DBG("Mnesia cluster: ~p", [Cluster]), 214 | ok 215 | end, 216 | 217 | %% there is no clear_all_tables() call, because in that case slave 218 | %% can clear all masters tables 219 | [copy_table(Table) || {Table, _Attrs} <- ?MXTABLES], 220 | mnesia:wait_for_tables(mnesia:system_info(local_tables), infinity). 221 | 222 | 223 | stop() -> 224 | mnesia:stop(), 225 | wait(10). % 10 sec 226 | 227 | wait(N) -> 228 | case mnesia:system_info(is_running) of 229 | no -> 230 | ok; 231 | stopping when N == 0 -> 232 | lager:error("Can not stop Mnesia"), 233 | cantstop; 234 | 235 | stopping -> 236 | timer:sleep(1000), 237 | wait(N - 1); 238 | X -> 239 | ?ERR("unhandled mnesia state: ~p", [X]), 240 | error 241 | end. 242 | 243 | create_table(T, A) -> 244 | case mnesia:create_table(T, A) of 245 | {atomic, ok} -> ok; 246 | {aborted, {already_exists, _}} -> ok; 247 | Error -> 248 | ?ERR("Got error on create table: ~p", [Error]), 249 | Error 250 | end. 251 | 252 | copy_table(T) -> 253 | case mnesia:add_table_copy(T, node(), ram_copies) of 254 | {atomic, ok} -> ok; 255 | {aborted, {already_exists, _, _}} -> ok; 256 | Error -> 257 | ?ERR("Got error on copy table: ~p", [Error]), 258 | Error 259 | end. 260 | 261 | clear_all_tables() -> 262 | [mnesia:clear_table(Table) || {Table, _Attts} <- ?MXTABLES]. 263 | -------------------------------------------------------------------------------- /src/mx_queue.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (C) 2015 Taras Halturin 2 | %% 3 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 4 | %% of this software and associated documentation files (the "Software"), to deal 5 | %% in the Software without restriction, including without limitation the rights 6 | %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | %% copies of the Software, and to permit persons to whom the Software is 8 | %% furnished to do so, subject to the following conditions: 9 | %% 10 | %% The above copyright notice and this permission notice shall be included in 11 | %% all copies or substantial portions of the Software. 12 | %% 13 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | %% THE SOFTWARE. 20 | %% 21 | 22 | -module(mx_queue). 23 | 24 | -export([new/1, put/2, get/1, pop/1, is_empty/1, len/1, total/1, name/1]). 25 | 26 | %% includes 27 | -include_lib("include/log.hrl"). 28 | -include_lib("include/mx.hrl"). 29 | 30 | -record(mxq,{queue = queue:new(), 31 | name :: non_neg_integer(), 32 | length = 0 :: non_neg_integer(), %% current len 33 | length_limit :: pos_integer(), 34 | threshold_low :: float(), 35 | threshold_high :: float(), 36 | total = 0, % total messages 37 | alarm}). 38 | 39 | -type mxq() :: #mxq{}. 40 | -export_type([mxq/0]). 41 | 42 | 43 | 44 | new(QueueName) when is_integer(QueueName) -> 45 | #mxq{ 46 | name = QueueName, 47 | length_limit = application:get_env(mx, queue_length_limit, 10000), 48 | threshold_low = application:get_env(mx, queue_low_threshold, 0.6), 49 | threshold_high = application:get_env(mx, queue_high_threshold, 0.8), 50 | alarm = alarm() 51 | }; 52 | new(_) -> 53 | {error, "non negative integer is expected"}. 54 | 55 | put(_, #mxq{queue = Q, length = L, length_limit = LM, alarm = F} = MXQ) when L > LM -> 56 | {defer, MXQ#mxq{alarm = F(mxq_alarm_queue_length_limit, Q)}}; 57 | 58 | put(Message, #mxq{queue = Q, length = L, threshold_high = LH, alarm = F} = MXQ) when L > LH -> 59 | MXQ#mxq{queue = queue:in(Message, Q), 60 | length = L + 1, 61 | alarm = F(mxq_alarm_threshold_high, Q)}; 62 | 63 | put(Message, #mxq{queue = Q, length = L, threshold_low = LL, alarm = F} = MXQ) when L > LL -> 64 | MXQ#mxq{queue = queue:in(Message, Q), 65 | length = L + 1, 66 | alarm = F(mxq_alarm_threshold_low, Q)}; 67 | 68 | put(Message, #mxq{queue = Q, length = L, alarm = F} = MXQ) when L == 0 -> 69 | MXQ#mxq{queue = queue:in(Message, Q), 70 | length = L + 1, 71 | alarm = F(mxq_alarm_has_message, Q)}; 72 | 73 | put(Message, #mxq{queue = Q, length = L} = MXQ) -> 74 | MXQ#mxq{queue = queue:in(Message, Q), 75 | length = L + 1}. 76 | 77 | 78 | get(#mxq{length = L} = MXQ) when L == 0 -> 79 | {empty, MXQ}; 80 | 81 | get(#mxq{queue = Q, length = L, alarm = F, total = T} = MXQ) -> 82 | {Message, Q1} = queue:out(Q), 83 | {Message, MXQ#mxq{ 84 | queue = Q1, 85 | length = L - 1, 86 | alarm = F(mxq_alarm_clear, Q), 87 | total = T + 1 88 | }}. 89 | 90 | pop(#mxq{length = L} = MXQ) when L == 0 -> 91 | {empty, MXQ}; 92 | 93 | pop(#mxq{queue = Q, length = L, alarm = F, total = T} = MXQ) -> 94 | {Message, Q1} = queue:out_r(Q), 95 | {Message, MXQ#mxq{ 96 | queue = Q1, 97 | length = L - 1, 98 | total = T + 1, 99 | alarm = F(mxq_alarm_clear, Q) 100 | }}. 101 | 102 | is_empty(#mxq{length = 0}) -> true; 103 | is_empty(#mxq{length = _}) -> false. 104 | 105 | len(#mxq{length = L}) -> L. 106 | total(#mxq{total = T}) -> T. 107 | name(#mxq{name = N}) -> N. 108 | 109 | alarm() -> 110 | alarm(alarm_clear). 111 | alarm(State) -> 112 | fun(Alarm, _) when Alarm =:= State -> alarm(State); 113 | (Alarm, _Q) when Alarm =/= State -> 114 | % ?LOG("Warinig: ~p -> ~p", [State, Alarm]), 115 | alarm(Alarm) 116 | end. 117 | 118 | -------------------------------------------------------------------------------- /src/mx_sup.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (C) 2015 Taras Halturin 2 | %% 3 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 4 | %% of this software and associated documentation files (the "Software"), to deal 5 | %% in the Software without restriction, including without limitation the rights 6 | %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | %% copies of the Software, and to permit persons to whom the Software is 8 | %% furnished to do so, subject to the following conditions: 9 | %% 10 | %% The above copyright notice and this permission notice shall be included in 11 | %% all copies or substantial portions of the Software. 12 | %% 13 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | %% THE SOFTWARE. 20 | %% 21 | 22 | -module(mx_sup). 23 | 24 | -behaviour(supervisor). 25 | 26 | -export([start_link/0]). 27 | 28 | %% Supervisor callbacks 29 | -export([init/1]). 30 | 31 | %% Helper macro for declaring children of supervisor 32 | -define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}). 33 | % -define(CHILD(I, supervisor), {I, {I, start_link, []}, permanent, infinity, supervisor, [I]}). 34 | 35 | %% API functions 36 | %% =================================================================== 37 | 38 | start_link() -> 39 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 40 | 41 | 42 | %% Supervisor callbacks 43 | %% =================================================================== 44 | 45 | init([]) -> 46 | {ok, { {one_for_one, 5, 10}, [ 47 | ?CHILD(mx_mnesia, worker), 48 | ?CHILD(mx_broker_sup, supervisor), 49 | ?CHILD(mx, worker) 50 | ]} }. 51 | 52 | -------------------------------------------------------------------------------- /test/ct/mx_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (C) 2015 Taras Halturin 2 | %% 3 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 4 | %% of this software and associated documentation files (the "Software"), to deal 5 | %% in the Software without restriction, including without limitation the rights 6 | %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | %% copies of the Software, and to permit persons to whom the Software is 8 | %% furnished to do so, subject to the following conditions: 9 | %% 10 | %% The above copyright notice and this permission notice shall be included in 11 | %% all copies or substantial portions of the Software. 12 | %% 13 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | %% THE SOFTWARE. 20 | %% 21 | 22 | -module(mx_SUITE). 23 | 24 | -include_lib("common_test/include/ct.hrl"). 25 | -include_lib("eunit/include/eunit.hrl"). 26 | 27 | -compile(export_all). 28 | 29 | %%-------------------------------------------------------------------- 30 | %% COMMON TEST CALLBACK FUNCTIONS 31 | %%-------------------------------------------------------------------- 32 | 33 | %%-------------------------------------------------------------------- 34 | %% Function: suite() -> Info 35 | %% 36 | %% Info = [tuple()] 37 | %% List of key/value pairs. 38 | %% 39 | %% Description: Returns list of tuples to set default properties 40 | %% for the suite. 41 | %% 42 | %% Note: The suite/0 function is only meant to be used to return 43 | %% default data values, not perform any other operations. 44 | %%-------------------------------------------------------------------- 45 | suite() -> 46 | [{timetrap,{seconds,30}}]. 47 | 48 | %%-------------------------------------------------------------------- 49 | %% Function: init_per_suite(Config0) -> 50 | %% Config1 | {skip,Reason} | {skip_and_save,Reason,Config1} 51 | %% 52 | %% Config0 = Config1 = [tuple()] 53 | %% A list of key/value pairs, holding the test case configuration. 54 | %% Reason = term() 55 | %% The reason for skipping the suite. 56 | %% 57 | %% Description: Initialization before the suite. 58 | %% 59 | %% Note: This function is free to add any key/value pairs to the Config 60 | %% variable, but should NOT alter/remove any existing entries. 61 | %%-------------------------------------------------------------------- 62 | init_per_suite(Config) -> 63 | % application:load(lager), 64 | % application:set_env(lager, handlers, 65 | % [ 66 | % {lager_console_backend, info}, 67 | % {lager_file_backend, [{file, "/tmp/mx.run.log"}, {level, debug}]} 68 | % ] 69 | % ), 70 | % application:ensure_all_started(lager), 71 | % application:ensure_all_started(mx), 72 | Config. 73 | 74 | %%-------------------------------------------------------------------- 75 | %% Function: end_per_suite(Config0) -> term() | {save_config,Config1} 76 | %% 77 | %% Config0 = Config1 = [tuple()] 78 | %% A list of key/value pairs, holding the test case configuration. 79 | %% 80 | %% Description: Cleanup after the suite. 81 | %%-------------------------------------------------------------------- 82 | end_per_suite(_Config) -> 83 | % application:stop(mx), 84 | ok. 85 | 86 | %%-------------------------------------------------------------------- 87 | %% Function: init_per_group(GroupName, Config0) -> 88 | %% Config1 | {skip,Reason} | {skip_and_save,Reason,Config1} 89 | %% 90 | %% GroupName = atom() 91 | %% Name of the test case group that is about to run. 92 | %% Config0 = Config1 = [tuple()] 93 | %% A list of key/value pairs, holding configuration data for the group. 94 | %% Reason = term() 95 | %% The reason for skipping all test cases and subgroups in the group. 96 | %% 97 | %% Description: Initialization before each test case group. 98 | %%-------------------------------------------------------------------- 99 | init_per_group(_GroupName, Config) -> 100 | Config. 101 | 102 | %%-------------------------------------------------------------------- 103 | %% Function: end_per_group(GroupName, Config0) -> 104 | %% term() | {save_config,Config1} 105 | %% 106 | %% GroupName = atom() 107 | %% Name of the test case group that is finished. 108 | %% Config0 = Config1 = [tuple()] 109 | %% A list of key/value pairs, holding configuration data for the group. 110 | %% 111 | %% Description: Cleanup after each test case group. 112 | %%-------------------------------------------------------------------- 113 | end_per_group(_GroupName, _Config) -> 114 | ok. 115 | 116 | %%-------------------------------------------------------------------- 117 | %% Function: init_per_testcase(TestCase, Config0) -> 118 | %% Config1 | {skip,Reason} | {skip_and_save,Reason,Config1} 119 | %% 120 | %% TestCase = atom() 121 | %% Name of the test case that is about to run. 122 | %% Config0 = Config1 = [tuple()] 123 | %% A list of key/value pairs, holding the test case configuration. 124 | %% Reason = term() 125 | %% The reason for skipping the test case. 126 | %% 127 | %% Description: Initialization before each test case. 128 | %% 129 | %% Note: This function is free to add any key/value pairs to the Config 130 | %% variable, but should NOT alter/remove any existing entries. 131 | %%-------------------------------------------------------------------- 132 | init_per_testcase(_TestCase, Config) -> 133 | Config. 134 | 135 | %%-------------------------------------------------------------------- 136 | %% Function: end_per_testcase(TestCase, Config0) -> 137 | %% term() | {save_config,Config1} | {fail,Reason} 138 | %% 139 | %% TestCase = atom() 140 | %% Name of the test case that is finished. 141 | %% Config0 = Config1 = [tuple()] 142 | %% A list of key/value pairs, holding the test case configuration. 143 | %% Reason = term() 144 | %% The reason for failing the test case. 145 | %% 146 | %% Description: Cleanup after each test case. 147 | %%-------------------------------------------------------------------- 148 | end_per_testcase(_TestCase, _Config) -> 149 | ok. 150 | 151 | %%-------------------------------------------------------------------- 152 | %% Function: groups() -> [Group] 153 | %% 154 | %% Group = {GroupName,Properties,GroupsAndTestCases} 155 | %% GroupName = atom() 156 | %% The name of the group. 157 | %% Properties = [parallel | sequence | Shuffle | {RepeatType,N}] 158 | %% Group properties that may be combined. 159 | %% GroupsAndTestCases = [Group | {group,GroupName} | TestCase] 160 | %% TestCase = atom() 161 | %% The name of a test case. 162 | %% Shuffle = shuffle | {shuffle,Seed} 163 | %% To get cases executed in random order. 164 | %% Seed = {integer(),integer(),integer()} 165 | %% RepeatType = repeat | repeat_until_all_ok | repeat_until_all_fail | 166 | %% repeat_until_any_ok | repeat_until_any_fail 167 | %% To get execution of cases repeated. 168 | %% N = integer() | forever 169 | %% 170 | %% Description: Returns a list of test case group definitions. 171 | %%-------------------------------------------------------------------- 172 | groups() -> 173 | [ 174 | 175 | {direct,[sequence], [ 176 | direct_test_Recievers_1_Messages_1 177 | % direct_test_Recievers_1_Messages_1000, 178 | % direct_test_Recievers_1000_Messages_1, 179 | % direct_test_Recievers_1000_Messages_100 180 | ]}, 181 | 182 | {direct,[parallel], [ 183 | 184 | direct_test_Recievers_1_Messages_1000 185 | % direct_test_Recievers_1000_Messages_1, 186 | % direct_test_Recievers_1_Messages_1000, 187 | % direct_test_Recievers_1000_Messages_1, 188 | % direct_test_Recievers_1_Messages_1000, 189 | % direct_test_Recievers_1000_Messages_1 190 | 191 | ]}, 192 | 193 | 194 | {channel,[sequence], [ 195 | channel_test_Subscribers_1_Messages_1 196 | % channel_test_Subscribers_1_Messages_1000 197 | % channel_test_Subscribers_1000_Messages_1 198 | % channel_test_Subscribers_1000_Messages_10 199 | ]}, 200 | 201 | {channel,[parallel], [ 202 | % channel_test_Subscribers_1_Messages_1000, 203 | % channel_test_Subscribers_1_Messages_1000, 204 | % channel_test_Subscribers_1_Messages_1000, 205 | % channel_test_Subscribers_1_Messages_1000 206 | % channel_test_Subscribers_1000_Messages_1, 207 | % channel_test_Subscribers_1000_Messages_1, 208 | % channel_test_Subscribers_1000_Messages_1, 209 | % channel_test_Subscribers_1000_Messages_1, 210 | % channel_test_Subscribers_1000_Messages_1 211 | 212 | % channel_test_Subscribers_1_Messages_1000 213 | % channel_test_Subscribers_1000_Messages_1 214 | % channel_test_Subscribers_1000_Messages_10 215 | ]}, 216 | {pool, [sequence],[ 217 | pool_test_1_1 218 | ]} 219 | ]. 220 | 221 | 222 | 223 | %%-------------------------------------------------------------------- 224 | %% Function: all() -> GroupsAndTestCases | {skip,Reason} 225 | %% 226 | %% GroupsAndTestCases = [{group,GroupName} | TestCase] 227 | %% GroupName = atom() 228 | %% Name of a test case group. 229 | %% TestCase = atom() 230 | %% Name of a test case. 231 | %% Reason = term() 232 | %% The reason for skipping all groups and test cases. 233 | %% 234 | %% Description: Returns the list of groups and test cases that 235 | %% are to be executed. 236 | %%-------------------------------------------------------------------- 237 | 238 | all() -> 239 | [ 240 | {group, direct}, 241 | {group, channel}, % pub/sub 242 | {group, pool} % workers pool 243 | ]. 244 | 245 | 246 | -define(MX(A,B), gen_server:call({mx, 'mxnode01@127.0.0.1'}, {A,B})). 247 | -define(MX(A,B,C), gen_server:call({mx, 'mxnode01@127.0.0.1'}, {A,B,C})). 248 | -define(MX(A,B,C,D), gen_server:call({mx, 'mxnode01@127.0.0.1'}, {A,B,C,D})). 249 | 250 | client(ID, KeyTo, N, DonePid) -> 251 | Self = self(), 252 | Pid = erlang:spawn_link(fun() -> 253 | ClientName = lists:flatten(io_lib:format("client-~p-~p", [ID,self()])), 254 | ct:print("registering: ~p", [ClientName]), 255 | case ?MX(register_client, ClientName, [{monitor, true}]) of 256 | {duplicate, ClientKey} -> 257 | ?MX(set, ClientKey, [{handler, self()}, {monitor, true}]); 258 | {clientkey, ClientKey} -> 259 | pass 260 | end, 261 | Self ! ClientKey, 262 | case KeyTo of 263 | <<$#, _/binary>> = Key -> 264 | ?MX(subscribe,ClientKey, Key); 265 | <<$@, _/binary>> = Key -> 266 | ?MX(join,ClientKey, Key); 267 | _ -> 268 | ct:print("skip the key!"), 269 | pass 270 | end, 271 | Loop = fun Loop(I) when N > I -> 272 | receive 273 | {'$gen_call', ReplyTo, _M} -> 274 | gen_server:reply(ReplyTo, ok), 275 | Loop(I); 276 | {'$gen_cast', _M} -> 277 | Loop(I); 278 | {mx, M} -> 279 | ct:print("GOT MX MESSAGE: (~p): ~p", [self(), M]), 280 | Loop(I+1); 281 | stop -> 282 | ok; 283 | M -> 284 | ct:print("GOT UNKNOWN MESSAGE: (~p): ~p", [self(), M]), 285 | Loop(I) 286 | end; 287 | 288 | Loop(_I) -> 289 | DonePid ! {done, ID} 290 | 291 | end, 292 | DonePid ! {ready, ID}, 293 | Loop(0) 294 | end), 295 | 296 | receive 297 | ClientKey -> 298 | {Pid, ClientKey} 299 | end. 300 | 301 | 302 | handler_responses(NumClients) -> 303 | Finish = self(), 304 | erlang:spawn_link(fun() -> 305 | F = fun Done(N,N) when N == NumClients-> 306 | ct:print("DONE!"), 307 | Finish ! finish; 308 | Done(N,NDone) when N == NumClients, N > NDone -> 309 | receive 310 | {done, _ID} -> 311 | Done(N, NDone+1) 312 | after 25000 -> 313 | Finish ! timeout_done 314 | end; 315 | Done(N,_) -> 316 | receive 317 | {ready, _ID} when N == NumClients -1 -> 318 | Finish ! ready, 319 | Done(NumClients, 0); 320 | {ready, _ID} -> 321 | Done(N+1, 0) 322 | after 5000 -> 323 | Finish ! timeout_ready 324 | end 325 | end, 326 | F(0,0) 327 | end). 328 | 329 | 330 | direct_test(NumClients, NMessages) -> 331 | DonePid = handler_responses(NumClients), 332 | Clients = [client(ID, none, NMessages, DonePid) || ID <- lists:seq(1,NumClients)], 333 | 334 | receive 335 | ready -> 336 | [[?MX(send, Key, "hello-"++integer_to_list(N)) || N <- lists:seq(1,NMessages)] || {_, Key} <- Clients] 337 | end, 338 | 339 | ?assert(receive finish -> true; X -> X after 26000 -> timeout_finish end), 340 | ok. 341 | 342 | channel_test(NumClients, NMessages) -> 343 | 344 | DonePid = handler_responses(NumClients), 345 | 346 | % create publisher 347 | ChannelName = lists:flatten(io_lib:format("Channel_test_~p", [self()])), 348 | PublisherName = lists:flatten(io_lib:format("Publisher_~p", [self()])), 349 | case ?MX(register_client, PublisherName, [{monitor, true}]) of 350 | {duplicate, PublisherClientKey} -> 351 | ?MX(set, PublisherClientKey, [{handler, self()}, {monitor, true}]); 352 | {clientkey, PublisherClientKey} -> 353 | pass 354 | end, 355 | {_, PublisherChannelKey} = ?MX(register_channel, ChannelName, PublisherClientKey, [{own, true}]), 356 | 357 | % create subscribers 358 | 359 | _Clients = [client(ID, PublisherChannelKey, NMessages, DonePid) || ID <- lists:seq(1,NumClients)], 360 | receive 361 | ready -> 362 | [?MX(send, PublisherChannelKey, "hello-"++integer_to_list(NM)) || NM <- lists:seq(1,NMessages)] 363 | end, 364 | 365 | ?assert(receive finish -> true; X -> X after 26000 -> timeout_finish end), 366 | ok. 367 | 368 | direct_test_Recievers_1_Messages_1(_X) -> 369 | direct_test(1,1). 370 | 371 | direct_test_Recievers_1_Messages_1000(_X) -> 372 | direct_test(1,1000). 373 | 374 | direct_test_Recievers_1000_Messages_1(_X) -> 375 | direct_test(1000,1). 376 | 377 | direct_test_Recievers_1000_Messages_100(_X) -> 378 | direct_test(1000,100). 379 | 380 | channel_test_Subscribers_1_Messages_1(_X) -> 381 | % NumClients = 1, % NMessages = 1, 382 | channel_test(1, 1), 383 | ok. 384 | 385 | channel_test_Subscribers_1_Messages_1000(_X) -> 386 | channel_test(1, 20000), 387 | ok. 388 | 389 | channel_test_Subscribers_1000_Messages_1(_X) -> 390 | channel_test(1000, 1), 391 | ok. 392 | 393 | channel_test_Subscribers_1000_Messages_10(_X) -> 394 | channel_test(1000, 10), 395 | ok. 396 | 397 | pool_test_1_1(_X) -> 398 | ok. 399 | 400 | % test1(_Config) -> 401 | % % mnesia:dirty_select(mx_table_client, [{'_',[],['$_']}]). 402 | % % mnesia:dirty_select(mx_table_defer, [{'_',[],['$_']}]). 403 | 404 | % mx:register(client, "Client1"), 405 | % mx:register(client, "Client2"), 406 | % mx:register(client, "Client3"), 407 | % mx:register(client, "Client4"), 408 | 409 | % mx:register(channel, "Channel1", <<42,95,236,108,64,253,36,90,36,60,50,179,219,73,1,61,69>>), 410 | 411 | % mx:subscribe(<<42,47,231,226,247,215,105,46,217,181,173,247,93,206,248,15,209>>, "Channel1"), 412 | % mx:subscribe(<<42,150,205,12,62,219,183,158,157,112,8,45,243,123,103,79,105>>, "Channel1"), 413 | % mx:subscribe(<<42,215,87,81,75,153,148,193,185,32,38,111,156,162,183,64,229>>, "Channel1"), 414 | 415 | % mx:register(pool, "Pool1", <<42,95,236,108,64,253,36,90,36,60,50,179,219,73,1,61,69>>), 416 | % mx:join(<<42,47,231,226,247,215,105,46,217,181,173,247,93,206,248,15,209>>, "Pool1"), 417 | % mx:join(<<42,150,205,12,62,219,183,158,157,112,8,45,243,123,103,79,105>>, "Pool1"), 418 | % mx:join(<<42,215,87,81,75,153,148,193,185,32,38,111,156,162,183,64,229>>, "Pool1"), 419 | 420 | % mx:send(<<42,95,236,108,64,253,36,90,36,60,50,179,219,73,1,61,69>>, "hi body"), 421 | % mx:send(<<35,110,121,48,217,228,92,240,133,25,235,94,163,50,133,167,55>>, "hi channel"), 422 | % mx:send(<<64,92,61,224,106,0,184,142,80,34,47,10,105,152,188,54,23>>, "hi pool"), 423 | 424 | % ok. 425 | -------------------------------------------------------------------------------- /test/mx_tests.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (C) 2015 Taras Halturin 2 | %% 3 | %% Permission is hereby granted, free of charge, to any person obtaining a copy 4 | %% of this software and associated documentation files (the "Software"), to deal 5 | %% in the Software without restriction, including without limitation the rights 6 | %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | %% copies of the Software, and to permit persons to whom the Software is 8 | %% furnished to do so, subject to the following conditions: 9 | %% 10 | %% The above copyright notice and this permission notice shall be included in 11 | %% all copies or substantial portions of the Software. 12 | %% 13 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | %% THE SOFTWARE. 20 | %% 21 | 22 | -module(mx_tests). 23 | 24 | -include_lib("eunit/include/eunit.hrl"). 25 | -compile(export_all). 26 | 27 | mx_test() -> 28 | 29 | 30 | ok. 31 | 32 | --------------------------------------------------------------------------------