├── .gitignore ├── Makefile ├── README.md ├── rebar ├── rebar.config └── src ├── redgrid.app.src └── redgrid.erl /.gitignore: -------------------------------------------------------------------------------- 1 | ebin 2 | *.swp 3 | src/*.swp 4 | erl_crash.dump 5 | deps 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | ./rebar get-deps update-deps compile 3 | 4 | clean: 5 | ./rebar clean 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## About 2 | 3 | Redgrid performs automatic Erlang node discovery through a shared Redis pub/sub channel. 4 | 5 | ## Build 6 | 7 | $ make 8 | 9 | ## Run 10 | 11 | #### Start Redgrid with no arguments 12 | 13 | $ erl -pa ebin deps/*/ebin 14 | 1> redgrid:start_link(). 15 | {ok,<0.33.0>} 16 | 2> whereis(redgrid). 17 | <0.33.0> 18 | 19 | #### Start Redgrid with custom Redis url 20 | 21 | $ erl -pa ebin deps/*/ebin 22 | 1> redgrid:start_link([{redis_url, "redis://localhost:6379/"}]). 23 | {ok,<0.33.0>} 24 | 25 | #### Start Redgrid as a non-registered (anonymous) process 26 | 27 | $ erl -pa ebin deps/*/ebin 28 | 1> redgrid:start_link([anonymous]). 29 | {ok,<0.33.0>} 30 | 31 | #### Start with arbitrary meta data 32 | 33 | $ erl -pa ebin deps/*/ebin 34 | 1> redgrid:start_link([{<<"foo">>, <<"bar">>}]). 35 | {ok,<0.33.0>} 36 | 37 | ## Example 38 | 39 | #### TAB 1 40 | 41 | Start **foo** node 42 | 43 | $ erl -pa ebin deps/*/ebin -name foo@`hostname` 44 | (foo@Jacob-Vorreuters-MacBook-Pro.local)1> redgrid:start_link(). 45 | {ok,<0.39.0>} 46 | 47 | #### TAB 2 48 | 49 | Start **bar** node 50 | 51 | $ erl -pa ebin deps/*/ebin -name bar@`hostname` 52 | (bar@Jacob-Vorreuters-MacBook-Pro.local)1> redgrid:start_link(). 53 | {ok,<0.39.0>} 54 | 55 | View registered nodes 56 | 57 | (bar@Jacob-Vorreuters-MacBook-Pro.local)2> redgrid:nodes(). 58 | [{'bar@Jacob-Vorreuters-MacBook-Pro.local',[<<"ip">>, <<"localhost">>]}, 59 | {'foo@Jacob-Vorreuters-MacBook-Pro.local',[{<<"ip">>, <<"localhost">>}]}] 60 | (bar@Jacob-Vorreuters-MacBook-Pro.local)3> [node()|nodes()]. 61 | ['bar@Jacob-Vorreuters-MacBook-Pro.local', 'foo@Jacob-Vorreuters-MacBook-Pro.local'] 62 | 63 | Update meta data for **bar** node 64 | 65 | (bar@Jacob-Vorreuters-MacBook-Pro.local)4> redgrid:update_meta([{weight, 50}]). 66 | ok 67 | 68 | #### TAB 1 69 | 70 | View registered nodes (including updated meta data for **bar**) 71 | 72 | (foo@Jacob-Vorreuters-MacBook-Pro.local)2> redgrid:nodes(). 73 | [{'foo@Jacob-Vorreuters-MacBook-Pro.local',[<<"ip">>, <<"localhost">>]}, 74 | {'bar@Jacob-Vorreuters-MacBook-Pro.local',[{<<"ip">>,<<"localhost">>}, 75 | {<<"weight">>, <<"50">>}]}] 76 | 77 | 78 | ## Diagram 79 | 80 | This diagram demonstrates what's happening underneath the covers when the **foo** node joins a cluster comprised of a single node (**bar**). 81 | 82 | ![Redgrid](http://i.imgur.com/BbYxC.png) 83 | 84 | ## ENV VARS 85 | 86 | LOCAL_IP: The IP written to Redis to which other nodes will attempt to connect 87 | DOMAIN: Used to build Redis key and channel names 88 | VERSION: Used to build Redis key and channel names 89 | -------------------------------------------------------------------------------- /rebar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jkvor/redgrid/5182def5a8d3cd0fdf1f548579454b16d0ccd091/rebar -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {deps, [{redo, "1.0", {git, "git://github.com/JacobVorreuter/redo", "HEAD"}}]}. 2 | -------------------------------------------------------------------------------- /src/redgrid.app.src: -------------------------------------------------------------------------------- 1 | {application, redgrid, 2 | [ 3 | {description, "Automatic Erlang node discovery via Redis"}, 4 | {vsn, "1.1"}, 5 | {registered, []}, 6 | {applications, [kernel,stdlib]}, 7 | {env, []} 8 | ]}. 9 | -------------------------------------------------------------------------------- /src/redgrid.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2011 Jacob Vorreuter 2 | %% 3 | %% Permission is hereby granted, free of charge, to any person 4 | %% obtaining a copy of this software and associated documentation 5 | %% files (the "Software"), to deal in the Software without 6 | %% restriction, including without limitation the rights to use, 7 | %% copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | %% copies of the Software, and to permit persons to whom the 9 | %% Software is furnished to do so, subject to the following 10 | %% conditions: 11 | %% 12 | %% The above copyright notice and this permission notice shall be 13 | %% included in all copies or substantial portions of the Software. 14 | %% 15 | %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | %% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | %% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | %% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | %% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | %% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | %% OTHER DEALINGS IN THE SOFTWARE. 23 | -module(redgrid). 24 | -behaviour(gen_server). 25 | 26 | %% gen_server callbacks 27 | -export([init/1, handle_call/3, handle_cast/2, 28 | handle_info/2, terminate/2, code_change/3]). 29 | 30 | -export([start_link/0, start_link/1, 31 | update_meta/1, update_meta/2, 32 | nodes/0, nodes/1]). 33 | 34 | -record(state, {pub_pid, sub_pid, pubsub_chan, sub_ref, bin_node, 35 | ip, domain, version, meta, nodes}). 36 | 37 | start_link() -> 38 | start_link([]). 39 | 40 | start_link(Meta) when is_list(Meta) -> 41 | case lists:member(anonymous, Meta) of 42 | true -> 43 | gen_server:start_link(?MODULE, [Meta], []); 44 | false -> 45 | gen_server:start_link({local, ?MODULE}, ?MODULE, [Meta], []) 46 | end. 47 | 48 | update_meta(Meta) when is_list(Meta) -> 49 | update_meta(?MODULE, Meta). 50 | 51 | update_meta(NameOrPid, Meta) when is_list(Meta) -> 52 | gen_server:cast(NameOrPid, {update_meta, Meta}). 53 | 54 | nodes() -> 55 | ?MODULE:nodes(?MODULE). 56 | 57 | nodes(NameOrPid) -> 58 | gen_server:call(NameOrPid, nodes, 5000). 59 | 60 | %%==================================================================== 61 | %% gen_server callbacks 62 | %%==================================================================== 63 | 64 | %%-------------------------------------------------------------------- 65 | %% Function: init(Args) -> {ok, State} | 66 | %% {ok, State, Timeout} | 67 | %% ignore | 68 | %% {stop, Reason} 69 | %% Description: Initiates the server 70 | %%-------------------------------------------------------------------- 71 | init([Meta]) -> 72 | BinNode = atom_to_binary(node(), utf8), 73 | Ip = local_ip(), 74 | Domain = domain(), 75 | Version = version(), 76 | 77 | Opts = redo_uri:parse(proplists:get_value(redis_url, Meta, "redis://localhost:6379/")), 78 | log(debug, "Redis opts: ~p~n", [Opts]), 79 | {ok, Pub} = redo:start_link(undefined, Opts), 80 | {ok, Sub} = redo:start_link(undefined, Opts), 81 | Chan = pubsub_channel(Domain, Version), 82 | Ref = redo:subscribe(Sub, Chan), 83 | 84 | log(debug, "State: node=~s ip=~s domain=~s version=~s~n", [BinNode, Ip, Domain, Version]), 85 | {ok, #state{pub_pid = Pub, 86 | sub_pid = Sub, 87 | pubsub_chan = Chan, 88 | sub_ref = Ref, 89 | bin_node = BinNode, 90 | ip = Ip, 91 | domain = Domain, 92 | version = Version, 93 | meta = Meta, 94 | nodes = dict:new()}}. 95 | 96 | %%-------------------------------------------------------------------- 97 | %% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | 98 | %% {reply, Reply, State, Timeout} | 99 | %% {noreply, State} | 100 | %% {noreply, State, Timeout} | 101 | %% {stop, Reason, Reply, State} | 102 | %% {stop, Reason, State} 103 | %% Description: Handling call messages 104 | %%-------------------------------------------------------------------- 105 | handle_call(nodes, _From, #state{ip=Ip, meta=Meta, nodes=Nodes}=State) -> 106 | Node = {node(), [<<"ip">>, to_bin(Ip) | [to_bin(M) || M <- Meta]]}, 107 | {reply, [Node|dict:to_list(Nodes)], State}; 108 | 109 | handle_call(_Msg, _From, State) -> 110 | {reply, unknown_message, State}. 111 | 112 | %%-------------------------------------------------------------------- 113 | %% Function: handle_cast(Msg, State) -> {noreply, State} | 114 | %% {noreply, State, Timeout} | 115 | %% {stop, Reason, State} 116 | %% Description: Handling cast messages 117 | %%-------------------------------------------------------------------- 118 | handle_cast({update_meta, Meta}, State) -> 119 | State1 = State#state{meta=Meta}, 120 | ok = register_node(State1), 121 | {noreply, State1}; 122 | 123 | handle_cast(_Msg, State) -> 124 | {noreply, State}. 125 | 126 | %%-------------------------------------------------------------------- 127 | %% Function: handle_info(Info, State) -> {noreply, State} | 128 | %% {noreply, State, Timeout} | 129 | %% {stop, Reason, State} 130 | %% Description: Handling all non call/cast messages 131 | %%-------------------------------------------------------------------- 132 | handle_info({Ref, [<<"subscribe">>, Chan, _Subscribers]}, #state{sub_ref=Ref, pubsub_chan=Chan}=State) -> 133 | self() ! register, 134 | ping_nodes(State), 135 | {noreply, State}; 136 | 137 | 138 | handle_info({Ref, [<<"message">>, Chan, <<"ping">>]}, #state{sub_ref=Ref, pubsub_chan=Chan}=State) -> 139 | register_node(State), 140 | {noreply, State}; 141 | 142 | handle_info({Ref, [<<"message">>, Chan, Key]}, #state{sub_ref=Ref, pubsub_chan=Chan, pub_pid=Pid, domain=Domain, version=Version, nodes=Nodes}=State) -> 143 | case connect(Pid, Key, length(Domain), length(Version)) of 144 | undefined -> 145 | {noreply, State}; 146 | {Node, Meta} -> 147 | Nodes1 = dict:store(Node, Meta, Nodes), 148 | {noreply, State#state{nodes=Nodes1}} 149 | end; 150 | 151 | handle_info(register, State) -> 152 | register_node(State), 153 | erlang:send_after(10000, self(), register), 154 | {noreply, State}; 155 | 156 | handle_info(_Info, State) -> 157 | {noreply, State}. 158 | 159 | %%-------------------------------------------------------------------- 160 | %% Function: terminate(Reason, State) -> void() 161 | %% Description: This function is called by a gen_server when it is about to 162 | %% terminate. It should be the opposite of Module:init/1 and do any necessary 163 | %% cleaning up. When it returns, the gen_server terminates with Reason. 164 | %% The return value is ignored. 165 | %%-------------------------------------------------------------------- 166 | terminate(_Reason, _State) -> 167 | ok. 168 | 169 | %%-------------------------------------------------------------------- 170 | %% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} 171 | %% Description: Convert process state when code is changed 172 | %%-------------------------------------------------------------------- 173 | code_change(_OldVsn, State, _Extra) -> 174 | {ok, State}. 175 | 176 | %%-------------------------------------------------------------------- 177 | %% Internal functions 178 | %%-------------------------------------------------------------------- 179 | register_node(#state{pub_pid=Pid, pubsub_chan=Chan, ip=Ip, domain=Domain, version=Version, bin_node=BinNode, meta=Meta}) -> 180 | Key = iolist_to_binary([<<"redgrid:">>, Domain, <<":">>, Version, <<":">>, BinNode]), 181 | Cmds = [["HMSET", Key, "ip", Ip | flatten_proplist(Meta)], ["EXPIRE", Key, "60"]], 182 | case redo:cmd(Pid, Cmds) of 183 | [<<"OK">>, 1] -> 184 | redo:cmd(Pid, ["PUBLISH", Chan, Key]); 185 | {error, Reason} -> 186 | log(error, "Lost connection to redis: ~p~n", [Reason]) 187 | end. 188 | 189 | ping_nodes(#state{pub_pid=Pid, pubsub_chan=Chan}) -> 190 | redo:cmd(Pid, ["PUBLISH", Chan, "ping"]), 191 | ok. 192 | 193 | connect(Pid, Key, DomainSize, VersionSize) -> 194 | case Key of 195 | <<"redgrid:", _:DomainSize/binary, ":", _:VersionSize/binary, ":", BinNode/binary>> -> 196 | connect1(Pid, Key, binary_to_list(BinNode)); 197 | _ -> 198 | log(debug, "Attempting to connect to invalid key: ~s~n", [Key]), 199 | undefined 200 | end. 201 | 202 | connect1(Pid, Key, StrNode) -> 203 | Node = list_to_atom(StrNode), 204 | case node() == Node of 205 | true -> 206 | undefined; 207 | false -> 208 | connect2(Pid, Key, StrNode, Node) 209 | end. 210 | 211 | connect2(Pid, Key, StrNode, Node) -> 212 | case get_node(Pid, Key) of 213 | Props when is_list(Props) -> 214 | case proplists:get_value(<<"ip">>, Props) of 215 | undefined -> 216 | log(debug, "Failed to retrieve IP from key props ~p~n", [Props]), 217 | undefined; 218 | Ip -> 219 | connect3(StrNode, Node, Ip, Props) 220 | end; 221 | Other -> 222 | log(debug, "Failed to lookup node ip with key ~s: ~p~n", [Key, Other]), 223 | undefined 224 | end. 225 | 226 | connect3(StrNode, Node, Ip, Props) -> 227 | case inet:getaddr(binary_to_list(Ip), inet) of 228 | {ok, Addr} -> 229 | case re:run(StrNode, ".*@(.*)$", [{capture, all_but_first, list}]) of 230 | {match, [Host]} -> 231 | connect4(Node, Addr, Host, Props); 232 | Other -> 233 | log(debug, "Failed to parse host ~s: ~p~n", [StrNode, Other]), 234 | undefined 235 | end; 236 | Err -> 237 | log(debug, "Failed to resolve ip ~s: ~p~n", [Ip, Err]), 238 | undefined 239 | end. 240 | 241 | connect4(Node, Addr, Host, Props) -> 242 | inet_db:add_host(Addr, [Host]), 243 | case net_adm:ping(Node) of 244 | pong -> 245 | log(debug, "Monitoring node ~p~n", [Node]), 246 | erlang:monitor_node(Node, true), 247 | {Node, Props}; 248 | pang -> 249 | log(debug, "Ping failed ~p ~s -> ~p~n", [Node, Addr, Host]), 250 | {Node, Props} 251 | end. 252 | 253 | get_node(Pid, Key) -> 254 | case redo:cmd(Pid, [<<"HGETALL">>, Key]) of 255 | List when is_list(List) -> list_to_proplist(List); 256 | Err -> Err 257 | end. 258 | 259 | flatten_proplist(Props) -> 260 | flatten_proplist(Props, []). 261 | 262 | flatten_proplist([], Acc) -> 263 | Acc; 264 | 265 | flatten_proplist([{Key, Val}|Tail], Acc) -> 266 | flatten_proplist(Tail, [Key, Val | Acc]). 267 | 268 | list_to_proplist(List) -> 269 | list_to_proplist(List, []). 270 | 271 | list_to_proplist([], Acc) -> 272 | Acc; 273 | 274 | list_to_proplist([Key, Val|Tail], Acc) -> 275 | list_to_proplist(Tail, [{Key, Val}|Acc]). 276 | 277 | local_ip() -> 278 | env_var(local_ip, "LOCAL_IP", "127.0.0.1"). 279 | 280 | domain() -> 281 | env_var(domain, "DOMAIN", ""). 282 | 283 | version() -> 284 | env_var(version, "VERSION", ""). 285 | 286 | pubsub_channel(Domain, Version) -> 287 | iolist_to_binary([<<"redgrid:">>, Domain, <<":">>, Version]). 288 | 289 | to_bin(List) when is_list(List) -> 290 | list_to_binary(List); 291 | 292 | to_bin(Bin) when is_binary(Bin) -> 293 | Bin; 294 | 295 | to_bin(Atom) when is_atom(Atom) -> 296 | to_bin(atom_to_list(Atom)); 297 | 298 | to_bin(Tuple) when is_tuple(Tuple) -> 299 | list_to_tuple([to_bin(T) || T <- tuple_to_list(Tuple)]); 300 | 301 | to_bin(Int) when is_integer(Int) -> 302 | to_bin(integer_to_list(Int)). 303 | 304 | env_var(AppKey, EnvName, Default) -> 305 | case application:get_env(?MODULE, AppKey) of 306 | {ok, Val} when is_list(Val); is_binary(Val) -> Val; 307 | _ -> 308 | case os:getenv(EnvName) of 309 | false -> Default; 310 | Val -> Val 311 | end 312 | end. 313 | 314 | log(MsgLvl, Fmt, Args) when is_atom(MsgLvl); is_list(MsgLvl) -> 315 | log(log_to_int(MsgLvl), Fmt, Args); 316 | 317 | log(MsgLvl, Fmt, Args) when is_integer(MsgLvl), is_list(Fmt), is_list(Args) -> 318 | SysLvl = 319 | case os:getenv("LOGLEVEL") of 320 | false -> 1; 321 | Val -> log_to_int(Val) 322 | end, 323 | MsgLvl >= SysLvl andalso error_logger:info_msg(Fmt, Args). 324 | 325 | log_to_int(debug) -> 0; 326 | log_to_int("debug") -> 0; 327 | log_to_int("0") -> 0; 328 | log_to_int(info) -> 1; 329 | log_to_int("info") -> 1; 330 | log_to_int("1") -> 1; 331 | log_to_int(warning) -> 2; 332 | log_to_int("warning") -> 2; 333 | log_to_int("2") -> 2; 334 | log_to_int(error) -> 3; 335 | log_to_int("error") -> 3; 336 | log_to_int("3") -> 3; 337 | log_to_int(fatal) -> 4; 338 | log_to_int("fatal") -> 4; 339 | log_to_int("4") -> 4; 340 | log_to_int(_) -> 1. 341 | --------------------------------------------------------------------------------