-
Notifications
You must be signed in to change notification settings - Fork 25.8k
Fix race condition in getXOpaqueId in tests #45447
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
Changes from all commits
6aa716c
f9c0ad3
e214f99
c9f6b9b
f5dd36d
e4a8312
1dbad56
ae2fa64
d2bff75
a4ca5ac
f2e3865
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,6 +39,8 @@ | |
| import java.util.Objects; | ||
| import java.util.Set; | ||
| import java.util.concurrent.CopyOnWriteArraySet; | ||
| import java.util.concurrent.locks.ReadWriteLock; | ||
| import java.util.concurrent.locks.ReentrantReadWriteLock; | ||
| import java.util.regex.Matcher; | ||
| import java.util.regex.Pattern; | ||
|
|
||
|
|
@@ -60,6 +62,14 @@ public class DeprecationLogger { | |
| */ | ||
| private static final CopyOnWriteArraySet<ThreadContext> THREAD_CONTEXT = new CopyOnWriteArraySet<>(); | ||
|
|
||
| /** | ||
| * In order to prevent accessing closed ThreadContext in test environment | ||
| * when iterate <code>THREAD_CONTEXT</code> Set, read lock has to be acquired | ||
| * when ThreadContext is closed it has to be removed from the Set first, and removing should acquire write lock | ||
| * Since adding closed ThreadContext is not supported, this lock do not need to be acquired before adding to the Set. | ||
| */ | ||
| private static ReadWriteLock threadContextLock = new ReentrantReadWriteLock(true); | ||
|
|
||
| /** | ||
| * Set the {@link ThreadContext} used to add deprecation headers to network responses. | ||
| * <p> | ||
|
|
@@ -81,16 +91,18 @@ public static void setThreadContext(ThreadContext threadContext) { | |
| * Remove the {@link ThreadContext} used to add deprecation headers to network responses. | ||
| * <p> | ||
| * This is expected to <em>only</em> be invoked by the {@code Node}'s {@code close} method (therefore once outside of tests). | ||
| * | ||
| * Node: This method should be called before closing a <code>ThreadContext</code>. | ||
| * @see ThreadContext#close() | ||
| * @param threadContext The thread context owned by the {@code ThreadPool} (and implicitly a {@code Node}) | ||
| * @throws IllegalStateException if this {@code threadContext} is unknown (and presumably already unset before) | ||
| */ | ||
| public static void removeThreadContext(ThreadContext threadContext) { | ||
| * @return true if the context was removed, false if the context was now known (possibly already removed) | ||
| **/ | ||
| public static boolean removeThreadContext(ThreadContext threadContext) { | ||
| assert threadContext != null; | ||
|
|
||
| // remove returning false means it did not have it already | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This exception should not be thrown, closing a ThreadPool now also removes ThreadContext from a DeprecationLogger. |
||
| if (THREAD_CONTEXT.remove(threadContext) == false) { | ||
| throw new IllegalStateException("Removing unknown ThreadContext not allowed!"); | ||
| threadContextLock.writeLock().lock(); | ||
| try { | ||
| return THREAD_CONTEXT.remove(threadContext); | ||
| } finally { | ||
| threadContextLock.writeLock().unlock(); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -227,46 +239,54 @@ void deprecated(final Set<ThreadContext> threadContexts, final String message, f | |
| } | ||
|
|
||
| void deprecated(final Set<ThreadContext> threadContexts, final String message, final boolean log, final Object... params) { | ||
| final Iterator<ThreadContext> iterator = threadContexts.iterator(); | ||
| if (iterator.hasNext()) { | ||
| final String formattedMessage = LoggerMessageFormat.format(message, params); | ||
| final String warningHeaderValue = formatWarning(formattedMessage); | ||
| assert WARNING_HEADER_PATTERN.matcher(warningHeaderValue).matches(); | ||
| assert extractWarningValueFromWarningHeader(warningHeaderValue).equals(escapeAndEncode(formattedMessage)); | ||
| while (iterator.hasNext()) { | ||
| try { | ||
| threadContextLock.readLock().lock(); | ||
| try { | ||
| final Iterator<ThreadContext> iterator = threadContexts.iterator(); | ||
| if (iterator.hasNext()) { | ||
| final String formattedMessage = LoggerMessageFormat.format(message, params); | ||
| final String warningHeaderValue = formatWarning(formattedMessage); | ||
| assert WARNING_HEADER_PATTERN.matcher(warningHeaderValue).matches(); | ||
| assert extractWarningValueFromWarningHeader(warningHeaderValue).equals(escapeAndEncode(formattedMessage)); | ||
| while (iterator.hasNext()) { | ||
| final ThreadContext next = iterator.next(); | ||
| next.addResponseHeader("Warning", warningHeaderValue); | ||
| } catch (final IllegalStateException e) { | ||
| // ignored; it should be removed shortly | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (log) { | ||
| AccessController.doPrivileged(new PrivilegedAction<Void>() { | ||
| @SuppressLoggerChecks(reason = "safely delegates to logger") | ||
| @Override | ||
| public Void run() { | ||
| /** | ||
| * There should be only one threadContext (in prod env), @see DeprecationLogger#setThreadContext | ||
| */ | ||
| String opaqueId = getXOpaqueId(threadContexts); | ||
|
|
||
| logger.warn(new DeprecatedMessage(message, opaqueId, params)); | ||
| return null; | ||
| } | ||
| }); | ||
| if (log) { | ||
| AccessController.doPrivileged(new PrivilegedAction<Void>() { | ||
| @SuppressLoggerChecks(reason = "safely delegates to logger") | ||
| @Override | ||
| public Void run() { | ||
| /** | ||
| * There should be only one threadContext (in prod env), @see DeprecationLogger#setThreadContext | ||
| */ | ||
| String opaqueId = getXOpaqueId(threadContexts); | ||
| logger.warn(new DeprecatedMessage(message, opaqueId, params)); | ||
| return null; | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| }finally { | ||
| threadContextLock.readLock().unlock(); | ||
| } | ||
| } | ||
|
|
||
| public String getXOpaqueId(Set<ThreadContext> threadContexts) { | ||
| return threadContexts.stream() | ||
| .filter(t -> t.isClosed() == false) | ||
| .filter(t -> t.getHeader(Task.X_OPAQUE_ID) != null) | ||
| .findFirst() | ||
| .map(t -> t.getHeader(Task.X_OPAQUE_ID)) | ||
| .orElse(""); | ||
| threadContextLock.readLock().lock(); | ||
| try { | ||
| for (ThreadContext threadContext : threadContexts) { | ||
| assert threadContext.isClosed() == false; | ||
| String header = threadContext.getHeader(Task.X_OPAQUE_ID); | ||
| if (header != null) { | ||
| return header; | ||
| } | ||
| } | ||
| return ""; | ||
| } finally { | ||
| threadContextLock.readLock().unlock(); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,7 +42,6 @@ | |
| import java.security.PrivilegedAction; | ||
| import java.security.ProtectionDomain; | ||
| import java.util.Collections; | ||
| import java.util.HashSet; | ||
| import java.util.List; | ||
| import java.util.Locale; | ||
| import java.util.Map; | ||
|
|
@@ -197,19 +196,6 @@ public void testCanRemoveThreadContext() throws IOException { | |
| } | ||
| } | ||
|
|
||
| public void testIgnoresClosedThreadContext() throws IOException { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. removing as we don't expect closed contexts to be present in DeprecatedLogger. It should be removed before closing |
||
| ThreadContext threadContext = new ThreadContext(Settings.EMPTY); | ||
| Set<ThreadContext> threadContexts = new HashSet<>(1); | ||
|
|
||
| threadContexts.add(threadContext); | ||
|
|
||
| threadContext.close(); | ||
|
|
||
| logger.deprecated(threadContexts, "Ignored logger message"); | ||
|
|
||
| assertTrue(threadContexts.contains(threadContext)); | ||
| } | ||
|
|
||
| public void testSafeWithoutThreadContext() { | ||
| logger.deprecated(Collections.emptySet(), "Ignored"); | ||
| } | ||
|
|
@@ -231,9 +217,9 @@ public void testFailsWhenDoubleSettingSameThreadContext() throws IOException { | |
| } | ||
| } | ||
|
|
||
| public void testFailsWhenRemovingUnknownThreadContext() throws IOException { | ||
| public void testDoNotFailWhenRemovingUnknownThreadContext() throws IOException { | ||
| try (ThreadContext threadContext = new ThreadContext(Settings.EMPTY)) { | ||
| expectThrows(IllegalStateException.class, () -> DeprecationLogger.removeThreadContext(threadContext)); | ||
| assertFalse(DeprecationLogger.removeThreadContext(threadContext)); | ||
| } | ||
| } | ||
|
|
||
|
|
||
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.
a client was created with the same
threadPoolthat is terminated a line below.a client terminates a threadPool as well in NoOpClient.close() so this is possibly not needed
It might as well stay, but I think it is unecessary