You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
traffic_ctl config reload reports a completed reload before plugin config callbacks have
run, and a plugin that rejects a config has no way to say so. Found while reviewing #13600; rate_limit is used below as the probe, but this is not a rate_limit defect.
Impact
Plugin config callbacks work, and have since 2015 — TSMgmtUpdateRegister dispatch is
unaffected by this. What is missing is that the reload task tree neither waits for those
callbacks nor records their outcome, so the RPC asserts a completeness it cannot know.
Concretely: a plugin that rejects a config keeps its previous configuration, logs to diags.log, and the reload still exits 0 with every subtask green. An operator who fixes a
malformed plugin config, reloads, and sees a green result has no signal that the running
configuration is not the one on disk. The callback's return value is discarded, so there is
nowhere for the plugin to put the answer.
Six global plugins register on master:
plugin
registration
can it reject a config today?
experimental/rate_limit
3-arg, file + mtime gate
yes — return false keeps the previous config
regex_revalidate
2-arg
no reject path surfaced
compress
2-arg
no — management_update() calls load_global_configuration() and returns 0
lua
2-arg
no reject path surfaced
stats_over_http
2-arg
no reject path surfaced
experimental/wasm
2-arg
no reject path surfaced
rate_limit is the only one passing a filename, so the only one gated on mtime; the others
fire on every reload whether or not their config changed. It is also the only one where the
tree currently reports a wrong answer rather than merely an incomplete one.
Since when. The dispatch is old — ConfigUpdateCbTable traces to 2015 — but it was not
wrong until there was a task tree to be absent from. #12892 (5bab268cb4, 2026-03-13)
introduced ReloadCoordinator, the per-task status model, and the reserve_subtask
protocol, wiring in FileManager, Configuration.cc, and traffic_server.cc. It did not
touch src/api/ConfigUpdateCbTable.cc or include/api/InkAPIInternal.h. Before that commit
the RPC made no granular claim; after it, it does.
Version: master @ 956747452dcd36e8e56064aeaa47b0d8493ad0ac
Platform: Darwin 25.6.0, Apple clang 21.0.0
Config: plugin.config -> rate_limit.so <path>/rate_limit.yaml
Proof
Two halves of one protocol; the plugin path has only the second.
ConfigRegistry::on_record_change() reserves before scheduling, and says why:
// Pre-register a CREATED subtask so the main task knows work is pending.// Without this, aggregate_status() can reach SUCCESS before the continuation// runs and creates the subtask.ReloadCoordinator::Get_Instance().reserve_subtask(ctx->config_key);
eventProcessor.schedule_imm(new RecordTriggeredReloadContinuation(ctx->mutex, ctx->config_key), ET_TASK);
ConfigUpdateCbTable::invoke() schedules without reserving — exactly the case that comment
warns about:
(timestamps, id, and absolute paths elided as ... / <path>)
ssl_ticket_key is the only subtask in either form — rate_limit never appears, in the
tree or in the exit code. The main task's last_updated_time_ms falls in the same
millisecond as the plugin's ERROR lines: the task closed while the callback was still
failing beside it. The 1/1 and the 13ms are the clearest statement of the problem —
the tree finished counting before the plugin was consulted.
Proposed change
Two defects, and they need fixing together rather than separately.
(a) No reservation.invoke() schedules on ET_TASK without reserve_subtask(), so aggregate_status() reaches SUCCESS before the callback has run. Affects all six
registrants regardless of whether they can fail — it is what produces 1/1 success (13ms)
above with rate_limit absent from the tree.
(b) No failure channel.ConfigUpdateCallback::event_handler discards the callback's
return value, so a plugin that rejects a config has nowhere to report it.
Worth stating explicitly: fixing (a) alone would be worse than the current behaviour. A
reserved subtask with no way to fail can only ever be marked complete, so the tree would
show rate_limit: success for a config rate_limit had just rejected. Today it shows
nothing, which is at least not a false claim.
I have deliberately not proposed a patch, because the right shape depends on a decision I
should not make alone — whether TSMgmtUpdateRegister is worth extending at all:
Extend it. Add reserve_subtask() in invoke() (the plugin name is already the cb_table key) and a failure channel — for example treating TS_EVENT_ERROR from the
callback as failure and mapping it to ConfigContext::fail(). No new TSAPI, no enum
change, no ABI break, and each existing plugin becomes fixable with a one-line change.
Would need a sweep of the six to confirm none returns TS_EVENT_ERROR today for an
unrelated reason.
Replace it.[TS API] Add plugin API for config reload framework #13146 adds TSCfgRegister / TSCfgLoadCtx*, which gives migrated
plugins both halves properly. If that is the intended direction, this issue becomes
"migrate the six and deprecate TSMgmtUpdateRegister" once it lands. Noting it as a
candidate, not a dependency — the defect above is present on master today either way.
Not done: no test. An autest asserting a non-zero exit from traffic_ctl config reload when
a plugin rejects its config would be the regression guard, and would fail today.
traffic_ctl config reloadreports a completed reload before plugin config callbacks haverun, and a plugin that rejects a config has no way to say so. Found while reviewing #13600;
rate_limitis used below as the probe, but this is not arate_limitdefect.Impact
Plugin config callbacks work, and have since 2015 —
TSMgmtUpdateRegisterdispatch isunaffected by this. What is missing is that the reload task tree neither waits for those
callbacks nor records their outcome, so the RPC asserts a completeness it cannot know.
Concretely: a plugin that rejects a config keeps its previous configuration, logs to
diags.log, and the reload still exits 0 with every subtask green. An operator who fixes amalformed plugin config, reloads, and sees a green result has no signal that the running
configuration is not the one on disk. The callback's return value is discarded, so there is
nowhere for the plugin to put the answer.
Six global plugins register on master:
experimental/rate_limitreturn falsekeeps the previous configregex_revalidatecompressmanagement_update()callsload_global_configuration()and returns 0luastats_over_httpexperimental/wasmrate_limitis the only one passing a filename, so the only one gated on mtime; the othersfire on every reload whether or not their config changed. It is also the only one where the
tree currently reports a wrong answer rather than merely an incomplete one.
Since when. The dispatch is old —
ConfigUpdateCbTabletraces to 2015 — but it was notwrong until there was a task tree to be absent from. #12892 (
5bab268cb4, 2026-03-13)introduced
ReloadCoordinator, the per-task status model, and thereserve_subtaskprotocol, wiring in
FileManager,Configuration.cc, andtraffic_server.cc. It did nottouch
src/api/ConfigUpdateCbTable.ccorinclude/api/InkAPIInternal.h. Before that committhe RPC made no granular claim; after it, it does.
Proof
Two halves of one protocol; the plugin path has only the second.
ConfigRegistry::on_record_change()reserves before scheduling, and says why:trafficserver/src/mgmt/config/ConfigRegistry.cc
Lines 165 to 174 in 9567474
ConfigUpdateCbTable::invoke()schedules without reserving — exactly the case that commentwarns about:
trafficserver/src/api/ConfigUpdateCbTable.cc
Lines 75 to 79 in 9567474
ConfigUpdateCallback::event_handlerthen discards the callback's return value, so even aconvention-based failure signal would have nowhere to go:
trafficserver/include/api/InkAPIInternal.h
Lines 117 to 134 in 9567474
Reproduction (rate_limit used as a probe)
A duplicate-SNI config, which takes
rate_limit's existing non-throwing rejection path:The plugin rejects it and keeps the previous configuration:
The same reload cycle reports success and exits 0:
Human:
JSON:
(timestamps,
id, and absolute paths elided as.../<path>)ssl_ticket_keyis the only subtask in either form —rate_limitnever appears, in thetree or in the exit code. The main task's
last_updated_time_msfalls in the samemillisecond as the plugin's ERROR lines: the task closed while the callback was still
failing beside it. The
1/1and the13msare the clearest statement of the problem —the tree finished counting before the plugin was consulted.
Proposed change
Two defects, and they need fixing together rather than separately.
(a) No reservation.
invoke()schedules onET_TASKwithoutreserve_subtask(), soaggregate_status()reaches SUCCESS before the callback has run. Affects all sixregistrants regardless of whether they can fail — it is what produces
1/1 success (13ms)above with
rate_limitabsent from the tree.(b) No failure channel.
ConfigUpdateCallback::event_handlerdiscards the callback'sreturn value, so a plugin that rejects a config has nowhere to report it.
Worth stating explicitly: fixing (a) alone would be worse than the current behaviour. A
reserved subtask with no way to fail can only ever be marked complete, so the tree would
show
rate_limit: successfor a configrate_limithad just rejected. Today it showsnothing, which is at least not a false claim.
I have deliberately not proposed a patch, because the right shape depends on a decision I
should not make alone — whether
TSMgmtUpdateRegisteris worth extending at all:reserve_subtask()ininvoke()(the plugin name is already thecb_tablekey) and a failure channel — for example treatingTS_EVENT_ERRORfrom thecallback as failure and mapping it to
ConfigContext::fail(). No new TSAPI, no enumchange, no ABI break, and each existing plugin becomes fixable with a one-line change.
Would need a sweep of the six to confirm none returns
TS_EVENT_ERRORtoday for anunrelated reason.
TSCfgRegister/TSCfgLoadCtx*, which gives migratedplugins both halves properly. If that is the intended direction, this issue becomes
"migrate the six and deprecate
TSMgmtUpdateRegister" once it lands. Noting it as acandidate, not a dependency — the defect above is present on master today either way.
Not done: no test. An autest asserting a non-zero exit from
traffic_ctl config reloadwhena plugin rejects its config would be the regression guard, and would fail today.