Skip to content

fix(config_etcd): keep the previous value when a full reload gets invalid data - #13717

Open
AlinsRan wants to merge 5 commits into
apache:masterfrom
AlinsRan:fix/etcd-full-reload-keep-invalid-items
Open

fix(config_etcd): keep the previous value when a full reload gets invalid data#13717
AlinsRan wants to merge 5 commits into
apache:masterfrom
AlinsRan:fix/etcd-full-reload-keep-invalid-items

Conversation

@AlinsRan

@AlinsRan AlinsRan commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes #12834

The incremental watch path and the full reload path in core/config_etcd.lua disagree on invalid data:

A full reload is triggered asynchronously when etcd cancels the watch with compacted. So an item that validation rejects can serve for days, then vanish the moment an unrelated compaction happens. Because global plugins usually share one global rule, one invalid plugin conf takes down every global plugin — with no visible cause.

Reporter's case: a proxy-cache conf with the default cache_zone: disk_cache_one, on a node whose config.yaml declares different zones. check_schema validates cache_zone against the local node config, so the item is permanently invalid on that node.

Changes

Commit 1 — make full reload behave like the watch path. On validation failure load_full_data now carries the previous value over as is (no clean handlers, no filter re-run, no conf_version bump), instead of dropping it. It still skips an invalid item with no previous value (first load), still drops keys absent from etcd (no resurrection), and fires the clean handlers of replaced/deleted items after the new table is built. This is the same semantics the watch path has had since 3.10.0 — no new behavior, only removing "did a compaction happen to occur" as a factor.

Commit 2 — remove the amplifier. A plugin disabled via _meta.disable: true is never executed, so an environment-dependent validation failure should not invalidate the item that carries it. check_schema still runs (its default injection is wanted) but the failure is fatal only when the plugin is enabled; otherwise it is logged and accepted. Re-enabling goes through a normal fully-validated write, so an invalid conf can't silently become active. stream_check_schema gets the same treatment. This also relaxes the Admin API (shared check_single_plugin_schema), which is intended given heterogeneous config.yaml across data planes.

Tests

  • t/core/config_etcd.t: full reload keeps the previous value on invalid data (TEST 16), does not resurrect a deleted item (TEST 17), still skips an invalid item with no previous value (TEST 18). All drive the real sync loop against etcd.
  • t/plugin/proxy-cache/disk.t: Admin PUT of a disabled proxy-cache with a non-existent zone returns 200; the same conf enabled still returns 400.

Checklist

  • I have explained the need for this PR and the problem it solves
  • I have explained the changes or the new features added to this PR
  • I have added tests corresponding to this change
  • I have updated the documentation to reflect this change
  • I have verified that this change is backward compatible

AlinsRan added 2 commits July 20, 2026 15:28
…alid data

The full reload path (load_full_data) and the incremental watch path treat
an item that fails validation differently:

- the watch path only cancels the update of that item and keeps the value
  already in memory (`goto CONTINUE`, see apache#11268);
- the full reload path fires the clean handlers of every old item before
  reading from etcd and then skips the invalid item, so the item silently
  disappears from the data plane.

A full reload is triggered whenever the watch is cancelled by etcd with
`compacted`, which happens on ordinary etcd auto compaction when the global
revision moved on outside the watched prefix. An item that has been rejected
by the validation for a long time without any impact therefore vanishes at a
random point in time. Since global plugins usually all live on the same
global rule, one invalid plugin conf takes down every global plugin.

Make the full reload behave like the watch path: pass the previous values to
load_full_data and, when the new data of a key fails the validation and a
previous value for the same key exists, carry the old item over as is - no
clean handlers, no filter re-run, no version bump. Keys that are absent from
the readdir result are still dropped, and an invalid item without a previous
value (first load) is still skipped. The clean handlers of the old items that
were replaced or deleted are now fired after the new table has been built.

Fixes apache#12834
A plugin conf carrying `_meta.disable: true` is never executed, but its
check_schema is still run unconditionally, and a failure invalidates the
whole item (route, service, global rule, ...). Some plugins validate the
local environment inside check_schema - proxy-cache requires cache_zone to
be declared in the config.yaml of this very node - so a conf that is legal
elsewhere permanently invalidates the item on this node, even though the
plugin is turned off.

Keep running check_schema (its side effects, e.g. default injection, are
still wanted) but only reject the conf when the plugin is enabled; a
disabled plugin logs a warning and is accepted. Re-enabling it goes through
a full write and is validated again, so an invalid conf cannot silently
become active. Same treatment for the stream subsystem.
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. bug Something isn't working labels Jul 20, 2026
AlinsRan added 3 commits July 20, 2026 15:43
The blocks drive a real etcd sync round and sleep past the 5s default
block timeout, so they died on a client socket timeout rather than on an
assertion.
…y disabled

check_disable returns _meta.disable verbatim, so a truthy non-boolean
value such as the string "false" would have taken the downgrade path and
silently accepted a conf that the schema should have rejected.
The downgrade for _meta.disable plugins piped the check_schema error
straight into a warn. That string can echo config values (e.g. a pattern
mismatch), so a disabled plugin's bad config could leak into the shared
error log. Keep the warn as the signal that the config did not validate,
but drop the error detail — the caller can still see it by re-submitting
with the plugin enabled. Also assert the admin PUT actually returns
passed in the disk.t case, not just the log line.

@membphis membphis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comment on lines +689 to +697
-- fire the clean handlers of the previous items that were not carried
-- over: they were either replaced by a new value or deleted from etcd
if prev_values then
for _, item in ipairs(prev_values) do
if item and not carried[item] then
config_util.fire_all_clean_handlers(item)
end
end
end

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The entire clean handler should be removable after #12426

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right that clean handlers have no registrant after #12426 (health checks moved to healthcheck_manager), so this is effectively a no-op.

I've kept it here for consistency: config_yaml/config_xds full reloads fire on replaced/deleted items the same way, and dropping it only in the etcd full-reload path would leave a confusing asymmetry. Removing the whole clean_handlers mechanism uniformly (the config_util API plus all fire sites across the config backends) is broader than this bugfix, so I'll do it in a dedicated follow-up PR rather than entangle it with a backportable fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

3 participants