Skip to content

Fix Reactor adapter async-context race in SentinelReactorSubscriber.currentContext - #3624

Open
wuwen5 wants to merge 1 commit into
alibaba:1.8from
wuwen5:1.8-fix-race-condition-reactor-adapter
Open

Fix Reactor adapter async-context race in SentinelReactorSubscriber.currentContext#3624
wuwen5 wants to merge 1 commit into
alibaba:1.8from
wuwen5:1.8-fix-race-condition-reactor-adapter

Conversation

@wuwen5

@wuwen5 wuwen5 commented May 22, 2026

Copy link
Copy Markdown
Contributor

Describe what this PR does / why we need it

SentinelReactorSubscriber.currentContext() could throw NullPointerException: value under concurrent termination because it read currentEntry.getAsyncContext() twice. If AsyncEntry.exit() cleared async context between those reads, Context.put(...) received null.

  • Race fix in context propagation

    • Updated currentContext() to use the already-captured local sentinelContext when putting Sentinel context into Reactor Context.
    • This removes the check-then-act race window created by re-reading currentEntry.getAsyncContext().
  • Targeted regression coverage

    • Added SentinelReactorSubscriberTest in the reactor adapter test module.
    • The test drives a deterministic interleaving where currentContext() captures a non-null async context, a concurrent cancel path exits/clears context, and currentContext() must still propagate the captured value (the pre-fix path would pass null to Context.put).
com.alibaba.csp.sentinel.context.Context sentinelContext = currentEntry.getAsyncContext();
if (sentinelContext == null) {
    return actual.currentContext();
}
return actual.currentContext()
    .put(SentinelReactorConstants.SENTINEL_CONTEXT_KEY, sentinelContext);
26-05-21 09:28:59.436  WARN [x-server,,,TID:N/A] 766243 --- [ventExecutorLoop-1-4] [i.n.u.c.SingleThreadEventExecutor       :1002] [                 run] : Unexpected exception from an event executor:
java.lang.NullPointerException: value
        at java.util.Objects.requireNonNull(Objects.java:228)
        at reactor.util.context.Context5.put(Context5.java:61)
        at com.alibaba.csp.sentinel.adapter.reactor.SentinelReactorSubscriber.currentContext(SentinelReactorSubscriber.java:70)
        at com.alibaba.csp.sentinel.adapter.reactor.InheritableBaseSubscriber.onError(InheritableBaseSubscriber.java:169)
        at com.alibaba.csp.sentinel.adapter.reactor.SentinelReactorSubscriber.onError(SentinelReactorSubscriber.java:37)
        at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onError(FluxOnAssembly.java:544)
        at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onError(FluxOnAssembly.java:544)
        at reactor.core.publisher.FluxDoFinally$DoFinallySubscriber.onError(FluxDoFinally.java:119)
        at reactor.core.publisher.FluxContextWrite$ContextWriteSubscriber.onError(FluxContextWrite.java:121)
        at reactor.core.publisher.MonoFlatMap$FlatMapMain.onError(MonoFlatMap.java:172)
        at reactor.core.publisher.MonoFlatMap$FlatMapMain.secondError(MonoFlatMap.java:192)
        at reactor.core.publisher.MonoFlatMap$FlatMapInner.onError(MonoFlatMap.java:259)
        at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.onError(MonoIgnoreThen.java:278)
        at reactor.core.publisher.FluxDoFinally$DoFinallySubscriber.onError(FluxDoFinally.java:119)
        at reactor.core.publisher.FluxPeek$PeekSubscriber.onError(FluxPeek.java:222)
        at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.onError(MonoIgnoreThen.java:278)
        at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.onError(MonoPeekTerminal.java:258)
        at org.springframework.http.server.reactive.ChannelSendOperator$WriteCompletionBarrier.onError(ChannelSendOperator.java:418)

某Redis Reactive请求先发生异常
→ Reactor进入onError链路
→ SentinelReactorSubscriber.currentContext()
→ Context.put(null)
→ NullPointerException
→ 未捕获异常打穿Netty EventExecutor
→ lettuce-eventExecutorLoop-1-4 号线程死亡(threadStatus=2)
→ executor terminated
→ 后续Redis Reactive callback无法调度
→ Spring Session Reactive Pipeline部分卡死 (重启恢复)

Does this pull request fix one issue?

fixed #3556 #1907

This pull request was created from Copilot chat.

@CLAassistant

CLAassistant commented May 22, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@LearningGp

Copy link
Copy Markdown
Collaborator
image

…currentContext` (#1)

* fix(reactor): avoid async context race in currentContext and add regression test
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: wuwen5 <5037807+wuwen5@users.noreply.github.com>
@wuwen5
wuwen5 force-pushed the 1.8-fix-race-condition-reactor-adapter branch from d5bdc20 to b6f58d3 Compare May 27, 2026 10:25
@wuwen5

wuwen5 commented May 27, 2026

Copy link
Copy Markdown
Contributor Author

@LearningGp done.

All committers have signed the CLA.

@angus-guo angus-guo 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.

Excellent fix for a subtle race condition! ✅

Problem Analysis

The root cause is a check-then-act race in currentContext():

// Line 65: First read
com.alibaba.csp.sentinel.context.Context sentinelContext = currentEntry.getAsyncContext();
if (sentinelContext == null) {
    return actual.currentContext();
}
// Line 70: Second read - race window here!
return actual.currentContext()
    .put(SentinelReactorConstants.SENTINEL_CONTEXT_KEY, currentEntry.getAsyncContext());

Between the two getAsyncContext() calls, a concurrent cancellation thread can:

  1. Call tryCompleteEntry()currentEntry.exit()
  2. Which triggers AsyncEntry.clearEntryContext()asyncContext = null
  3. The second getAsyncContext() now returns null
  4. Reactor's Context.put(key, value) throws NPE when value == null

Solution Quality

Core fix: One-line change using the already-captured sentinelContext variable

.put(SentinelReactorConstants.SENTINEL_CONTEXT_KEY, sentinelContext);  // ✅ Use captured value

Test coverage: The regression test is excellent:

  • Uses reflection to verify internal state before/after the race
  • Deterministically triggers the race via a mock CoreSubscriber.currentContext() that cancels mid-read
  • Asserts both the fix (no NPE) and the invariant (captured context propagates correctly)

Impact

  • Severity: High — affects all reactive Sentinel users under concurrent load
  • Scope: All versions 1.8.6+ (verified 1.8.7, 1.8.8, 1.8.9 all have the bug)
  • Risk: Minimal — changes only the variable reference, no behavior change

Recommendation

Approve and merge immediately. This is a production-critical fix that's been waiting 3 months.

Fixes #3556


cc @sczyh30 @jasonjoo2010 for final approval

@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

LGTM — the fix correctly removes the check-then-act race in SentinelReactorSubscriber.currentContext() by reusing the already-captured non-null async context instead of re-reading currentEntry.getAsyncContext(), closing the window where a concurrent cancel/exit could pass null to Context.put. The added regression test deterministically drives the interleaving and verifies both context propagation and async-context cleanup. Entry/exit pairing and exception-tracing semantics are preserved, the change is Java 8 compatible, and no public API is affected.


Automated review by github-manager-bot

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.

[BUG] springcloud-gateway sentinel 1.8.6版本报空指针异常

5 participants