Skip to content

fix: ensure subsequent slots execute after PriorityWaitException in FlowSlot - #3607

Open
daguimu wants to merge 1 commit into
alibaba:masterfrom
daguimu:fix/flow-slot-priority-wait-skips-slots-issue3604
Open

fix: ensure subsequent slots execute after PriorityWaitException in FlowSlot#3607
daguimu wants to merge 1 commit into
alibaba:masterfrom
daguimu:fix/flow-slot-priority-wait-skips-slots-issue3604

Conversation

@daguimu

@daguimu daguimu commented Mar 26, 2026

Copy link
Copy Markdown

Problem

When FlowSlot.checkFlow() throws PriorityWaitException (for prioritized requests that pass flow control by waiting for a future token), fireEntry() is never called. This causes all subsequent slots in the chain to be skipped.

The default slot chain order is:

StatisticSlot(-7000) → ... → FlowSlot(-2000) → DefaultCircuitBreakerSlot(-1500) → DegradeSlot(-1000)

When PriorityWaitException is thrown, DefaultCircuitBreakerSlot and DegradeSlot are never executed. This means circuit breaker checks are bypassed for priority-waited requests — even if a circuit breaker is in the OPEN state, the request passes through unchecked.

Root Cause

In FlowSlot.entry(), checkFlow() is called before fireEntry(). When checkFlow() throws PriorityWaitException, the exception propagates immediately and fireEntry() (which chains to downstream slots) is never reached:

// Before fix
public void entry(...) throws Throwable {
    checkFlow(...);         // throws PriorityWaitException
    fireEntry(...);         // NEVER reached
}

Fix

Catch PriorityWaitException in FlowSlot.entry(), execute downstream slots via fireEntry(), then re-throw the exception for StatisticSlot to handle:

// After fix
public void entry(...) throws Throwable {
    try {
        checkFlow(...);
    } catch (PriorityWaitException ex) {
        fireEntry(...);     // Execute downstream slots (circuit breaker, degrade)
        throw ex;           // Re-throw for StatisticSlot
    }
    fireEntry(...);
}

If a downstream slot throws BlockException (e.g., circuit breaker is OPEN), the BlockException propagates instead of PriorityWaitException, and StatisticSlot handles it as a block.

Tests Added

  • testPriorityWaitExceptionStillFiresSubsequentSlots: Verifies that downstream slots are executed when PriorityWaitException is thrown, and the exception is still re-thrown.
  • testPriorityWaitWithDownstreamBlockPropagatesBlockException: Verifies that if a downstream slot throws BlockException after a priority wait, the BlockException takes precedence.

All 225 tests in sentinel-core pass.

Impact

Only affects FlowSlot.entry() behavior when PriorityWaitException is thrown (prioritized QPS-based flow control with token waiting). Normal flow control (pass/block) is unchanged.

Fixes #3604

…lowSlot

When FlowSlot.checkFlow() throws PriorityWaitException (for prioritized
requests that pass by waiting), fireEntry() was never called, causing
all subsequent slots in the chain (DefaultCircuitBreakerSlot, DegradeSlot)
to be skipped. This means circuit breaker checks were bypassed for
priority-waited requests.

Catch PriorityWaitException in FlowSlot.entry(), execute downstream
slots via fireEntry(), then re-throw the exception for StatisticSlot
to handle. If a downstream slot throws BlockException (e.g. circuit
breaker is open), the BlockException propagates instead.

Fixes alibaba#3604

@oss-sentinel-ai oss-sentinel-ai left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Summary

Fixes a real bypass: when FlowSlot.checkFlow() throws PriorityWaitException for a prioritized request (future-token reservation via DefaultController.canPass()), the old code never calls fireEntry(), so the downstream DefaultCircuitBreakerSlot and DegradeSlot are skipped — circuit-breaker/degrade checks were silently bypassed for priority-waited requests. The fix wraps checkFlow() in a try/catch for PriorityWaitException only, calls fireEntry() so the rest of the chain runs its checks, then re-throws so CtSph still treats it as a prioritized pass. Entry/exit pairing is preserved (CtEntry is constructed before the chain runs and exit() unwinds symmetrically); non-prioritized requests are unaffected. Hot-path overhead is negligible: one try/catch, no allocations, no locking (PriorityWaitException.fillInStackTrace() is already a no-op). Both added tests are deterministic and directly assert the fixed behavior, including the downstream-block-takes-precedence semantics. Java 8 compatible. Two minor info-level notes inline.

Findings

  • [Info] sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/FlowSlot.java:165 — when a downstream slot throws BlockException from the new fireEntry() call, it takes precedence over PriorityWaitException and the reserved priority-wait token remains outstanding; consistent with existing StatisticSlot semantics, but worth a comment
  • [Info] sentinel-core/src/test/java/com/alibaba/csp/sentinel/slots/block/flow/FlowSlotTest.java:117 — tests chain a single mock downstream slot; production order is FlowSlot → DefaultCircuitBreakerSlot → DegradeSlot, so the full path isn't exercised

Suggestions

  • Optional: add a comment in FlowSlot.entry() documenting that a downstream BlockException takes precedence over PriorityWaitException and the token reservation stays outstanding in that case.
  • Optional: strengthen testPriorityWaitWithDownstreamBlockPropagatesBlockException by chaining two downstream slots so a real performChecking() path is exercised.

Automated review by github-manager-bot

try {
checkFlow(resourceWrapper, context, node, count, prioritized);
} catch (PriorityWaitException ex) {
// When a prioritized request passes flow control by waiting, subsequent slots

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Minor design note: when a downstream slot (e.g. DefaultCircuitBreakerSlot) throws BlockException from inside this fireEntry() call, the BlockException propagates out of the catch block and silently replaces the PriorityWaitException. The token reservation made by DefaultController.canPass() (tryOccupyNext + addWaitingRequest + addOccupiedPass) therefore remains outstanding even though the request is ultimately blocked. This is consistent with the existing StatisticSlot behavior for prioritized passes (pass-QPS is already incremented when PriorityWaitException is caught in StatisticSlot.java:81), and the circuit breaker's decision correctly takes precedence at the user-visible layer, but reviewers should be aware that prioritized-wait tokens can be "spent" on requests that never reach business logic when a downstream rule fires. No code change is strictly required — it's a consequence of the slot-chain model — but it may be worth mentioning in the PR description or in a comment here.

// Track whether the next slot in the chain is called
final AtomicBoolean nextSlotCalled = new AtomicBoolean(false);
AbstractLinkedProcessorSlot<DefaultNode> nextSlot = new AbstractLinkedProcessorSlot<DefaultNode>() {
@Override

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Test coverage observation: this test installs a single downstream blockingSlot directly via flowSlot.setNext(...), which only proves that FlowSlot.fireEntry() invokes its immediate successor's entry(). In production, the chain is FlowSlot → DefaultCircuitBreakerSlot → DegradeSlot, and both DefaultCircuitBreakerSlot and DegradeSlot perform their rule checks before calling fireEntry() (see performChecking(...) in each), so a more faithful test would chain two slots and assert that the circuit-breaker check actually runs (e.g. a mocked CircuitBreaker.tryPass() is invoked, or a DegradeException propagates out of flowSlot.entry()). The current test is still a valid regression guard for the specific fireEntry-skip bug, but it doesn't exercise the full path the PR description claims to fix.

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.

2 participants