-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement MdcExecutor to manage MDC context for query execution #15072
base: master
Are you sure you want to change the base?
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #15072 +/- ##
============================================
+ Coverage 61.75% 63.42% +1.67%
- Complexity 207 1484 +1277
============================================
Files 2436 2751 +315
Lines 133233 154546 +21313
Branches 20636 23818 +3182
============================================
+ Hits 82274 98028 +15754
- Misses 44911 49124 +4213
- Partials 6048 7394 +1346
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
|
||
@Override | ||
protected void registerOnMDC() { | ||
queryRequest.registerOnMdc(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why go through queryRequest
to register on MDC ? Instead a call can be made directly to LoggerConstants.QUERY_ID_KEY
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To keep MDC management clean and centralized.
_executorService = executorService; | ||
} | ||
|
||
protected abstract boolean alreadyRegistered(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At least with the current implementation, this fn. does not have to be abstract ? Both the implementations are the same.
* TODO: Convert this class and its usages into an Executor instead of an ExecutorService | ||
* | ||
*/ | ||
public abstract class MdcExecutor implements ExecutorService { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the current scope, this class is only used for query execution threads. So this can be less generic. It can be named MdcQueryExecutor
. The class can also take in a context then ? The root of my questions are because the following code pattern seems overkill in the current scope.
MdcExecutor mdcExecutor = new MdcExecutor(executorService) {
@Override
protected boolean alreadyRegistered() {
return LoggerConstants.QUERY_ID_KEY.isRegistered();
}
@Override
protected void registerOnMDC() {
executionContext.registerOnMDC();
}
@Override
protected void unregisterFromMDC() {
executionContext.unregisterFromMDC();
}
};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The first point makes sense. The fact that only affects queries is implicit given it is in the query runtime package, but we can change the implementation to receive a listener to customize the stuff we want to add to MDC.
But I don't understand the second part. There are two implementations in this PR. One uses OpChainExecutionContext and the other uses ServerQueryRequest
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I get the code structure now. I hadn't grokked the Decorator executor and how every task is decorated.
A follow up (out-of-band) discussion is the effort required to allow multiple decorators using this pattern. e.g. OOM protection.
@@ -40,6 +40,7 @@ | |||
import org.apache.pinot.common.utils.DataSchema; | |||
import org.apache.pinot.common.utils.config.QueryOptionsUtils; | |||
import org.apache.pinot.common.utils.request.RequestUtils; | |||
import org.apache.pinot.spi.executor.MdcExecutor; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to be re-arranged, it's currently causing the spotless plugin check to fail.
@@ -140,8 +142,25 @@ public synchronized void shutDown() { | |||
@Override | |||
public InstanceResponseBlock execute(ServerQueryRequest queryRequest, ExecutorService executorService, | |||
@Nullable ResultsBlockStreamer streamer) { | |||
MdcExecutor mdcExecutor = new MdcExecutor(executorService) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we really need to allocate an instance for every query request? Can't we use a common ThreadPoolExecutor
implementation with an overridden afterExecute
that clears the MDC context instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I just realized what you're aiming to do with the decorator pattern here - all the other alternatives would probably involve significantly more changes to make sure that the right executor is being created and used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it is easier and safer to do it this way. This allocation is super cheap, given we just decorate an actual executor and the decorator has almost no state
See | ||
https://logging.apache.org/log4j/2.x/manual/pattern-layout.html#converter-not-empty | ||
and https://logging.apache.org/log4j/2.x/manual/pattern-layout.html#converter-thread-context-map | ||
--> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's pinot.component.type
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is another contextual variable I'm using in the PR where I'm improving errors. There I'm using that to say whether the log comes from a server, controller or broker, which is very useful in Quickstarts where all logs are merged. But shouldn't be used here. I'm removing it.
registerOnMdcIfNotSet(value, false); | ||
} | ||
|
||
public boolean registerOnMdcIfNotSet(String value, boolean override) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What situations would we want to override the MDC value in? Currently we're not overriding in any case right? And we're simply logging if we see some MDC value already registered unexpectedly (potentially from missed cleanup earlier)? Shouldn't we override it in that case instead of using the older stale value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even if the older value is the same as the value we want to register (in case we already set it at some previous point in the thread's execution) it doesn't hurt to override the value right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It hurts because if we don't override the values, we can see where the original came from. Anyway, we can discuss that in another PR.
I've just modified the code to have:
String registerInMdc(String value)
, which always overrideString registerInMdc(String value, boolean override)
, which overrides depending on the paramenterString registerInMdcIfNotExist(String value)
, which never overrides
All three methods always return the older value (or null).
protected abstract <T> Callable<T> decorate(Callable<T> task); | ||
|
||
protected abstract Runnable decorate(Runnable task); | ||
|
||
protected <T> Collection<? extends Callable<T>> decorateTasks(Collection<? extends Callable<T>> tasks) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: some Javadocs could be useful here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added
_executorService.execute(decorate(command)); | ||
} | ||
|
||
public static abstract class BeforeAfter extends DecoratorExecutorService { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is unused? Is it just mean to be an example extensible implementation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it is unused, but it can help future implementations and is a good example for other custom implementations as well.
I've changed some interfaces and improved javadocs as requested. I hope it will be easier to read now |
This simplification of #14994 is focused on adding MDC support on the query side. MDC is a common feature in logging frameworks that is usually build on top of thread local. The idea is that sometimes logs should include some contextual information we don't need on the rest of the code. A very clear example on Pinot is the request id. It may be useful to include that id on all the logs related to a query but we don't want to add the request id attribute/parameter everywhere just in case some part of the code will need it to create a log.
One important part of using MDC is to actually be sure that the variables are cleaned up once the thread finished the request. Therefore I had to add some try-finally blocks in the code. I strongly recommend to review this PR with the ignore white changes option.
This PR modifies the log4j2.xml file used on quickstarts to actually use these new MDC values. Actual deployments will likely use their log4j2.xml files, and they will need to be modified to log request IDs and stage IDs. Please remember that this is optional. Logs are still as helpful as before, even if MDC properties are unused. Specifically, logs that already included the request ID or stage ID haven't been changed.
It is important to know that in MSE we use more than one request id. Specifically, leaf stages change the request id used in the SSE part of the query. This is done in ServerPlanRequestUtils and it a controversial feature we may decide to change in the future. At logging level, it is not useful to have different request ids for the same query. This is why the request id set in MDC is shared between MSE and SSE parts. This will be easier to understand with some examples:
Max rows limit in hash join. Here, we can see two new values between brackets:
qid
andstg
. The first one is the query id (similar to request id) and the second is the stage id. The worker id is also included in MDC, but the proposed log4j2.xml is not using it.A log in SSE code when being called by MSE. Here we can see we have two different request ids in the same log. The one called
qid
in square brackets (close tostg
) is the one used in MSE, while the other (included in the normal message and calledrequestId
) is the fake SSE id created on ServerPlanRequestUtilsSame log, when called from SSE. Here the
qid
andrequestId
is the same both places andstg
is not set