|
| 1 | +%% This Source Code Form is subject to the terms of the Mozilla Public |
| 2 | +%% License, v. 2.0. If a copy of the MPL was not distributed with this |
| 3 | +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. |
| 4 | +%% |
| 5 | +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. |
| 6 | +%% |
| 7 | +-module(mqtt_machine_v0). |
| 8 | +-behaviour(ra_machine). |
| 9 | + |
| 10 | +-include("mqtt_machine_v0.hrl"). |
| 11 | + |
| 12 | +-export([init/1, |
| 13 | + apply/3, |
| 14 | + state_enter/2, |
| 15 | + notify_connection/2]). |
| 16 | + |
| 17 | +-type state() :: #machine_state{}. |
| 18 | + |
| 19 | +-type config() :: map(). |
| 20 | + |
| 21 | +-type reply() :: {ok, term()} | {error, term()}. |
| 22 | +-type client_id() :: term(). |
| 23 | + |
| 24 | +-type command() :: {register, client_id(), pid()} | |
| 25 | + {unregister, client_id(), pid()} | |
| 26 | + list. |
| 27 | + |
| 28 | +-spec init(config()) -> state(). |
| 29 | +init(_Conf) -> |
| 30 | + #machine_state{}. |
| 31 | + |
| 32 | +-spec apply(map(), command(), state()) -> |
| 33 | + {state(), reply(), ra_machine:effects()}. |
| 34 | +apply(_Meta, {register, ClientId, Pid}, #machine_state{client_ids = Ids} = State0) -> |
| 35 | + {Effects, Ids1} = |
| 36 | + case maps:find(ClientId, Ids) of |
| 37 | + {ok, OldPid} when Pid =/= OldPid -> |
| 38 | + Effects0 = [{demonitor, process, OldPid}, |
| 39 | + {monitor, process, Pid}, |
| 40 | + {mod_call, ?MODULE, notify_connection, [OldPid, duplicate_id]}], |
| 41 | + {Effects0, maps:remove(ClientId, Ids)}; |
| 42 | + _ -> |
| 43 | + Effects0 = [{monitor, process, Pid}], |
| 44 | + {Effects0, Ids} |
| 45 | + end, |
| 46 | + State = State0#machine_state{client_ids = maps:put(ClientId, Pid, Ids1)}, |
| 47 | + {State, ok, Effects}; |
| 48 | + |
| 49 | +apply(Meta, {unregister, ClientId, Pid}, #machine_state{client_ids = Ids} = State0) -> |
| 50 | + State = case maps:find(ClientId, Ids) of |
| 51 | + {ok, Pid} -> State0#machine_state{client_ids = maps:remove(ClientId, Ids)}; |
| 52 | + %% don't delete client id that might belong to a newer connection |
| 53 | + %% that kicked the one with Pid out |
| 54 | + {ok, _AnotherPid} -> State0; |
| 55 | + error -> State0 |
| 56 | + end, |
| 57 | + Effects0 = [{demonitor, process, Pid}], |
| 58 | + %% snapshot only when the map has changed |
| 59 | + Effects = case State of |
| 60 | + State0 -> Effects0; |
| 61 | + _ -> Effects0 ++ snapshot_effects(Meta, State) |
| 62 | + end, |
| 63 | + {State, ok, Effects}; |
| 64 | + |
| 65 | +apply(_Meta, {down, DownPid, noconnection}, State) -> |
| 66 | + %% Monitor the node the pid is on (see {nodeup, Node} below) |
| 67 | + %% so that we can detect when the node is re-connected and discover the |
| 68 | + %% actual fate of the connection processes on it |
| 69 | + Effect = {monitor, node, node(DownPid)}, |
| 70 | + {State, ok, Effect}; |
| 71 | + |
| 72 | +apply(Meta, {down, DownPid, _}, #machine_state{client_ids = Ids} = State0) -> |
| 73 | + Ids1 = maps:filter(fun (_ClientId, Pid) when Pid =:= DownPid -> |
| 74 | + false; |
| 75 | + (_, _) -> |
| 76 | + true |
| 77 | + end, Ids), |
| 78 | + State = State0#machine_state{client_ids = Ids1}, |
| 79 | + Delta = maps:keys(Ids) -- maps:keys(Ids1), |
| 80 | + Effects = lists:map(fun(Id) -> |
| 81 | + [{mod_call, rabbit_log, debug, |
| 82 | + ["MQTT connection with client id '~s' failed", [Id]]}] end, Delta), |
| 83 | + {State, ok, Effects ++ snapshot_effects(Meta, State)}; |
| 84 | + |
| 85 | +apply(_Meta, {nodeup, Node}, State) -> |
| 86 | + %% Work out if any pids that were disconnected are still |
| 87 | + %% alive. |
| 88 | + %% Re-request the monitor for the pids on the now-back node. |
| 89 | + Effects = [{monitor, process, Pid} || Pid <- all_pids(State), node(Pid) == Node], |
| 90 | + {State, ok, Effects}; |
| 91 | +apply(_Meta, {nodedown, _Node}, State) -> |
| 92 | + {State, ok}; |
| 93 | + |
| 94 | +apply(Meta, {leave, Node}, #machine_state{client_ids = Ids} = State0) -> |
| 95 | + Ids1 = maps:filter(fun (_ClientId, Pid) -> node(Pid) =/= Node end, Ids), |
| 96 | + Delta = maps:keys(Ids) -- maps:keys(Ids1), |
| 97 | + |
| 98 | + Effects = lists:foldl(fun (ClientId, Acc) -> |
| 99 | + Pid = maps:get(ClientId, Ids), |
| 100 | + [ |
| 101 | + {demonitor, process, Pid}, |
| 102 | + {mod_call, ?MODULE, notify_connection, [Pid, decommission_node]}, |
| 103 | + {mod_call, rabbit_log, debug, |
| 104 | + ["MQTT will remove client ID '~s' from known " |
| 105 | + "as its node has been decommissioned", [ClientId]]} |
| 106 | + ] ++ Acc |
| 107 | + end, [], Delta), |
| 108 | + |
| 109 | + State = State0#machine_state{client_ids = Ids1}, |
| 110 | + {State, ok, Effects ++ snapshot_effects(Meta, State)}; |
| 111 | + |
| 112 | +apply(_Meta, Unknown, State) -> |
| 113 | + error_logger:error_msg("MQTT Raft state machine received unknown command ~p~n", [Unknown]), |
| 114 | + {State, {error, {unknown_command, Unknown}}, []}. |
| 115 | + |
| 116 | +state_enter(leader, State) -> |
| 117 | + %% re-request monitors for all known pids, this would clean up |
| 118 | + %% records for all connections are no longer around, e.g. right after node restart |
| 119 | + [{monitor, process, Pid} || Pid <- all_pids(State)]; |
| 120 | +state_enter(_, _) -> |
| 121 | + []. |
| 122 | + |
| 123 | +%% ========================== |
| 124 | + |
| 125 | +%% Avoids blocking the Raft leader. |
| 126 | +notify_connection(Pid, Reason) -> |
| 127 | + spawn(fun() -> gen_server2:cast(Pid, Reason) end). |
| 128 | + |
| 129 | +-spec snapshot_effects(map(), state()) -> ra_machine:effects(). |
| 130 | +snapshot_effects(#{index := RaftIdx}, State) -> |
| 131 | + [{release_cursor, RaftIdx, State}]. |
| 132 | + |
| 133 | +all_pids(#machine_state{client_ids = Ids}) -> |
| 134 | + maps:values(Ids). |
0 commit comments