Skip to content

Commit f5b1f3c

Browse files
committed
Stop the AAE tick waking every oplog instance
bondy_oplog:list_instances/0 walked the supervision tree — a which_children/1 per instance supervisor plus a gen_server:call(info) per instance, so 2N round trips that made all N instances runnable at once. It now answers from bondy_oplog_registry:list/0, a key-only ets:select that wakes nobody. Callers: sync scheduler (every tick), GC scheduler, origin retirement, and the Prometheus scrape. Signed-off-by: Alejandro M. Ramallo <alejandro.ramallo@leapsight.com>
1 parent 4cae29b commit f5b1f3c

7 files changed

Lines changed: 118 additions & 46 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
![Docker Pulls](https://img.shields.io/docker/pulls/leapsight/bondy?style=for-the-badge)
66
![Docker Build (master)](https://img.shields.io/github/actions/workflow/status/bondy-io/bondy/docker_image_build.yaml?&branch=master&label=docker-master&style=for-the-badge)
77
![Docker Build (develop)](https://img.shields.io/github/actions/workflow/status/bondy-io/bondy/docker_image_build.yaml?&branch=develop&label=docker-develop&style=for-the-badge)
8-
![Docker Build (latest-tag)](https://img.shields.io/github/actions/workflow/status/bondy-io/bondy/docker_image_build.yaml?&tag=version-1.0.0-rc-amber&label=docker-1.0.0-rc-amber&style=for-the-badge)
8+
![Docker Build (latest-tag)](https://img.shields.io/github/actions/workflow/status/bondy-io/bondy/docker_image_build.yaml?&tag=version-1.0.0-rc-citron&label=docker-1.0.0-rc-citron&style=for-the-badge)
99
<br>![Architectures](https://img.shields.io/badge/architecture-linux%2Famd64%20%7C%20linux%2Farm64%20%7C%20macOS%2Fintel%20%7C%20macOS%2FM1-lightgrey?style=for-the-badge)
1010

1111

apps/bondy_oplog/src/bondy_oplog.erl

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -211,22 +211,26 @@ stop_instance(InstanceId, _Opts) when is_binary(InstanceId) ->
211211

212212
?DOC("""
213213
Lists currently-running instances on this node. Order unspecified.
214+
215+
Answered from the registry's key-only select, so it wakes no process and
216+
takes no lock. This is called from periodic sweeps (the sync and GC
217+
scheduler ticks, origin retirement) and from the Prometheus scrape, and
218+
a supervision-tree walk is the wrong shape for all of them:
219+
`supervisor:which_children/1` is a `gen_server:call` that serialises
220+
against the process managing child starts and stops, and copies the whole
221+
child list per call. Walking the tree to reach each instance cost one such
222+
call per instance supervisor plus an `info/1` call per instance — 2N
223+
round trips that made all N instances runnable at the same instant.
224+
225+
A row exists from instance start until its `terminate/2`, so the set is
226+
the same one the tree reports, minus instances that have started but not
227+
yet registered. Sweeps pick those up on their next pass; boot-time
228+
enumeration uses `discover_instances/1,2` instead.
214229
""").
215230
-spec list_instances() -> [instance_id()].
216231

217232
list_instances() ->
218-
Children = supervisor:which_children(
219-
bondy_oplog_instance_dyn_sup
220-
),
221-
[
222-
InstanceId
223-
|| {_Id, SupPid, supervisor, _} <- Children,
224-
is_pid(SupPid),
225-
InstancePid <- [bondy_oplog_instance_sup:instance_pid(SupPid)],
226-
is_pid(InstancePid),
227-
#{instance_id := InstanceId} <-
228-
[bondy_oplog_instance:info(InstancePid)]
229-
].
233+
bondy_oplog_registry:list().
230234

231235
?DOC("""
232236
Discovers instances on disk under `BaseDir`, using the sharded path

apps/bondy_oplog/src/bondy_oplog_sync_scheduler.erl

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -770,9 +770,8 @@ dispatch_for(InstanceId, #state{} = State) ->
770770
end.
771771

772772
%% @private
773-
%% `list_instances/0` calls `info/1` on each running worker — if a
774-
%% worker is mid-restart that call may briefly fail. Soft-fail to an
775-
%% empty list rather than crash the scheduler.
773+
%% Soft-fail to an empty list rather than crash the scheduler: the
774+
%% registry table is absent before it starts and after it stops.
776775
safe_list_instances() ->
777776
try
778777
bondy_oplog:list_instances()

apps/bondy_regulator/src/bondy_regulator_load.erl

Lines changed: 65 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,13 @@ will time out expensively after holding resources.
2020
The status has hysteresis to avoid flapping at the boundary: it becomes
2121
`busy` when the sampled run queue reaches `high_watermark x
2222
schedulers_online` and returns to `normal` only when it falls to
23-
`low_watermark x schedulers_online`. Thresholds are expressed as factors
23+
`low_watermark x schedulers_online`. A crossing must also hold for three
24+
consecutive samples before it is committed. Hysteresis alone does not
25+
cover the dominant case on a quiet node: an instantaneous run queue
26+
spikes whenever a wave of periodic timers wakes together, and such a
27+
spike clears within one sample, so a single-sample commit would refuse
28+
admission — and log a state change — while nothing is actually
29+
saturated. Thresholds are expressed as factors
2430
of the online scheduler count so a configuration is portable across
2531
machine sizes: a run queue of N x schedulers means roughly N runnable
2632
processes ahead of any newly runnable one on every scheduler.
@@ -49,12 +55,19 @@ Configuration (`bondy_regulator` application environment, set via the
4955
-define(DEFAULT_HIGH_WATERMARK, 8).
5056
-define(DEFAULT_LOW_WATERMARK, 4).
5157
-define(DEFAULT_SAMPLE_INTERVAL_MS, 100).
58+
%% Consecutive samples a crossing must hold before the status changes. An
59+
%% instantaneous run queue spikes whenever a wave of periodic timers wakes
60+
%% together, which an idle node does routinely; committing on one sample
61+
%% turns that into a refused HELLO and a pair of log lines.
62+
-define(DWELL_SAMPLES, 3).
5263

5364
-record(state, {
5465
ref :: atomics:atomics_ref(),
5566
high :: pos_integer(),
5667
low :: non_neg_integer(),
57-
interval_ms :: pos_integer()
68+
interval_ms :: pos_integer(),
69+
%% Consecutive samples the pending (not yet committed) status has held.
70+
dwell = 0 :: non_neg_integer()
5871
}).
5972

6073
%% API
@@ -63,6 +76,13 @@ Configuration (`bondy_regulator` application environment, set via the
6376
-export([start_link/0]).
6477
-export([status/0]).
6578

79+
-ifdef(TEST).
80+
%% Exposed so the dwell window can be pinned directly: driving it through
81+
%% the sampler would mean manufacturing a real run-queue spike shorter
82+
%% than the sampling period, which is not reproducible.
83+
-export([step/3]).
84+
-endif.
85+
6686
%% GEN_SERVER CALLBACKS
6787
-export([code_change/3]).
6888
-export([handle_call/3]).
@@ -167,35 +187,41 @@ handle_cast(Event, State) ->
167187
}),
168188
{noreply, State}.
169189

170-
handle_info(sample, State) ->
171-
#state{ref = Ref, high = High, low = Low} = State,
190+
handle_info(sample, State0) ->
191+
#state{ref = Ref, high = High, low = Low, dwell = Dwell0} = State0,
172192
RunQueue = erlang:statistics(total_run_queue_lengths_all),
173193
ok = atomics:put(Ref, ?RUN_QUEUE_SLOT, RunQueue),
174194

175195
Status = atomics:get(Ref, ?STATUS_SLOT),
176-
177-
case transition(Status, RunQueue, High, Low) of
178-
Status ->
179-
ok;
180-
1 ->
181-
ok = atomics:put(Ref, ?STATUS_SLOT, 1),
182-
?LOG_NOTICE(#{
183-
description =>
184-
"Node entered the busy state: admission gates will "
185-
"refuse new work until the run queue drains below "
186-
"the low watermark.",
187-
run_queue => RunQueue,
188-
high_watermark => High,
189-
low_watermark => Low
190-
});
191-
0 ->
192-
ok = atomics:put(Ref, ?STATUS_SLOT, 0),
193-
?LOG_NOTICE(#{
194-
description => "Node returned to the normal state.",
195-
run_queue => RunQueue,
196-
low_watermark => Low
197-
})
198-
end,
196+
Pending = transition(Status, RunQueue, High, Low),
197+
198+
State =
199+
case step(Status, Pending, Dwell0) of
200+
{hold, Dwell} ->
201+
State0#state{dwell = Dwell};
202+
{commit, 1} ->
203+
ok = atomics:put(Ref, ?STATUS_SLOT, 1),
204+
?LOG_NOTICE(#{
205+
description =>
206+
"Node entered the busy state: admission gates will "
207+
"refuse new work until the run queue drains below "
208+
"the low watermark.",
209+
run_queue => RunQueue,
210+
high_watermark => High,
211+
low_watermark => Low,
212+
dwell_samples => ?DWELL_SAMPLES
213+
}),
214+
State0#state{dwell = 0};
215+
{commit, 0} ->
216+
ok = atomics:put(Ref, ?STATUS_SLOT, 0),
217+
?LOG_NOTICE(#{
218+
description => "Node returned to the normal state.",
219+
run_queue => RunQueue,
220+
low_watermark => Low,
221+
dwell_samples => ?DWELL_SAMPLES
222+
}),
223+
State0#state{dwell = 0}
224+
end,
199225

200226
{noreply, schedule_sample(State)};
201227
handle_info(Info, State) ->
@@ -217,6 +243,18 @@ code_change(_OldVsn, State, _Extra) ->
217243
%% PRIVATE
218244
%% =============================================================================
219245

246+
%% @private
247+
%% The dwell step. A crossing is committed only once it has held for
248+
%% `?DWELL_SAMPLES` consecutive samples; a return to the committed side
249+
%% voids any partial dwell, so a spike shorter than the dwell window
250+
%% never changes the status.
251+
step(Status, Status, _Dwell) ->
252+
{hold, 0};
253+
step(_Status, Pending, Dwell) when Dwell + 1 >= ?DWELL_SAMPLES ->
254+
{commit, Pending};
255+
step(_Status, _Pending, Dwell) ->
256+
{hold, Dwell + 1}.
257+
220258
%% @private
221259
%% The hysteresis step: `1` (busy) at or above the high watermark, `0`
222260
%% (normal) at or below the low watermark, unchanged in between.

apps/bondy_regulator/test/bondy_regulator_load_test.erl

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,37 @@ fails_open_when_not_running_test() ->
1515
?assertEqual(normal, bondy_regulator_load:status()),
1616
?assertEqual(0, bondy_regulator_load:run_queue()).
1717

18+
%% A crossing shorter than the dwell window must not change the status.
19+
%% This is the case an idle node hits constantly: a wave of periodic
20+
%% timers wakes together, the instantaneous run queue spikes for one
21+
%% sample, and committing on it would refuse a HELLO.
22+
transient_spike_does_not_flip_test() ->
23+
%% normal(0) sees the high watermark crossed for two samples, then
24+
%% drops back before the third.
25+
?assertEqual({hold, 1}, bondy_regulator_load:step(0, 1, 0)),
26+
?assertEqual({hold, 2}, bondy_regulator_load:step(0, 1, 1)),
27+
?assertEqual({hold, 0}, bondy_regulator_load:step(0, 0, 2)).
28+
29+
sustained_crossing_commits_test() ->
30+
?assertEqual({hold, 1}, bondy_regulator_load:step(0, 1, 0)),
31+
?assertEqual({hold, 2}, bondy_regulator_load:step(0, 1, 1)),
32+
?assertEqual({commit, 1}, bondy_regulator_load:step(0, 1, 2)).
33+
34+
%% Recovery is debounced the same way, so a single quiet sample during
35+
%% real saturation does not re-open admission.
36+
sustained_recovery_commits_test() ->
37+
?assertEqual({hold, 1}, bondy_regulator_load:step(1, 0, 0)),
38+
?assertEqual({hold, 0}, bondy_regulator_load:step(1, 1, 1)),
39+
?assertEqual({hold, 1}, bondy_regulator_load:step(1, 0, 0)),
40+
?assertEqual({hold, 2}, bondy_regulator_load:step(1, 0, 1)),
41+
?assertEqual({commit, 0}, bondy_regulator_load:step(1, 0, 2)).
42+
43+
%% Staying on the committed side is always a no-op reset.
44+
steady_state_is_a_noop_test() ->
45+
?assertEqual({hold, 0}, bondy_regulator_load:step(0, 0, 0)),
46+
?assertEqual({hold, 0}, bondy_regulator_load:step(1, 1, 0)),
47+
?assertEqual({hold, 0}, bondy_regulator_load:step(0, 0, 2)).
48+
1849
lifecycle_test_() ->
1950
{timeout, 10, fun lifecycle/0}.
2051

apps/bondy_router/src/bondy_router.app.src

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313
"Bondy implements the open Web Application Messaging Protocol (WAMP) "
1414
"and is written in Erlang."
1515
},
16-
{vsn, "1.0.0-rc-amber"},
16+
{vsn, "1.0.0-rc-citron"},
1717
{registered, []},
1818
%% We pass the version number in the bondy_app:start/2 arguments
19-
{mod, {bondy_app, [{vsn, "1.0.0-rc-amber"}]}},
19+
{mod, {bondy_app, [{vsn, "1.0.0-rc-citron"}]}},
2020
{applications,[
2121
%% Erlang/OTP
2222
stdlib,

rebar.config

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@
149149

150150

151151
{relx, [
152-
{release, {bondy, "1.0.0-rc-amber"},[
152+
{release, {bondy, "1.0.0-rc-citron"},[
153153
%% Erlang/OTP
154154
crypto,
155155
inets,

0 commit comments

Comments
 (0)