Skip to content

Commit 2ef1651

Browse files
authored
core/amqp_leader: fix cluster cron never starting (#72)
* core: fix cluster cron never starting amqp_cron has never started a task on any node. Two defects, the second masked by the first. 1. amqp_leader declared no dependency on kazoo_globals, despite amqp_leader_listener gating readiness on kz_nodes:is_up/1. The release boot script therefore started amqp_leader and amqp_cron *before* kazoo_globals, so the kz_nodes ETS table did not exist yet and ets:match/2 raised badarg instead of returning []. The retry loop in ready_when_node_is_up/0 only handles is_up -> false, so the badarg escaped, burned amqp_leader_proc_sup's restart budget in ~5ms and left it dead: transient children are not restarted after a 'shutdown' exit. Declaring the dependency fixes the boot order (kazoo_globals 32 -> amqp_leader 33 -> amqp_cron 34). No cycle: nothing kazoo_globals depends on reaches amqp_leader. 2. With the gate cleared, amqp_cron reached its first AMQP message and died on "undefined function amqp_cron:handle_info/3". amqp_leader_proc:exec_callback/2 appends [ModState, Leader] to every callback's arguments. amqp_cron honours that for handle_cast/3, handle_call/4, handle_DOWN/3 and from_leader/3, but exported handle_info/2. Also make kz_nodes:is_up/1 total: it now answers false when the registry is not running rather than raising, which is what its one caller has always wanted. The unused expires = '$2' binding is dropped -- it was matched and discarded, implying a recency check that is not performed. Verified on a dev node: amqp_cron:status/0 now reports an elected leader, the pid is stable across minutes, and the boot log contains no ets:match badarg, no reached_max_restart_intensity and no handle_info crash. * CONTEXT: name the cluster-membership layer The glossary had no vocabulary for the layer this bug lives in: no term for the node registry, for leader election, or for cluster-wide scheduled work. That gap is plausibly why amqp_leader's dependency on kazoo_globals went undeclared for so long -- the relationship had no name. Adds Node, Node registry, Leader and Cluster cron. The Node registry entry records the semantics kz_nodes actually implements: membership is liveness, pruned by the expiry sweep, with no separate recency check.
1 parent 3dfb75b commit 2ef1651

4 files changed

Lines changed: 40 additions & 11 deletions

File tree

‎CONTEXT.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,27 @@ _Avoid_: switch, PBX
121121
The RabbitMQ message bus every whapp communicates over.
122122
_Avoid_: queue, broker
123123

124+
**Node**:
125+
One Erlang VM running Kazoo, named `<whapp>@<host>`. A node and a cluster
126+
member are the same thing; a FreeSWITCH box is a media server, never a node.
127+
_Avoid_: server, instance, box
128+
129+
**Node registry**:
130+
Every node's view of cluster membership, built from heartbeats over the AMQP
131+
bus and pruned by an expiry sweep. Membership *is* liveness: a node is up
132+
exactly while it holds a row, and nothing checks recency separately.
133+
_Avoid_: node list, cluster state, membership table
134+
135+
**Leader**:
136+
The single node elected to act for a named leader group, chosen over the AMQP
137+
bus. Groups elect independently, so one node may lead some groups and not others.
138+
_Avoid_: master, primary, coordinator
139+
140+
**Cluster cron**:
141+
Scheduled work owned by exactly one node per cluster — its group's leader —
142+
rather than run on every node.
143+
_Avoid_: scheduler, job runner, crontab
144+
124145
**kapi**:
125146
The typed contract modules that define AMQP messages. Prefix `kapi_`.
126147
_Avoid_: event schema

‎core/amqp_cron/src/amqp_cron.erl‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
-export([init/1,
5252
handle_cast/3,
5353
handle_call/4,
54-
handle_info/2,
54+
handle_info/3,
5555
handle_leader_call/4,
5656
handle_leader_cast/3,
5757
handle_DOWN/3,
@@ -301,8 +301,8 @@ handle_call(_Request, _From, State, _Election) ->
301301
handle_cast(_Msg, State, _Election) ->
302302
{'noreply', State}.
303303

304-
-spec handle_info(any(), state()) -> kz_types:handle_info_ret_state(state()).
305-
handle_info(_Info, State) ->
304+
-spec handle_info(any(), state(), any()) -> kz_types:handle_info_ret_state(state()).
305+
handle_info(_Info, State, _Election) ->
306306
{'noreply', State}.
307307

308308
-spec terminate(any(), state()) -> 'ok'.

‎core/amqp_leader/src/amqp_leader.app.src‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
, stdlib
99

1010
, kazoo_amqp
11+
%% amqp_leader_listener gates readiness on kz_nodes:is_up/1,
12+
%% so the node registry must be started before this app.
13+
, kazoo_globals
1114

1215
, lager
1316
]},

‎core/kazoo_globals/src/kz_nodes.erl‎

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -125,16 +125,21 @@ start_link() ->
125125
,[]
126126
).
127127

128+
%% @doc Is `Node' a member of the node registry?
129+
%%
130+
%% Membership, not heartbeat recency: a node stays "up" until the `expire_nodes'
131+
%% sweep removes its row, which is the registry's single source of truth for
132+
%% liveness. This deliberately does not re-check `expires'.
133+
%%
134+
%% Answers `false' rather than crashing when the registry itself is not running
135+
%% yet. Callers poll this during startup (see `amqp_leader_listener'), and a node
136+
%% whose registry has not started is not up in any sense they can use.
128137
-spec is_up(node()) -> boolean().
129138
is_up(Node) ->
130-
case ets:match(?MODULE, #kz_node{node = Node
131-
,expires = '$2'
132-
,_ = '_'
133-
})
134-
of
135-
[] -> 'false';
136-
[_] -> 'true'
137-
end.
139+
'undefined' =/= ets:whereis(?MODULE)
140+
andalso [] =/= ets:match(?MODULE, #kz_node{node = Node
141+
,_ = '_'
142+
}).
138143

139144
-spec node_to_json(kz_term:text() | kz_types:kz_node()) -> kz_json:object().
140145
node_to_json(NodeName) when is_atom(NodeName) ->

0 commit comments

Comments
 (0)