├── .gitignore ├── LICENSE ├── README.md ├── rebar.config ├── rebar.lock └── src ├── loquat.app.src ├── loquat_app.erl ├── loquat_plumtree_backend.erl └── loquat_sup.erl /.gitignore: -------------------------------------------------------------------------------- 1 | .rebar3 2 | _* 3 | .eunit 4 | *.o 5 | *.beam 6 | *.plt 7 | *.swp 8 | *.swo 9 | .erlang.cookie 10 | ebin 11 | log 12 | erl_crash.dump 13 | .rebar 14 | logs 15 | _build 16 | .idea 17 | rebar3.crashdump 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Christopher Meiklejohn . 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | * The names of its contributors may not be used to endorse or promote 16 | products derived from this software without specific prior written 17 | permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | loquat 2 | ===== 3 | 4 | An OTP application 5 | 6 | Build 7 | ----- 8 | 9 | $ rebar3 compile 10 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {min_otp_version, "19.0"}. 2 | {erl_opts, [debug_info, 3 | warnings_as_errors, 4 | {platform_define, "^[0-9]+", namespaced_types}, 5 | {parse_transform, lager_transform}]}. 6 | {eunit_opts, [verbose, {report,{eunit_surefire,[{dir,"."}]}}]}. 7 | {edoc_opts, [{preprocess, true}]}. 8 | {deps, [ 9 | lager, 10 | plumtree, 11 | partisan 12 | ]}. 13 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.1.0", 2 | [{<<"acceptor_pool">>,{pkg,<<"acceptor_pool">>,<<"1.0.0-rc.0">>},1}, 3 | {<<"goldrush">>,{pkg,<<"goldrush">>,<<"0.1.9">>},1}, 4 | {<<"lager">>,{pkg,<<"lager">>,<<"3.2.4">>},0}, 5 | {<<"lasp_support">>,{pkg,<<"lasp_support">>,<<"0.0.1">>},1}, 6 | {<<"partisan">>,{pkg,<<"partisan">>,<<"0.2.0">>},0}, 7 | {<<"plumtree">>,{pkg,<<"plumtree">>,<<"0.1.0">>},0}, 8 | {<<"rand_compat">>,{pkg,<<"rand_compat">>,<<"0.0.1">>},1}, 9 | {<<"riak_dt">>,{pkg,<<"riak_dt">>,<<"2.1.1">>},2}, 10 | {<<"time_compat">>,{pkg,<<"time_compat">>,<<"0.0.1">>},1}, 11 | {<<"types">>,{pkg,<<"types">>,<<"0.0.6">>},1}]}. 12 | [ 13 | {pkg_hash,[ 14 | {<<"acceptor_pool">>, <<"679D741DF87FC13599B1AEF2DF8F78F1F880449A6BEFAB7C44FB6FAE0E92A2DE">>}, 15 | {<<"goldrush">>, <<"F06E5D5F1277DA5C413E84D5A2924174182FB108DABB39D5EC548B27424CD106">>}, 16 | {<<"lager">>, <<"A6DEB74DAE7927F46BD13255268308EF03EB206EC784A94EAF7C1C0F3B811615">>}, 17 | {<<"lasp_support">>, <<"28E79936E10C5BFB9B8246AC1F4F8385224D4E1E288F318C6D51C42D277A7A6E">>}, 18 | {<<"partisan">>, <<"8E44E20FD2E904D53D8FF2287C1397F71BC9BFF36B4DB6996C207AAFEFBE3ED1">>}, 19 | {<<"plumtree">>, <<"6FD05600AFE22706ED2C4CD64B37FB155363B4FAD922F538FB8376A409569A9E">>}, 20 | {<<"rand_compat">>, <<"624B590931D27252D0BCF710211699DB3695540706F57D2E91B918A17AB58839">>}, 21 | {<<"riak_dt">>, <<"56B1898A543C561994F5D052FDEA972525CA98F46FDB3DCDB7366E4F92EE8F54">>}, 22 | {<<"time_compat">>, <<"23FE0AD1FDF3B5B88821B2D04B4B5E865BF587AE66056D671FE0F53514ED8139">>}, 23 | {<<"types">>, <<"DF10A65C2A06B79D300BD74DEB783FA8E9B16A1B5AFC75F26C0C7302D558EEE9">>}]} 24 | ]. 25 | -------------------------------------------------------------------------------- /src/loquat.app.src: -------------------------------------------------------------------------------- 1 | {application, loquat, 2 | [{description, "Loquat: A System for Large-Scale Actor Programming"}, 3 | {vsn, "0.0.1"}, 4 | {registered, []}, 5 | {mod, { loquat_app, []}}, 6 | {applications, 7 | [kernel, 8 | stdlib 9 | ]}, 10 | {env,[]}, 11 | {modules, []}, 12 | 13 | {maintainers, []}, 14 | {licenses, []}, 15 | {links, []} 16 | ]}. 17 | -------------------------------------------------------------------------------- /src/loquat_app.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2017 Christopher S. Meiklejohn. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | -module(loquat_app). 22 | -author("Christopher S. Meiklejohn "). 23 | 24 | -behaviour(application). 25 | 26 | %% Application callbacks 27 | -export([start/2, stop/1]). 28 | 29 | %%==================================================================== 30 | %% API 31 | %%==================================================================== 32 | 33 | start(_StartType, _StartArgs) -> 34 | loquat_sup:start_link(). 35 | 36 | %%-------------------------------------------------------------------- 37 | stop(_State) -> 38 | ok. 39 | 40 | %%==================================================================== 41 | %% Internal functions 42 | %%==================================================================== 43 | -------------------------------------------------------------------------------- /src/loquat_plumtree_backend.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2017 Christopher S. Meiklejohn. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | -module(loquat_plumtree_backend). 22 | -author("Christopher S. Meiklejohn "). 23 | 24 | -behaviour(plumtree_broadcast_handler). 25 | 26 | %% API 27 | -export([start_link/0, 28 | start_link/1]). 29 | 30 | %% plumtree_broadcast_handler callbacks 31 | -export([broadcast_data/1, 32 | merge/2, 33 | is_stale/1, 34 | graft/1, 35 | exchange/1]). 36 | 37 | %% gen_server callbacks 38 | -export([init/1, 39 | handle_call/3, 40 | handle_cast/2, 41 | handle_info/2, 42 | terminate/2, 43 | code_change/3]). 44 | 45 | %% transmission callbacks 46 | -export([extract_log_type_and_payload/1]). 47 | 48 | %% State record. 49 | -record(state, {}). 50 | 51 | %% Broadcast record. 52 | -record(broadcast, {timestamp}). 53 | 54 | %%%=================================================================== 55 | %%% API 56 | %%%=================================================================== 57 | 58 | %% @doc Same as start_link([]). 59 | -spec start_link() -> {ok, pid()} | ignore | {error, term()}. 60 | start_link() -> 61 | start_link([]). 62 | 63 | %% @doc Start and link to calling process. 64 | -spec start_link(list())-> {ok, pid()} | ignore | {error, term()}. 65 | start_link(Opts) -> 66 | gen_server:start_link({local, ?MODULE}, ?MODULE, Opts, []). 67 | 68 | %%%=================================================================== 69 | %%% plumtree_broadcast_handler callbacks 70 | %%%=================================================================== 71 | 72 | -type timestamp() :: non_neg_integer(). 73 | 74 | -type broadcast_message() :: #broadcast{}. 75 | -type broadcast_id() :: timestamp(). 76 | -type broadcast_payload() :: timestamp(). 77 | 78 | %% @doc Returns from the broadcast message the identifier and the payload. 79 | -spec broadcast_data(broadcast_message()) -> 80 | {broadcast_id(), broadcast_payload()}. 81 | broadcast_data(#broadcast{timestamp=Timestamp}) -> 82 | {Timestamp, Timestamp}. 83 | 84 | %% @doc Perform a merge of an incoming object with an object in the 85 | %% local datastore. 86 | -spec merge(broadcast_id(), broadcast_payload()) -> boolean(). 87 | merge(Timestamp, Timestamp) -> 88 | lager:info("Heartbeat received: ~p", [Timestamp]), 89 | 90 | case is_stale(Timestamp) of 91 | true -> 92 | false; 93 | false -> 94 | gen_server:call(?MODULE, {merge, Timestamp}, infinity), 95 | true 96 | end. 97 | 98 | %% @doc Use the clock on the object to determine if this message is 99 | %% stale or not. 100 | -spec is_stale(broadcast_id()) -> boolean(). 101 | is_stale(Timestamp) -> 102 | gen_server:call(?MODULE, {is_stale, Timestamp}, infinity). 103 | 104 | %% @doc Given a message identifier and a clock, return a given message. 105 | -spec graft(broadcast_id()) -> 106 | stale | {ok, broadcast_payload()} | {error, term()}. 107 | graft(Timestamp) -> 108 | gen_server:call(?MODULE, {graft, Timestamp}, infinity). 109 | 110 | %% @doc Anti-entropy mechanism. 111 | -spec exchange(node()) -> {ok, pid()}. 112 | exchange(_Peer) -> 113 | %% Ignore the standard anti-entropy mechanism from plumtree. 114 | %% 115 | %% Spawn a process that terminates immediately, because the 116 | %% broadcast exchange timer tracks the number of in progress 117 | %% exchanges and bounds it by that limit. 118 | %% 119 | %% Ignore the anti-entropy mechanism because we don't need to worry 120 | %% about reliable delivery: we always know we'll have another 121 | %% message to further repair duing the next interval. 122 | %% 123 | Pid = spawn_link(fun() -> ok end), 124 | {ok, Pid}. 125 | 126 | %%%=================================================================== 127 | %%% gen_server callbacks 128 | %%%=================================================================== 129 | 130 | %% @private 131 | -spec init([]) -> {ok, #state{}}. 132 | init([]) -> 133 | %% Seed the process at initialization. 134 | rand_compat:seed(erlang:phash2([node()]), 135 | erlang:monotonic_time(), 136 | erlang:unique_integer()), 137 | 138 | schedule_heartbeat(), 139 | 140 | %% Open an ETS table for tracking heartbeat messages. 141 | ets:new(?MODULE, [named_table]), 142 | 143 | {ok, #state{}}. 144 | 145 | %% @private 146 | -spec handle_call(term(), {pid(), term()}, #state{}) -> 147 | {reply, term(), #state{}}. 148 | 149 | %% @private 150 | handle_call({is_stale, Timestamp}, _From, State) -> 151 | Result = case ets:lookup(?MODULE, Timestamp) of 152 | [] -> 153 | false; 154 | _ -> 155 | true 156 | end, 157 | {reply, Result, State}; 158 | handle_call({graft, Timestamp}, _From, State) -> 159 | Result = case ets:lookup(?MODULE, Timestamp) of 160 | [] -> 161 | lager:info("Timestamp: ~p not found for graft.", [Timestamp]), 162 | {error, {not_found, Timestamp}}; 163 | [{Timestamp, _}] -> 164 | {ok, Timestamp} 165 | end, 166 | {reply, Result, State}; 167 | handle_call({merge, Timestamp}, _From, State) -> 168 | true = ets:insert(?MODULE, [{Timestamp, true}]), 169 | {reply, ok, State}; 170 | handle_call(Msg, _From, State) -> 171 | _ = lager:warning("Unhandled messages: ~p", [Msg]), 172 | {reply, ok, State}. 173 | 174 | -spec handle_cast(term(), #state{}) -> {noreply, #state{}}. 175 | %% @private 176 | handle_cast(Msg, State) -> 177 | _ = lager:warning("Unhandled messages: ~p", [Msg]), 178 | {noreply, State}. 179 | 180 | %% @private 181 | handle_info(heartbeat, State) -> 182 | Node = node(), 183 | Servers = servers(), 184 | 185 | case lists:member(Node, Servers) of 186 | true -> 187 | %% Generate message with monotonically increasing integer. 188 | Counter = time_compat:unique_integer([monotonic, positive]), 189 | 190 | %% Make sure the node prefixes the timestamp with it's own 191 | %% identifier: this means that we can have this tree 192 | %% participate in multiple trees, each rooted at a different 193 | %% node. 194 | Timestamp = {node(), Counter}, 195 | 196 | %% Insert a new message into the table. 197 | true = ets:insert(?MODULE, [{Timestamp, true}]), 198 | 199 | %% Send message with monotonically increasing integer. 200 | ok = plumtree_broadcast:broadcast(#broadcast{timestamp=Timestamp}, ?MODULE), 201 | 202 | lager:info("Heartbeat triggered: sending ping ~p to ensure tree.", 203 | [Timestamp]); 204 | false -> 205 | ok 206 | end, 207 | 208 | %% Schedule report. 209 | schedule_heartbeat(), 210 | 211 | {noreply, State}; 212 | handle_info(Msg, State) -> 213 | _ = lager:warning("Unhandled messages: ~p", [Msg]), 214 | {noreply, State}. 215 | 216 | %% @private 217 | -spec terminate(term(), #state{}) -> term(). 218 | terminate(_Reason, _State) -> 219 | ok. 220 | 221 | %% @private 222 | -spec code_change(term() | {down, term()}, #state{}, term()) -> 223 | {ok, #state{}}. 224 | code_change(_OldVsn, State, _Extra) -> 225 | {ok, State}. 226 | 227 | %%%=================================================================== 228 | %%% Internal functions 229 | %%%=================================================================== 230 | 231 | %% @private 232 | schedule_heartbeat() -> 233 | case lasp_config:get(broadcast, false) of 234 | true -> 235 | Interval = lasp_config:get(heartbeat_interval, 10000), 236 | timer:send_after(Interval, heartbeat); 237 | false -> 238 | ok 239 | end. 240 | 241 | %%%=================================================================== 242 | %%% Transmission functions 243 | %%%=================================================================== 244 | 245 | extract_log_type_and_payload({prune, Root, From}) -> 246 | Servers = servers(), 247 | case lists:member(Root, Servers) of 248 | true -> 249 | [{broadcast_protocol, {Root, From}}]; 250 | false -> 251 | [] 252 | end; 253 | extract_log_type_and_payload({ignored_i_have, MessageId, _Mod, Round, Root, From}) -> 254 | Servers = servers(), 255 | case lists:member(Root, Servers) of 256 | true -> 257 | [{broadcast_protocol, {MessageId, Round, Root, From}}]; 258 | false -> 259 | [] 260 | end; 261 | extract_log_type_and_payload({graft, MessageId, _Mod, Round, Root, From}) -> 262 | Servers = servers(), 263 | case lists:member(Root, Servers) of 264 | true -> 265 | [{broadcast_protocol, {MessageId, Round, Root, From}}]; 266 | false -> 267 | [] 268 | end; 269 | extract_log_type_and_payload({broadcast, MessageId, Timestamp, _Mod, Round, Root, From}) -> 270 | Servers = servers(), 271 | case lists:member(Root, Servers) of 272 | true -> 273 | [{broadcast_protocol, {Timestamp, MessageId, Round, Root, From}}]; 274 | false -> 275 | [] 276 | end; 277 | extract_log_type_and_payload({i_have, MessageId, _Mod, Round, Root, From}) -> 278 | Servers = servers(), 279 | case lists:member(Root, Servers) of 280 | true -> 281 | [{broadcast_protocol, {MessageId, Round, Root, From}}]; 282 | false -> 283 | [] 284 | end; 285 | extract_log_type_and_payload(Message) -> 286 | lager:info("No match for extracted payload: ~p", [Message]), 287 | []. 288 | 289 | %% @private 290 | servers() -> 291 | {ok, Servers} = sprinter_backend:servers(), 292 | Servers. 293 | -------------------------------------------------------------------------------- /src/loquat_sup.erl: -------------------------------------------------------------------------------- 1 | %% ------------------------------------------------------------------- 2 | %% 3 | %% Copyright (c) 2017 Christopher S. Meiklejohn. All Rights Reserved. 4 | %% 5 | %% This file is provided to you under the Apache License, 6 | %% Version 2.0 (the "License"); you may not use this file 7 | %% except in compliance with the License. You may obtain 8 | %% a copy of the License at 9 | %% 10 | %% http://www.apache.org/licenses/LICENSE-2.0 11 | %% 12 | %% Unless required by applicable law or agreed to in writing, 13 | %% software distributed under the License is distributed on an 14 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %% KIND, either express or implied. See the License for the 16 | %% specific language governing permissions and limitations 17 | %% under the License. 18 | %% 19 | %% ------------------------------------------------------------------- 20 | 21 | -module(loquat_sup). 22 | -author("Christopher S. Meiklejohn "). 23 | 24 | -behaviour(supervisor). 25 | 26 | %% API 27 | -export([start_link/0]). 28 | 29 | %% Supervisor callbacks 30 | -export([init/1]). 31 | 32 | -define(SERVER, ?MODULE). 33 | 34 | %%==================================================================== 35 | %% API functions 36 | %%==================================================================== 37 | 38 | start_link() -> 39 | supervisor:start_link({local, ?SERVER}, ?MODULE, []). 40 | 41 | %%==================================================================== 42 | %% Supervisor callbacks 43 | %%==================================================================== 44 | 45 | %% Child :: {Id,StartFunc,Restart,Shutdown,Type,Modules} 46 | init([]) -> 47 | PlumtreeBackend = {loquat_plumtree_backend, 48 | {loquat_plumtree_backend, start_link, []}, 49 | permanent, 5000, worker, 50 | [loquat_plumtree_backend]}, 51 | 52 | {ok, { {one_for_all, 0, 1}, [PlumtreeBackend]} }. 53 | 54 | %%==================================================================== 55 | %% Internal functions 56 | %%==================================================================== 57 | --------------------------------------------------------------------------------