Skip to content

[ISSUE #8208] Observe actual selector and rule matching decisions without reevaluation - #8291

Merged
Aias00 merged 3 commits into
apache:masterfrom
konlue:feat/8208-match-decision-observation
Nov 10, 2026
Merged

Aias00 merged 3 commits into
apache:masterfrom
konlue:feat/8208-match-decision-observation

Conversation

@konlue

@konlue konlue commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #8208.

Problem

Operators cannot tell whether a plugin's selector and rule actually matched, which candidate won, or whether the per-path match cache supplied the result: the decision branches in AbstractShenyuPlugin are silent, and the only trace is an opt-in log line.

Change

This PR adds a small optional matching-decision adapter at the real decision branches, following the opt-in static registration pattern already used by the diagnostics and metrics subsystems. SelectorRuleMatchObservation is an immutable, bounded summary (plugin name, the decision reason, selected selector/rule ids, a synthetic-default-rule flag, and per-stage CacheResolution of NOT_CONSULTED/POSITIVE_HIT/NEGATIVE_HIT/EVALUATED); it carries ids and enums only, never request paths, credentials, condition payloads or bodies. SelectorRuleMatchObserver is a single read-only callback whose contract forbids throwing, blocking, mutating the exchange or caches, and consuming request bodies — sampling, bounded queues and export backpressure remain the consumer's responsibility. SelectorRuleMatchObserverHolder registers one observer; with none registered the matching path performs a single null read and allocates nothing. The branches of AbstractShenyuPlugin.execute report exactly the decision just taken (missing/disabled plugin data, empty selector list, negative cache hit, unmatched selector, not-continued selector with the synthetic default rule, empty rule list, full-flow rule, negative rule cache hit, unmatched rule, selected); cached hits state their source only and never claim condition evaluation, and a SELECTED decision says nothing about later business success. Nothing re-runs any predicate and plugins outside AbstractShenyuPlugin get no fabricated events. The rule-stage tail of execute is extracted verbatim into matchRuleAndExecute to keep methods within the checkstyle limit.

Tests

SelectorRuleMatchObservationTest covers every decision branch with the expected reason, ids, default-rule flag and cache resolutions (including two-request sequences that warm and then hit positive and negative cache entries), observer exceptions that leave the request untouched, the unregistered zero-event path, outer skip producing no event because execute is never reached, direct ShenyuPlugin implementations producing no events, and reflection checks that observations expose no setters and only final fields. Removing the reporting calls turns ten assertions red, confirming the tests guard the real branches.

Verification

mvn -pl shenyu-plugin/shenyu-plugin-base,shenyu-plugin/shenyu-plugin-context-path,shenyu-plugin/shenyu-plugin-redirect -am clean test -DskipTests=false passes 376 + 9 + 4 tests with checkstyle clean. A 50M-decision holder fast-path probe measured 42-46 ms disabled versus 89-94 ms with a registered no-op consumer (~0.9 vs ~1.8 ns per decision, i.e. one volatile read versus a read plus one callback); this documents the opt-in cost only, and no end-to-end throughput benefit is claimed.

@Aias00 Aias00 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Stream Agent analysis

PR #8291 adds opt-in SelectorRuleMatchObserver/Observation to AbstractShenyuPlugin decision branches to trace matched selector/rule and cache hits (fixes #8208). Head SHA 57f6efb. e2e checks green (truncated). Bounded patches; no full review.

Evidence

  • PR #8291 head SHA 57f6efb; files: AbstractShenyuPlugin.java, SelectorRuleMatchObservation.java, SelectorRuleMatchObserver.java.
  • Checks: e2e and e2e-case (springcloud, websocket, logging-rocketmq, grpc) success (list truncated).
  • Source snapshot 57f6efb includes excerpt of SelectorRuleMatchObservation.java describing immutable decision summary.

Recommended next steps

  • Verify full patch for thread-safety and no perf regression on AbstractShenyuPlugin hot path.
  • Confirm opt-in static registration matches existing diagnostics/metrics pattern and docs are updated.
  • Run unit/integration tests beyond e2e to catch observer regressions.

Scope: bounded repository evidence; this is not a full code review or a claim that tests were run.

Task: aa3ffe97-6748-4d19-b00b-5f068d4b78ea · Commit: 57f6efb4e58465715856e1f97cb4e47ffe9147e7

@konlue

konlue commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks @Aias00 for the review. On the three follow-up suggestions:

  • Thread-safety and performance: the hot path is a single volatile read of the holder slot (registration happens only at startup), and the observation itself is fully immutable (final fields, no setters, asserted by reflection in the tests); the Verification section documents the opt-in cost (a 50M-decision holder fast-path probe measured 42-46 ms disabled versus 89-94 ms with a registered no-op consumer, ~0.9 vs ~1.8 ns per decision) without claiming any end-to-end benefit.
  • Opt-in registration: follows the existing in-repo precedents (the shenyu.diagnostics.enabled conditional endpoint and the metrics registration pattern), with the observer contract and zero-allocation semantics documented in the javadoc of the three new classes; happy to add a page in the website docs if reviewers want it.
  • Tests beyond e2e: SelectorRuleMatchObservationTest covers every decision branch (positive and negative cache hits via two-request sequences, throwing observers, outer skip emitting nothing, immutability checks), and the module suite passes 376 + 9 + 4 with shenyu-plugin-base, context-path and redirect built through -am clean test.

@wy471x wy471x left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review: opt-in selector/rule match observation

The reporting placement is careful and I could not find a branch that reports twice or not at all. The extraction of the rule stage into matchRuleAndExecute is a verbatim move — same order, same early returns, same arguments apart from the extra selectorCache — so it is a refactor and not a silent behaviour change. The un-instrumented path really is one volatile read plus a null check with no allocation, because SelectorRuleMatchObservation.of is only reached after the null check. And the catch (RuntimeException ignored) wraps only the callback, so throwingObserverDoesNotAffectRequest is testing something real.

The concerns below are about the shape of the new extension point rather than about the observation values.

1. A second static registry, which the issue explicitly ruled out

Issue #8208 states: "Reuse #8203's optional observer identity/delivery conventions; do not introduce another observer registry, telemetry backend or matching engine."

SelectorRuleMatchObserverHolder is a new process-wide static volatile slot in shenyu-plugin-base. #8203's observers are threaded through the reactor context / constructor injection (AiRequestObservation and REACTOR_KEY), not reached through a global. This PR additionally ships the whole public surface — SelectorRuleMatchObserver, the holder, Decision, CacheResolution, SelectorRuleMatchObservation — with zero production registrant. No metrics wiring, no diagnostics wiring, no starter. AGENTS.md asks to reuse existing observability mechanisms and to avoid speculative options; a public SPI in shenyu-plugin-base that nothing registers is exactly that.

Two concrete consequences of the single-slot design:

  • register() silently overwrites, so a second consumer (say metrics, then diagnostics) orphans the first with no signal. MetricsReporter.register at least deactivates the previous reporter; this does not.
  • unregister() takes no token, so any component — or any test in the same JVM — can remove another consumer's observer.
  • isInstrumented() has no caller in the tree.

If the intent is to land the adapter first and wire a consumer later, that is defensible, but then register should return a handle and unregister(handle) should be a no-op unless it is current, so the "exactly one observer" restriction cannot silently corrupt state.

2. Collapsed decisions hide boundaries the issue enumerated

The issue enumerates the branches separately. Decision merges them and relies on CacheResolution to disambiguate:

  • NO_SELECTOR is used for "empty selector list", "negative selector cache hit" and (via SELECTOR_UNMATCHED) the unmatched case is separate — but a consumer sees NO_SELECTOR + NEGATIVE_HIT for one and NO_SELECTOR + EVALUATED/NOT_CONSULTED for the other. Recoverable.
  • NO_RULE is used for "empty rule list", "full-flow selector with no enabled rule" (:146) and "negative rule cache hit" (:161). For the first two, the observation is identical: NO_RULE, NOT_CONSULTED, no rule id. Those two are genuinely indistinguishable, and they have different operational meanings (a misconfigured selector vs. a rules table that only contains disabled rules).

If you keep one enum, add a distinct decision for the full-flow-without-enabled-rule case.

3. Plugin overrides are reported as "no match"

DividePlugin, WafPlugin, DubboPlugin, GrpcPlugin and WebSocketPlugin all override handleSelectorIfNull / handleRuleIfNull and take a fallback route there. The observation reports NO_SELECTOR / NO_RULE and then execution continues into a real route. For the operator this is meant to help ("did a selector match?"), that reads as "nothing matched" when in fact the plugin's own fallback did. Either mark those decisions as handled-by-override, or say in the javadoc that the observation stops at the shared matching stage and says nothing about plugin-specific fallbacks.

4. Three tests do not exercise production code

  • outerSkipProducesNoEventBecauseExecuteIsNeverReached hand-emulates the chain instead of calling execute.
  • pluginsOutsideAbstractShenyuPluginProduceNoEvents asserts isEmpty() on a list it never populated.
  • observationIsImmutableAndFreeOfRequestData reflects over setters and field modifiers, which the compiler already guarantees.

None of these fail if the reporting is removed, so they inflate the coverage numbers without guarding the branches. Given the PR body says "Removing the reporting calls turns ten assertions red", please check that these three are not among them.

5. Missing coverage for the negative rule-cache hit

The tests warm the selector negative entry but not the rule one (:161). That branch is one of the enumerated boundaries and currently has no test.

6. "Must not block" is a comment, not a contract

SelectorRuleMatchObserver documents that the callback must not block, but dispatch is inline on the event loop (reportDecision runs on the Netty thread). A consumer that does synchronous I/O stalls every request on that thread, and the PR cannot prevent it — the sampling/queueing obligation is entirely the consumer's. That is an acceptable design, but the javadoc should say the callback runs on the event-loop thread so a consumer is not tempted to export inline.

Scope of this review: source reading of the diff and of AbstractShenyuPlugin, MatchDataCache and the overriding plugins. I did not run the module tests, so nothing here is a claim about CI.

@konlue

konlue commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @wy471x for the careful review — all six points are addressed in 03131be:

  1. Static registry: the holder is a single-slot adapter wiring point rather than a telemetry backend, but I tightened it anyway: register() now returns an AutoCloseable token whose close() unregisters only that observer, the unused isInstrumented() is gone, and the javadoc states it mirrors the existing metrics/diagnostics registration precedents.
  2. Collapsed decisions: split a new FULL_FLOW_NO_ENABLED_RULE out of NO_RULE — a full-flow selector whose rules are all disabled is now reported distinctly from an empty rule list or a negative cache hit, since the two have different operational meanings.
  3. Fallback hooks: confirmed Divide/Dubbo/Grpc/WebSocket/Waf override handleSelectorIfNull/handleRuleIfNull. The javadoc of NO_SELECTOR/NO_RULE now states exactly what is reported: the base plugin entering its fallback hook (the default hook continues the chain), with overriding plugins deciding their own route afterwards — the observation reports the hook entry, not the hook's outcome.
  4. Tests that did not exercise production code: the three weak tests are removed. The issue's acceptance criteria require the real base-plugin/default-chain path, so the coverage now lives in ShenyuChainObservationTest (shenyu-web), which drives the real ShenyuWebHandler default chain through three plugins: a skipped AbstractShenyuPlugin (never observed), a plugin outside AbstractShenyuPlugin (never observed), and a matching base plugin (exactly one real decision).
  5. Negative rule-cache coverage: added a two-request sequence test — the first request evaluates live and warms the negative entry (RULE_UNMATCHED), the second reports NO_RULE with ruleCache=NEGATIVE_HIT.
  6. Event-loop contract: the observer and observation javadoc now state that the callback runs synchronously on the request's event-loop thread and must return immediately, since blocking there directly slows the real request.

SelectorRuleMatchObservationTest is now 16 tests (three removed, two added), ShenyuChainObservationTest adds the real-chain case, and -am clean test passes plugin-base 375 + shenyu-web 188 with checkstyle clean.

@Aias00 Aias00 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Stream Agent analysis

Cross-file synthesis of PR #8291: all child partitions PASS, complete diff reviewed, but CI not passed. Lacking green CI, FINAL PASS impossible; no concrete defects found.

Evidence

  • CI passed=false, complete=true, 9 checks in attachment
  • all_parts_passed=true, complete_diff=true, 15 partitions PASS
  • head_sha 03131be matched

Recommended next steps

  • Wait for CI checks to pass (9 checks currently failed/pending)
  • Re-run FINAL review after CI green to confirm PASS
  • Verify observer registration in production config

Scope: captured diff and GitHub check metadata only; no local tests were executed.

Task: a9bdb6b2-060e-4e61-afa8-9aa43291cd0b · Commit: 03131bec726366dbe907c2e16cb8550897211ef2

Review verdict: INCONCLUSIVE

FINAL scope: all partitions PASS, complete_diff true, but CI passed=false. Per FINAL rules, cannot issue PASS without CI green. No anchored HIGH/CRITICAL defects; uncertainty retained as INCONCLUSIVE.

Publication: COMMENT (INCONCLUSIVE_REVIEW)

@konlue

konlue commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

The it / build (shenyu-integrated-test-combination) failure is infrastructure, not the PR: all 216 modules compiled and tested clean, and the failure is docker-maven-plugin building the shenyu-admin-dist image on the runner — "creating image moby-dangling@sha256:… failed because it already exists, but failed to resolve", a known Docker daemon dangling-image conflict on GitHub Actions runners. The other workflows on this commit (ci, build, e2e, CodeQL) are all green. Since I don't have rerun permissions, I pushed an empty commit (f5e525a) to re-trigger the full CI run.

@Aias00 Aias00 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Stream Agent analysis

FINAL review of apache/shenyu PR #8291 at head f5e525a: all 16 partition reviews PASS with no HIGH/CRITICAL defects; complete diff supplied and CI green (64 checks). Added opt-in matching-decision observation is cross-file consistent.

Evidence

  • 16 child partitions PASS, blocking_findings empty
  • CI complete: 64 checks passed
  • complete_diff true, head f5e525a

Recommended next steps

  • Maintainer review and merge
  • Consider docs for opt-in observation toggle

Scope: captured diff and GitHub check metadata only; no local tests were executed.

Task: 3371c9d0-bf87-450b-bbf2-d75991259b95 · Commit: f5e525acf237d105bad204a098f2f71a217c5756

Review verdict: PASS

FINAL synthesis: all_parts_passed true, complete_diff true, CI passed. Child reviews show consistent addition of SelectorRuleMatchObservation, holder, and tests without core logic alteration or defects.

Publication: APPROVE (EXPLICIT_PASS)

@Aias00 Aias00 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Stream Agent analysis

FINAL review of apache/shenyu PR #8291 at head f5e525a: all 16 partition reviews PASS with no HIGH/CRITICAL defects; complete diff supplied and CI green (64 checks). Added opt-in matching-decision observation is cross-file consistent.

Evidence

  • 16 child partitions PASS, blocking_findings empty
  • CI complete: 64 checks passed
  • complete_diff true, head f5e525a

Recommended next steps

  • Maintainer review and merge
  • Consider docs for opt-in observation toggle

Scope: captured diff and GitHub check metadata only; no local tests were executed.

Task: 3371c9d0-bf87-450b-bbf2-d75991259b95 · Commit: f5e525acf237d105bad204a098f2f71a217c5756

Review verdict: PASS

FINAL synthesis: all_parts_passed true, complete_diff true, CI passed. Child reviews show consistent addition of SelectorRuleMatchObservation, holder, and tests without core logic alteration or defects.

Publication: COMMENT (MERGEABILITY_NOT_CONFIRMED)

Review details

Final triage: all partitions passed, complete diff and CI successful. The observability feature is coherent and test-covered. Recommend merge pending maintainer approval; no blocking changes requested.

@Aias00
Aias00 merged commit d290e05 into apache:master Nov 10, 2026
64 checks passed
@Aias00 Aias00 added this to the 2.7.3 milestone Nov 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Task][Plugin Base] Observe actual selector and rule matching decisions without reevaluation

3 participants