Skip to content
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

Add TraceAbleRequestContextStorage for ease of detect context leaks #4232

Merged
merged 46 commits into from Sep 5, 2022
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
1d64827
Add TraceAbleRequestContextStorage for ease of detect context leak
klurpicolo Apr 26, 2022
c79b852
Update classname
klurpicolo Apr 28, 2022
443e740
Add Sampler to constructor of LeakTracingRequestContextStorage
klurpicolo Apr 28, 2022
c431103
Add benchmark on LeakTracingRequestContextStorage
klurpicolo May 1, 2022
06c728e
Add finally when pop
klurpicolo May 5, 2022
0f5e5e5
Change generic of Sampler
klurpicolo May 5, 2022
bcf92df
Merge branch 'master' into context-leak-debug
ikhoon May 13, 2022
82bb1a8
Add UnstableApi and check null input
klurpicolo May 13, 2022
a7fd563
Refactor DeferredClose
klurpicolo May 13, 2022
ea2e675
Add current stacktrace to exception
klurpicolo May 14, 2022
cb8dcf4
Use await instead of CountDownLatch in some tests
klurpicolo May 14, 2022
50d4127
Fix lint
klurpicolo May 14, 2022
818c166
Fix test
klurpicolo May 14, 2022
5207a4c
Provide a flag for enable requestContextLeakDetection
klurpicolo May 24, 2022
d24e0a2
Move LeakDetectionConfiguration package outside of internal
klurpicolo May 24, 2022
61e767d
Replace LeakDetectionConfiguration with Sampler<? super RequestContext>
klurpicolo Jun 1, 2022
449f051
Change flag name from requestContextLeakDetection to requestContextLe…
klurpicolo Jun 1, 2022
4dabd01
Refactor RequestContextUtil
klurpicolo Jun 1, 2022
8909ace
Correct Java doc grammar
klurpicolo Jun 1, 2022
fdd8490
Add newline to EOF
klurpicolo Jun 1, 2022
5562cfe
Correct comment and add UnstableApi
klurpicolo Jun 2, 2022
f124fc8
Update generic of Sampler
klurpicolo Jun 3, 2022
a71a725
Move LeakTracingRequestContextStorage to internal.common
klurpicolo Jun 3, 2022
c4a534f
Correct spelling
klurpicolo Jun 3, 2022
3b00c2a
Modifying Samplers.of(String) to accept true and false
klurpicolo Jun 12, 2022
af31a6e
Add illegallyPushServiceRequestContext test
klurpicolo Jun 18, 2022
ab4de84
Refactor LeakTracingRequestContextStorage
klurpicolo Jul 1, 2022
9f681d4
Merge branch 'master' into context-leak-debug
klurpicolo Aug 12, 2022
a72a9ca
Apply unwrapAll()
klurpicolo Aug 12, 2022
d43b052
Update core/src/main/java/com/linecorp/armeria/internal/common/LeakTr…
ikhoon Aug 16, 2022
4f8a8f2
Use unwrap for root checking
klurpicolo Aug 25, 2022
6cbcfab
Override unwrap method
klurpicolo Aug 25, 2022
89ddc46
Add space on comment
klurpicolo Aug 25, 2022
6b4b9f9
Correct happen-before relationship
klurpicolo Aug 25, 2022
083d560
Add check latch to wait for another thread
klurpicolo Aug 25, 2022
4fb9575
Ensure multiThreadContextLeak executor2 summit
klurpicolo Aug 28, 2022
7b3f3d1
Use StackTraceElement
klurpicolo Aug 28, 2022
9d929bf
Fix case root is null
klurpicolo Aug 29, 2022
90e6e78
Dedup toString
klurpicolo Aug 29, 2022
a0540c8
Use gradle task to copy test from core
klurpicolo Aug 29, 2022
9156898
Fix lint
klurpicolo Aug 29, 2022
50384a2
Refactor equalUnwrapAllNullable
klurpicolo Aug 30, 2022
0fe21fd
Remove thread name from stacktrace
klurpicolo Sep 4, 2022
caba3d6
Refactor equalUnwrapAllNullable
klurpicolo Sep 4, 2022
1302481
Add guard clause
klurpicolo Sep 4, 2022
9e8e10e
Correct spelling and remove UnstableApi
klurpicolo Sep 5, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,91 @@
/*
* Copyright 2022 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

package com.linecorp.armeria.common;

import org.openjdk.jmh.annotations.Benchmark;

import com.linecorp.armeria.common.util.Sampler;
import com.linecorp.armeria.server.ServiceRequestContext;

/**
* Microbenchmarks for LeakTracingRequestContextStorage.
*/
public class LeakTracingRequestContextStorageBenchmark {

private static final RequestContextStorage threadLocalReqCtxStorage =
RequestContextStorage.threadLocal();
private static final RequestContextStorage neverSample =
new LeakTracingRequestContextStorage(threadLocalReqCtxStorage, Sampler.never());
private static final RequestContextStorage rateLimited1 =
new LeakTracingRequestContextStorage(threadLocalReqCtxStorage, Sampler.of("rate-limited=1"));
private static final RequestContextStorage rateLimited10 =
new LeakTracingRequestContextStorage(threadLocalReqCtxStorage, Sampler.of("rate-limited=10"));
private static final RequestContextStorage random1 =
new LeakTracingRequestContextStorage(threadLocalReqCtxStorage, Sampler.of("random=0.01"));
private static final RequestContextStorage random10 =
new LeakTracingRequestContextStorage(threadLocalReqCtxStorage, Sampler.of("random=0.10"));
private static final RequestContextStorage alwaysSample =
new LeakTracingRequestContextStorage(threadLocalReqCtxStorage, Sampler.always());
private static final RequestContext reqCtx = newCtx("/");

private static ServiceRequestContext newCtx(String path) {
return ServiceRequestContext.builder(HttpRequest.of(HttpMethod.GET, path))
.build();
}

@Benchmark
public void baseline_threadLocal() {
final RequestContext oldCtx = threadLocalReqCtxStorage.push(reqCtx);
threadLocalReqCtxStorage.pop(reqCtx, oldCtx);
}

@Benchmark
public void leakTracing_never_sample() {
final RequestContext oldCtx = neverSample.push(reqCtx);
neverSample.pop(reqCtx, oldCtx);
}

@Benchmark
public void leakTracing_rateLimited_1() {
final RequestContext oldCtx = rateLimited1.push(reqCtx);
rateLimited1.pop(reqCtx, oldCtx);
}

@Benchmark
public void leakTracing_rateLimited_10() {
final RequestContext oldCtx = rateLimited10.push(reqCtx);
rateLimited10.pop(reqCtx, oldCtx);
}

@Benchmark
public void leakTracing_random_1() {
final RequestContext oldCtx = random1.push(reqCtx);
random1.pop(reqCtx, oldCtx);
}

@Benchmark
public void leakTracing_random_10() {
final RequestContext oldCtx = random10.push(reqCtx);
random10.pop(reqCtx, oldCtx);
}

@Benchmark
public void leakTracing_always_sample() {
final RequestContext oldCtx = alwaysSample.push(reqCtx);
alwaysSample.pop(reqCtx, oldCtx);
}
}
@@ -0,0 +1,113 @@
/*
* Copyright 2022 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

package com.linecorp.armeria.common;

import static java.lang.Thread.currentThread;
import static java.util.Objects.requireNonNull;

import com.linecorp.armeria.client.ClientRequestContext;
import com.linecorp.armeria.common.annotation.Nullable;
import com.linecorp.armeria.common.util.Sampler;
import com.linecorp.armeria.server.ServiceRequestContext;

import io.netty.util.concurrent.FastThreadLocal;

/**
* A {@link RequestContextStorage} which keeps track of {@link RequestContext}s, reporting pushed thread
* information if a {@link RequestContext} is leaked.
*/
public final class LeakTracingRequestContextStorage implements RequestContextStorage {
ikhoon marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: Is there any reason this class can't be under internal.common? (so that this class wouldn't need to be public)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Previously, I didn't add Flag requestContextLeakDetectionSampler for enabling this feature. I intentionally make LeakTracingRequestContextStorage public, so that users can use it to supply via RequestContextStorageProvider SPI. Since we already added the flag and RequestContextStorageProvider SPI will be removed as #4211, I'll move to it internal.common. Thank you.


private final RequestContextStorage delegate;
private final FastThreadLocal<PendingRequestContextStackTrace> pendingRequestCtx;
private final Sampler<Object> sampler;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
private final Sampler<Object> sampler;
private final Sampler<? super RequestContext> sampler;


/**
* Creates a new instance.
* @param delegate the underlying {@link RequestContextStorage} that stores {@link RequestContext}
*/
public LeakTracingRequestContextStorage(RequestContextStorage delegate) {
this(delegate, Flags.verboseExceptionSampler());
Copy link
Contributor

Choose a reason for hiding this comment

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

Now, we can use Flags.requestContextLeakDetectionSampler() as the default value.

Suggested change
this(delegate, Flags.verboseExceptionSampler());
this(delegate, Flags.requestContextLeakDetectionSampler());

}

/**
* Creates a new instance.
* @param delegate the underlying {@link RequestContextStorage} that stores {@link RequestContext}
* @param sampler the {@link Sampler} that determines whether to retain the stacktrace of the context leaks
*/
public LeakTracingRequestContextStorage(RequestContextStorage delegate,
Sampler<?> sampler) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
Sampler<?> sampler) {
Sampler<? super RequestContext> sampler) {

this.delegate = delegate;
ikhoon marked this conversation as resolved.
Show resolved Hide resolved
pendingRequestCtx = new FastThreadLocal<>();
this.sampler = (Sampler<Object>) sampler;
}

@Nullable
@Override
public <T extends RequestContext> T push(RequestContext toPush) {
requireNonNull(toPush, "toPush");
Copy link
Contributor

@ikhoon ikhoon Jun 2, 2022

Choose a reason for hiding this comment

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

Should we just call delegate.push(...) if sampler == Sampler.never()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think sampler member of LeakTracingRequestContextStorage cannot be Sampler.never() because we already have the condition sampler == Sampler.never() in RequestContextUtil

if (sampler == Sampler.never()) {
        requestContextStorage = provider.newStorage();
    } else {
        requestContextStorage = new LeakTracingRequestContextStorage(provider.newStorage(), sampler);
    }

Also, I'll move LeakTracingRequestContextStorage to internal.common as @jrhee17 mentioned.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see. If it is moved to internal, we don't have to double-check it.


final RequestContext prevContext = delegate.currentOrNull();
if (prevContext != null) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm a little concerned that this validation logic is:

  1. In some parts duplicated with the following code points:
  1. At the same time, there are some corner cases where this filter isn't comprehensive.

e.g.

final ServiceRequestContext ctx = newCtx("/1");
try (SafeCloseable ignored = ctx.push()) {
    final ClientRequestContext ctx2 = clientRequestContext();
    @SuppressWarnings("MustBeClosedChecker")
    final SafeCloseable notPopped = ctx2.push();
    final ClientRequestContext ctx3 = clientRequestContext();
    try (SafeCloseable ignored2 = ctx3.push()) {
        // stacktrace for ctx2.push won't be printed
    }
}

I guess this PR is a good start, but I'm wondering if it would be more robust to "attach" the caller thread/stacktrace to the RequestContext when pushing, and retrieving this information if the RequestContext ends up in an invalid state.

Copy link
Contributor

Choose a reason for hiding this comment

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

cc. other maintainers @ikhoon @minwoox @trustin

Copy link
Member

Choose a reason for hiding this comment

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

  • Re: Duplication - How about deduplicating in a follow-up PR?
  • Re: Related contexts are not recorded - How about storing whether the current context is sampled? For example, we could always record when prevContext or its root is sampled, just like how distributed tracing is propagated to its subspans.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

At the same time, there are some corner cases where this filter isn't comprehensive

I see. Thanks for pointing this out 🙏.

Re: Related contexts are not recorded - How about storing whether the current context is sampled? For example, we could always record when prevContext or its root is sampled, just like how distributed tracing is propagated to its subspans

Hi @trustin, Could help elaborate on this option? thanks.

Another option that I can think of is to store a stack of stack traces. Actually, @minwoox mentioned in the issue that stack is needed because context can be pushed multiple times before getting popped.

Copy link
Member

Choose a reason for hiding this comment

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

Actually, @minwoox mentioned in the issue that stack is needed because context can be pushed multiple times before getting popped.

Yeah, I think we need a stack to store stack traces. A request context could be pushed multiple times before it's popped, so the stack is the right data structure for this case.
We might be able to do it something like:

private final FastThreadLocal<Deque<ContextAndStackTrace>> threadLocalStack = new FastThreadLocal<>();

@Nullable
@Override
public <T extends RequestContext> T push(RequestContext toPush) {
    ...

    final RequestContext prevContext = delegate.currentOrNull();
    if (prevContext != null) {
        if (...) {
            // valid
        } else {
            throw newIllegalContextPushingException(toPush, prevContext, threadLocalStack.get());
        }
    }
    
    ...
    threadLocalStack.get().push(new ContextAndStackTrace(toPush, currentThread().getStackTrace()));
    ...
}

@Override
public void pop(RequestContext current, @Nullable RequestContext toRestore) {
    final ContextAndStackTrace last = threadLocalStack.get().peekLast();
    if (last == null || last.ctx != current) {
        // exception!
    }
    threadLocalStack.get().pop();
    delegate.pop(current, toRestore);
}

private static class ContextAndStackTrace {

    private final RequestContext ctx;
    private final StackTraceElement[] stackTrace;

    ContextAndStackTrace(RequestContext ctx, StackTraceElement[] stackTrace) {
        this.ctx = ctx;
        this.stackTrace = stackTrace;
    }
}

Hi @trustin, Could help elaborate on this option? thanks.

Let's suppose this situation:

a.push()
  b.push() // b.root() == a
    c.push() // c.root() == a
a.pop() // exception! because b and c are not popped.

Because b and c are not popped we need to provide the stack traces of those contexts. However, if b is sampled and c isn't sampled we cannot provide them.
In order to solve this situation, I think we need to make a decision to sample or not only when the root of the context is pushed so that we can have all stack traces of the context flow. 😉

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I got it, Thanks for the explanation 🙏

if (prevContext == toPush) {
// Re-entrance
} else if (toPush instanceof ServiceRequestContext &&
prevContext.root() == toPush) {
// The delegate has the ServiceRequestContext whose root() is toPush
} else if (toPush instanceof ClientRequestContext &&
prevContext.root() == toPush.root()) {
// The delegate has the ClientRequestContext whose root() is the same toPush.root()
} else {
throw pendingRequestCtx.get();
}
}

if (sampler.isSampled(PendingRequestContextStackTrace.class)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (sampler.isSampled(PendingRequestContextStackTrace.class)) {
if (sampler.isSampled(RequestContext.class)) {

pendingRequestCtx.set(new PendingRequestContextStackTrace(toPush, true));
} else {
pendingRequestCtx.set(new PendingRequestContextStackTrace(toPush, false));
}

return delegate.push(toPush);
}

@Override
public void pop(RequestContext current, @Nullable RequestContext toRestore) {
try {
Copy link
Contributor

@ikhoon ikhoon Jun 2, 2022

Choose a reason for hiding this comment

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

Ditto. Should we just call delegate.pop(...) if sampler == Sampler.never()?

delegate.pop(current, toRestore);
} finally {
pendingRequestCtx.remove();
}
}

@Nullable
@Override
public <T extends RequestContext> T currentOrNull() {
return delegate.currentOrNull();
}

static class PendingRequestContextStackTrace extends RuntimeException {

private static final long serialVersionUID = -689451606253441556L;

PendingRequestContextStackTrace(RequestContext context, boolean isSample) {
super("At thread [" + currentThread().getName() + "], previous RequestContext didn't popped : " +
context + (isSample ? ", It is pushed at the following stacktrace" : ""), null,
true, isSample);
ikhoon marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
@@ -0,0 +1,25 @@
/*
* Copyright 2022 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

package com.linecorp.armeria.common;

public final class LeakTracingRequestContextStorageProvider implements RequestContextStorageProvider {

@Override
public RequestContextStorage newStorage() {
return new LeakTracingRequestContextStorage(RequestContextStorage.threadLocal());
}
}