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 1 commit
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,94 @@
/*
* 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.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 TraceAbleRequestContextStorage implements RequestContextStorage {
Copy link
Contributor

Choose a reason for hiding this comment

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

How about?

Suggested change
public final class TraceAbleRequestContextStorage implements RequestContextStorage {
public final class LeakTracingRequestContextStorage implements RequestContextStorage {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's a good name.


private final RequestContextStorage delegate;
private final FastThreadLocal<PendingRequestContextStackTrace> pendingRequestCtx;

/**
* Creates a new instance.
* @param delegate the underlying {@link RequestContextStorage} that stores {@link RequestContext}
*/
public TraceAbleRequestContextStorage(RequestContextStorage delegate) {
this.delegate = delegate;
pendingRequestCtx = new FastThreadLocal<>();
}

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

final RequestContext prevContext = delegate.currentOrNull();
if (prevContext != null) {
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();
}
}
pendingRequestCtx.set(new PendingRequestContextStackTrace(toPush));
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we take a Sampler as a constructor parameter so that users can customize the sampling rate?
If not specified, I prefer to follow DefaultFlagsProvider#VERBOSE_EXCEPTION_SAMPLER_SPEC as the sensible default value.

Copy link
Contributor

Choose a reason for hiding this comment

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

You might want to refer to

if (Flags.verboseExceptionSampler().isSampled(getClass())) {
super.fillInStackTrace();
}

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 agree, Thanks.🙏


return delegate.push(toPush);
}

@Override
public void pop(RequestContext current, @Nullable RequestContext toRestore) {
delegate.pop(current, toRestore);
pendingRequestCtx.remove();
}

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

private static class PendingRequestContextStackTrace extends IllegalStateException {

private static final long serialVersionUID = -689451606253441556L;

final RequestContext context;

PendingRequestContextStackTrace(RequestContext context) {
super("At thread [" + currentThread().getName() + "], previous RequestContext didn't popped : " +
context + ", It is pushed at the following stacktrace");
this.context = context;
}
}
}
@@ -0,0 +1,210 @@
/*
* Copyright 2019 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 org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.util.function.Function;

import org.junit.jupiter.api.Test;

import com.google.common.collect.ImmutableList;

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

/**
* This is copy from ServiceRequestContextTest on core module to test behaviour consistency.
*/
class ServiceRequestContextTest {

@Test
void current() {
assertThatThrownBy(ServiceRequestContext::current).isInstanceOf(IllegalStateException.class)
.hasMessageContaining("unavailable");

final ServiceRequestContext sctx = serviceRequestContext();
try (SafeCloseable unused = sctx.push()) {
assertThat(ServiceRequestContext.current()).isSameAs(sctx);
final ClientRequestContext cctx = clientRequestContext();
try (SafeCloseable unused1 = cctx.push()) {
assertThat(ServiceRequestContext.current()).isSameAs(sctx);
assertThat(ClientRequestContext.current()).isSameAs(cctx);
assertThat((ClientRequestContext) RequestContext.current()).isSameAs(cctx);
}
assertCurrentCtx(sctx);
}
assertCurrentCtx(null);

try (SafeCloseable unused = clientRequestContext().push()) {
assertThatThrownBy(ServiceRequestContext::current)
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("not a server-side context");
}
}

@Test
void currentOrNull() {
assertThat(ServiceRequestContext.currentOrNull()).isNull();

final ServiceRequestContext sctx = serviceRequestContext();
try (SafeCloseable unused = sctx.push()) {
assertThat(ServiceRequestContext.currentOrNull()).isSameAs(sctx);
final ClientRequestContext cctx = clientRequestContext();
try (SafeCloseable unused1 = cctx.push()) {
assertThat(ServiceRequestContext.currentOrNull()).isSameAs(sctx);
assertThat(ClientRequestContext.current()).isSameAs(cctx);
assertThat((ClientRequestContext) RequestContext.current()).isSameAs(cctx);
}
assertCurrentCtx(sctx);
}
assertCurrentCtx(null);

try (SafeCloseable unused = clientRequestContext().push()) {
assertThat(ServiceRequestContext.currentOrNull()).isNull();
}
}

@Test
void mapCurrent() {
assertThat(ServiceRequestContext.mapCurrent(ctx -> "foo", () -> "defaultValue"))
.isEqualTo("defaultValue");
assertThat(ServiceRequestContext.mapCurrent(Function.identity(), null)).isNull();

final ServiceRequestContext sctx = serviceRequestContext();
try (SafeCloseable unused = sctx.push()) {
assertThat(ServiceRequestContext.mapCurrent(c -> c == sctx ? "foo" : "bar",
() -> "defaultValue"))
.isEqualTo("foo");
assertThat(ServiceRequestContext.mapCurrent(Function.identity(), null)).isSameAs(sctx);
final ClientRequestContext cctx = clientRequestContext();
try (SafeCloseable unused1 = cctx.push()) {
assertThat(ServiceRequestContext.mapCurrent(c -> c == sctx ? "foo" : "bar",
() -> "defaultValue"))
.isEqualTo("foo");
assertThat(ClientRequestContext.mapCurrent(c -> c == cctx ? "baz" : "qux",
() -> "defaultValue"))
.isEqualTo("baz");
assertThat(ServiceRequestContext.mapCurrent(Function.identity(), null)).isSameAs(sctx);
assertThat(ClientRequestContext.mapCurrent(Function.identity(), null)).isSameAs(cctx);
assertThat(RequestContext.mapCurrent(Function.identity(), null)).isSameAs(cctx);
}
assertCurrentCtx(sctx);
}
assertCurrentCtx(null);

try (SafeCloseable unused = clientRequestContext().push()) {
assertThatThrownBy(() -> ServiceRequestContext.mapCurrent(c -> "foo", () -> "bar"))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("not a server-side context");
}
}

@Test
void pushReentrance() {
final ServiceRequestContext ctx = serviceRequestContext();
try (SafeCloseable ignored = ctx.push()) {
assertCurrentCtx(ctx);
try (SafeCloseable ignored2 = ctx.push()) {
assertCurrentCtx(ctx);
}
assertCurrentCtx(ctx);
}
assertCurrentCtx(null);
}

@Test
void pushWithOldClientCtxWhoseRootIsThisServiceCtx() {
final ServiceRequestContext sctx = serviceRequestContext();
try (SafeCloseable ignored = sctx.push()) {
assertCurrentCtx(sctx);
// The root of ClientRequestContext is sctx.
final ClientRequestContext cctx = clientRequestContext();
try (SafeCloseable ignored1 = cctx.push()) {
assertCurrentCtx(cctx);
try (SafeCloseable ignored2 = sctx.push()) {
assertCurrentCtx(sctx);
}
assertCurrentCtx(cctx);
}
assertCurrentCtx(sctx);
}
assertCurrentCtx(null);
}

@Test
void pushWithOldIrrelevantClientCtx() {
final ClientRequestContext cctx = clientRequestContext();
try (SafeCloseable ignored = cctx.push()) {
assertCurrentCtx(cctx);
final ServiceRequestContext sctx = serviceRequestContext();
assertThatThrownBy(sctx::push).isInstanceOf(IllegalStateException.class);
}
assertCurrentCtx(null);
}

@Test
void pushWithOldIrrelevantServiceCtx() {
final ServiceRequestContext sctx1 = serviceRequestContext();
final ServiceRequestContext sctx2 = serviceRequestContext();
try (SafeCloseable ignored = sctx1.push()) {
assertCurrentCtx(sctx1);
assertThatThrownBy(sctx2::push).isInstanceOf(IllegalStateException.class);
}
assertCurrentCtx(null);
}

@Test
void queryParams() {
final String path = "/foo";
final QueryParams queryParams = QueryParams.of("param1", "value1",
"param1", "value2",
"Param1", "Value3",
"PARAM1", "VALUE4");
final String pathAndQuery = path + '?' + queryParams.toQueryString();
final ServiceRequestContext ctx = ServiceRequestContext.of(HttpRequest.of(HttpMethod.GET,
pathAndQuery));

assertThat(ctx.queryParams()).isEqualTo(queryParams);

assertThat(ctx.queryParam("param1")).isEqualTo("value1");
assertThat(ctx.queryParam("Param1")).isEqualTo("Value3");
assertThat(ctx.queryParam("PARAM1")).isEqualTo("VALUE4");
assertThat(ctx.queryParam("Not exist")).isNull();

assertThat(ctx.queryParams("param1")).isEqualTo(ImmutableList.of("value1", "value2"));
assertThat(ctx.queryParams("Param1")).isEqualTo(ImmutableList.of("Value3"));
assertThat(ctx.queryParams("PARAM1")).isEqualTo(ImmutableList.of("VALUE4"));
assertThat(ctx.queryParams("Not exist")).isEmpty();
}

private static void assertCurrentCtx(@Nullable RequestContext ctx) {
final RequestContext current = RequestContext.currentOrNull();
assertThat(current).isSameAs(ctx);
}

private static ServiceRequestContext serviceRequestContext() {
return ServiceRequestContext.of(HttpRequest.of(HttpMethod.GET, "/"));
}

private static ClientRequestContext clientRequestContext() {
return ClientRequestContext.of(HttpRequest.of(HttpMethod.GET, "/"));
}
}