armeria
armeria copied to clipboard
Support micrometer context-propagation
Motivation:
- Related Issue : https://github.com/line/armeria/issues/5145
-
Armeria
already support context-propagation to maintainRequestContext
during executing Reactor code. How it requires maintenance. -
Reactor
integratemicro-meter:context-propagation
to do context-propagation duringFlux
,Mono
officially. thus, it would be better to migrate fromRequestContextHook
toRequestContextPropagationHooks
because it can reduce maintenance cost.
Modifications:
- Add new
Hook
forReactor
. - Add new
ThreadLocalAccessor
formicro-meter:context-propagation
to mainRequestContext
during executing Reactor code likeMono
,Flux
. - Add new config
enableContextPropagation
to integratemicro-meter:context-propagation
withspring-boot3
.
Result:
- Closes https://github.com/line/armeria/issues/5145
- If user want to use
micrometer:context-propagation
to maintainRequestContext
during executing Reactor code likeMono
,Flux
, just callRequestContextPropagationHook.enable()
.
FYI, flow of micro-meter/context-propagation
.
- If we enable
context-propagation
, hook forcontext-propagation
will be addedReactor
's hooks.
- if
subscribe()
are called, hook likecaptureThreadLocals()
are called.
- and then,
ContextSnapshot
are created to propagate context. in this time,accessor
are used to capture state ofThreadLocals
.
- When
Mono
orFlux
are executed inScheduler
's executor, scope are gotten by callingsetThreadLocals()
.Scope
instance are returned, and it is concrete class ofAutoClosable
.
- As you can see,
Context-Propagation
is restore thread local state at end oftry ~ resource
.
- On the other hand,
setThreadLocals()
be about to restoreThreadlocal
state fromContextSnapshot
beforerunnable.run()
.
IMHO, it is very similar to RequestContextHook
on armeria.
🔍 Build Scan® (commit: 1e6cd9bb6e3c6d703b87a2ed592b8fd31cccd820)
Job name | Status | Build Scan® |
---|---|---|
build-windows-latest-jdk-21 | ✅ | https://ge.armeria.dev/s/gjvoef5prmmle |
build-self-hosted-unsafe-jdk-8 | ✅ | https://ge.armeria.dev/s/s4emdzuw7owuy |
build-self-hosted-unsafe-jdk-21-snapshot-blockhound | ✅ | https://ge.armeria.dev/s/f2soiblmtmso4 |
build-self-hosted-unsafe-jdk-17-min-java-17-coverage | ✅ | https://ge.armeria.dev/s/7yczrfhcqrtmk |
build-self-hosted-unsafe-jdk-17-min-java-11 | ✅ | https://ge.armeria.dev/s/seg3ttdpzg5bg |
build-self-hosted-unsafe-jdk-17-leak | ✅ | https://ge.armeria.dev/s/iagbkvjyvpos4 |
build-self-hosted-unsafe-jdk-11 | ✅ | https://ge.armeria.dev/s/xwsu3uwjxg6as |
build-macos-12-jdk-21 | ✅ | https://ge.armeria.dev/s/64pzf6lau2udo |
I investigate that internal of Context-Propagation
.
Case 1. publisher.subscribe(...)
. downstream -> upstream flow
- When
publisher.subscribe(...)
are called andpublisher
is instance ofContextWriteRestoringThreadLocals
, it createsContextSnapshot.Scope
.in this sequence, read the Key-Value stored inReactorContext
, then use theThreadLocalAccessor
instance to store the value fromReactorContext
intoThreadLocal
. Additionally, keep the previously stored values inThreadLocal
asPreviousValues
, and use this values to revert the state ofThreadLocal
to its previous state when theContextSnapshot.Scope
ends. (FYI,ContextSnapshot.Scope
implementAutoClosable
).
Case 2. subscriber.onSubscribe(...)
. upstream -> downstream flow
- When
subscriber.onSubscribe(...)
are called andsubscriber
is instance ofContextWriteRestoringThreadLocal
, it createsContextSnapshot.Scope
as well. it means that the values inReactorContext
will be stored toThreadLocal
at the start ofsubscriber.onSubscribe()
andThreadLocal
will be revert to its previous state at the end of the function.
Case 3. subscription.request(...)
. downstream -> upstream flow
- When
subscription.request(...) are called and
subscriptionis instance of
ContextWriteRestoringThreadLocal, it creates
ContextSnapshot.scope` as well.
ContextWriteRestoringThreadLocals
operator are integrated when both [Mono|Flux]#contextCapture()
and [Mono|Flux]#contextWrite()
are called. It means that Reactor Context
are essential to propagate RequestContext
to each ThreadLocal
during Reactor Operations
.
In practice, it works like this:
- Reads all values accessible by the
KEY
ofThreadLocalAccessor
in theReactor Context
. - Stores the read values in the
Threadlocal
by usingThreadLocalAccessor
. At this time, the values that previously existed in theThreadllocal
are maintained in a Map calledPreviousValues
. - When the
Scope
ends, it restores thePreviousValues
back to theThreadLocal
.
It means that Reactor Context
for writing, ThreadLocals
for reading.
@trustin nim, There are also major differences in the test. I would like to inform you about them.
The test utility function addCallbacks()
should be change.
private static <T> Mono<T> addCallbacks(Mono<T> mono, ClientRequestContext ctx) {
return mono.doFirst(() -> assertThat(ctxExists(ctx)).isTrue())
.doOnSubscribe(s -> assertThat(ctxExists(ctx)).isTrue())
.doOnRequest(l -> assertThat(ctxExists(ctx)).isTrue())
.doOnNext(foo -> assertThat(ctxExists(ctx)).isTrue())
.doOnSuccess(t -> assertThat(ctxExists(ctx)).isTrue())
.doOnEach(s -> assertThat(ctxExists(ctx)).isTrue())
.doOnError(t -> assertThat(ctxExists(ctx)).isTrue())
.doAfterTerminate(() -> assertThat(ctxExists(ctx)).isTrue())
// I added contextWrite(...)
.contextWrite(Context.of(RequestContextAccessor.getInstance().key(), ctx));
// doOnCancel and doFinally do not have context because we cannot add a hook to the cancel.
}
contextWrite(Context.of(RequestContextAccessor.getInstance().key(), ctx));
are added. As we know, micro-meter:context-propagation
require Reactor Context
to propagate context during reactor operations. thus, i added this method.
// Before : StepVerifier.create(mono1)
// After : Add initiali Reactor Context to StepVerifier.
StepVerifier.create(mono1, initialReactorContext(ctx))
.expectSubscriptionMatches(s -> ctxExists(ctx))
.expectNextMatches(s -> ctxExists(ctx) && "baz".equals(s))
.verifyComplete();
In previous test code, StepVerifier.create(mono1)
is enough. however, it is not enough to micro-meter:context-propagation
.
StepVerifier
create DefaultVerifySubscriber
to subscribe Flux|Mono
and valid result is correct. however, DefaultVerifySubscriber
has only empty Reactor Context
. it means that .expectSubscriptionMatches(s -> ctxExists(ctx))
should be failed.
micro-meter:context-propagation
is used to read the values from the Reactor Context
and restore the state of ThreadLocal
. however, since the DefaultStepVerifierSubscriber
has an Empty Reactor Context
, the RequestContext
stored in ThreadLocal
will become Null.
Thus, initial Reactor Context should be include to StepVerifier
as well.
Could you fix the build failures before getting reviews?
@minwoox IIRC you had some comments related with request context hooks you wanted @chickenchickenlove to address. Could you leave some comment about that?
Thanks! I left my opinion here: https://github.com/line/armeria/pull/5577#discussion_r1593720102
Codecov Report
All modified and coverable lines are covered by tests :white_check_mark:
Project coverage is 74.05%. Comparing base (
14c5566
) to head (e80117c
). Report is 69 commits behind head on main.
:exclamation: Current head e80117c differs from pull request most recent head 1e6cd9b
Please upload reports for the commit 1e6cd9b to get more accurate results.
Additional details and impacted files
@@ Coverage Diff @@
## main #5577 +/- ##
============================================
- Coverage 74.05% 74.05% -0.01%
+ Complexity 21253 21242 -11
============================================
Files 1850 1850
Lines 78600 78540 -60
Branches 10032 10020 -12
============================================
- Hits 58209 58164 -45
+ Misses 15686 15680 -6
+ Partials 4705 4696 -9
:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.
Thanks trustin nim. Thank you for taking the time to review this. 🙇♂️
Hi @ikhoon nim, orry to bother you. When you have time, could you take a look this PR? Thanks in advanced 🙇♂️