Packages

Autocluster and Autoheal for EMQ X Broker

Current section

Files

Jump to
ekka src ekka_locker.erl
Raw

src/ekka_locker.erl

%% Copyright (c) 2018 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
-module(ekka_locker).
-include_lib("stdlib/include/ms_transform.hrl").
-behaviour(gen_server).
-export([start_link/0, start_link/1, start_link/2]).
%% for test cases
-export([stop/0, stop/1]).
-export([acquire/1, acquire/2, acquire/3, acquire/4]).
-export([release/1, release/2, release/3]).
%% for rpc call
-export([acquire_lock/2, acquire_lock/3, release_lock/2]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-type(resource() :: term()).
-type(lock_type() :: local | leader | quorum | all).
-type(lock_result() :: {boolean, [node() | {node(), any()}]}).
-type(piggyback() :: mfa()).
-export_type([resource/0, lock_type/0, lock_result/0, piggyback/0]).
-record(lock, {resource :: resource(),
owner :: pid(),
counter :: integer(),
created :: erlang:timestamp()}).
-record(lease, {expiry, timer}).
-record(state, {locks, lease, monitors}).
-define(SERVER, ?MODULE).
%% 15 seconds by default
-define(LEASE_TIME, 15000).
%%--------------------------------------------------------------------
%% API
%%--------------------------------------------------------------------
-spec(start_link() -> {ok, pid()} | ignore | {error, any()}).
start_link() ->
start_link(?SERVER).
-spec(start_link(atom()) -> {ok, pid()} | ignore | {error, any()}).
start_link(Name) ->
start_link(Name, ?LEASE_TIME).
-spec(start_link(atom(), pos_integer()) -> {ok, pid()} | ignore | {error, any()}).
start_link(Name, LeaseTime) ->
gen_server:start_link({local, Name}, ?MODULE, [Name, LeaseTime], []).
-spec(stop() -> ok).
stop() ->
stop(?SERVER).
-spec(stop(atom()) -> ok).
stop(Name) ->
gen_server:call(Name, stop).
-spec(acquire(resource()) -> {boolean(), [node()]}).
acquire(Resource) ->
acquire(?SERVER, Resource).
-spec(acquire(atom(), resource()) -> lock_result()).
acquire(Name, Resource) when is_atom(Name) ->
acquire(Name, Resource, local).
-spec(acquire(atom(), resource(), lock_type()) -> lock_result()).
acquire(Name, Resource, Type) ->
acquire(Name, Resource, Type, undefined).
-spec(acquire(atom(), resource(), lock_type(), piggyback()) -> lock_result()).
acquire(Name, Resource, local, Piggyback) when is_atom(Name) ->
acquire_lock(Name, lock_obj(Resource), Piggyback);
acquire(Name, Resource, leader, Piggyback) when is_atom(Name)->
Leader = ekka_membership:leader(),
case rpc:call(Leader, ?MODULE, acquire_lock,
[Name, lock_obj(Resource), Piggyback]) of
Err = {badrpc, _Reason} ->
{false, [{Leader, Err}]};
Res -> Res
end;
acquire(Name, Resource, quorum, Piggyback) when is_atom(Name) ->
acquire_locks(ekka_ring:find_nodes(Resource),
Name, lock_obj(Resource), Piggyback);
acquire(Name, Resource, all, Piggyback) when is_atom(Name) ->
acquire_locks(ekka_membership:nodelist(up),
Name, lock_obj(Resource), Piggyback).
acquire_locks(Nodes, Name, LockObj, Piggyback) ->
{ResL, _BadNodes}
= rpc:multicall(Nodes, ?MODULE, acquire_lock, [Name, LockObj, Piggyback]),
case merge_results(ResL) of
Res = {true, _} -> Res;
Res = {false, _} ->
rpc:multicall(Nodes, ?MODULE, release_lock, [Name, LockObj]),
Res
end.
acquire_lock(Name, LockObj, Piggyback) ->
{acquire_lock(Name, LockObj), [with_piggyback(node(), Piggyback)]}.
acquire_lock(Name, LockObj = #lock{resource = Resource, owner = Owner}) ->
Pos = #lock.counter,
try ets:update_counter(Name, Resource, [{Pos, 0}, {Pos, 1, 1, 1}]) of
[0, 1] -> true;
[1, 1] ->
case ets:lookup(Name, Resource) of
[#lock{owner = Owner}] ->
true;
_Other -> false
end
catch
error:badarg ->
ets:insert_new(Name, LockObj)
end.
%%check_local(Name, #lock{resource = Resource, owner = Owner}) ->
%% case ets:lookup(Name, Resource) of
%% [#lock{owner = Owner}] ->
%% true;
%% [_Lock] -> false;
%% [] -> true
%% end.
with_piggyback(Node, undefined) ->
Node;
with_piggyback(Node, {M, F, Args}) ->
{Node, erlang:apply(M, F, Args)}.
lock_obj(Resource) ->
#lock{resource = Resource,
owner = self(),
counter = 1,
created = erlang:system_time(millisecond)}.
-spec(release(resource()) -> lock_result()).
release(Resource) ->
release(?SERVER, Resource).
-spec(release(atom(), resource()) -> lock_result()).
release(Name, Resource) ->
release(Name, Resource, local).
-spec(release(atom(), resource(), lock_type()) -> lock_result()).
release(Name, Resource, local) ->
release_lock(Name, lock_obj(Resource));
release(Name, Resource, leader) ->
Leader = ekka_membership:leader(),
case rpc:call(Leader, ?MODULE, release_lock, [Name, lock_obj(Resource)]) of
Err = {badrpc, _Reason} ->
{false, [{Leader, Err}]};
Res -> Res
end;
release(Name, Resource, quorum) ->
release_locks(ekka_ring:find_nodes(Resource), Name, lock_obj(Resource));
release(Name, Resource, all) ->
release_locks(ekka_membership:nodelist(up), Name, lock_obj(Resource)).
release_locks(Nodes, Name, LockObj) ->
{ResL, _BadNodes} = rpc:multicall(Nodes, ?MODULE, release_lock, [Name, LockObj]),
merge_results(ResL).
release_lock(Name, #lock{resource = Resource, owner = Owner}) ->
Res = case ets:lookup(Name, Resource) of
[Lock = #lock{owner = Owner}] ->
ets:delete_object(Name, Lock);
[_Lock] -> false;
[] -> false
end,
{Res, [node()]}.
merge_results(ResL) ->
merge_results(ResL, [], []).
merge_results([], Succ, []) ->
{true, Succ};
merge_results([], _, Failed) ->
{false, Failed};
merge_results([{true, Res}|ResL], Succ, Failed) ->
merge_results(ResL, [Res|Succ], Failed);
merge_results([{false, Res}|ResL], Succ, Failed) ->
merge_results(ResL, Succ, [Res|Failed]).
%%--------------------------------------------------------------------
%% gen_server callbacks
%%--------------------------------------------------------------------
init([Name, LeaseTime]) ->
Tab = ets:new(Name, [public, set, named_table, {keypos, 2},
{read_concurrency, true}, {write_concurrency, true}]),
TRef = timer:send_interval(LeaseTime * 2, check_lease),
Lease = #lease{expiry = LeaseTime, timer = TRef},
{ok, #state{locks = Tab, lease = Lease, monitors = #{}}}.
handle_call(stop, _From, State) ->
{stop, normal, ok, State};
handle_call(_Request, _From, State) ->
{reply, ignore, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(check_lease, State = #state{locks = Tab, lease = Lease, monitors = Monitors}) ->
Monitors1 = lists:foldl(
fun(#lock{resource = Resource, owner = Owner}, MonAcc) ->
case maps:find(Owner, MonAcc) of
{ok, Resources} ->
maps:put(Owner, [Resource|Resources], MonAcc);
error ->
_MRef = erlang:monitor(process, Owner),
maps:put(Owner, [Resource], MonAcc)
end
end, Monitors, check_lease(Tab, Lease, erlang:system_time(millisecond))),
{noreply, State#state{monitors = Monitors1}, hibernate};
handle_info({'DOWN', _MRef, process, DownPid, _Reason},
State = #state{locks = Tab, monitors = Monitors}) ->
io:format("Lock owner DOWN: ~p~n", [DownPid]),
case maps:find(DownPid, Monitors) of
{ok, Resources} ->
lists:foreach(
fun(Resource) ->
case ets:lookup(Tab, Resource) of
[Lock = #lock{owner = OwnerPid}] when OwnerPid =:= DownPid ->
ets:delete_object(Tab, Lock);
_ -> ok
end
end, Resources),
{noreply, State#state{monitors = maps:remove(DownPid, Monitors)}};
error ->
{noreply, State}
end;
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State = #state{lease = Lease}) ->
cancel_lease(Lease).
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
check_lease(Tab, #lease{expiry = Expiry}, Now) ->
Spec = ets:fun2ms(fun({_, _, _, _, T} = Resource) when (Now - T) > Expiry -> Resource end),
ets:select(Tab, Spec).
cancel_lease(#lease{timer = TRef}) ->
timer:cancel(TRef).