Skip to content

RUMS-4305 Fix the memory leak in the PendingTrace#cleaner#2607

Merged
mariusc83 merged 1 commit into
developfrom
mconstantin/rums-4305/fix-pendingtrace-memory-leak
Apr 24, 2025
Merged

RUMS-4305 Fix the memory leak in the PendingTrace#cleaner#2607
mariusc83 merged 1 commit into
developfrom
mconstantin/rums-4305/fix-pendingtrace-memory-leak

Conversation

@mariusc83

@mariusc83 mariusc83 commented Apr 17, 2025

Copy link
Copy Markdown
Member

What does this PR do?

After analysing the borrowed code from the deprecated APM java tracer following some internal investigations PendingTrace was actually leaking in 2 places:

  1. In one place when using dropSpan the cleaner could not actually clean the PendingTrace reference and this was persisted forever in the pendingTraces set. Basically when the cleaner was trying to clean that method was doing nothing as there was no pending Span to write and the reference was not removed.
  2. Because of using a mutable LinkedList as an entry key to a Set<PendingTrace>. Basically because the key is resolved from the LinkedList#hashCode() method which is dependent on the values contained in the list first time a PendingTrace is created the hashCode() is always 1 and when later we're trying to remove it through clean the hashCodes no longer match.
    The problem was not that visible when instrumenting the httpRequest as the hashCode when adding the list was always 1 so only 1 PendingTrace was persisted but it became more visible when using a very trivial example as:
val tracer = GlobalTracer.get()
            repeat(100){
                val span = tracer.buildSpan("AsyncOperation")
                    .start()
                Thread.sleep(1)
                span.finish()
            }

In this moment we're going to leak 200 PendingTraces because:

  • when adding each first PendingTrace the set will contain [1]->[(pendingTrace1, key1)]
  • when adding second PendingTrace the set will check if key 1 exist and then will equal the keys by values but because at the lists are equaling the elements in the equal method in this moment the first trace has 1 element and the one to be added has 0 so they do not match so the set will contain `[1]->[(pendingTrace1, key1), (pendingTrace2, key2)]
  • when later trying to remove each of these it will first try to match the hashCode but because they all have elements in them the hashCode will not longer match 1 key so the will all be leaked.
    This problem is not reproducible when using the DatadogInterceptor because there we are using drop instead of finish in the Span and this will not add the span into the PendingTrace a.k.a LinkedList. In that situation the pendingTraces set will only contain one empty PendingTrace: [1]->[(PendingTrace, key)]. Every time a new PendingTrace will be created it will match through hashCode and equal to the one in the Set and it will not be added. In this situation only the first added PendingTrace will be leaked and this is because of the reason number 1 described above.

Motivation

What inspired you to submit this pull request?

Additional Notes

Anything else we should know when reviewing?

Review checklist (to be filled by reviewers)

  • Feature or bugfix MUST have appropriate tests (unit, integration, e2e)
  • Make sure you discussed the feature or bugfix with the maintaining team in an Issue
  • Make sure each commit and the PR mention the Issue number (cf the CONTRIBUTING doc)

@mariusc83 mariusc83 self-assigned this Apr 17, 2025
@mariusc83
mariusc83 force-pushed the mconstantin/rums-4305/fix-pendingtrace-memory-leak branch from 583cdda to d4ba379 Compare April 22, 2025 13:23
}

@Test
fun `M cleanup random traces W add and close`(forge: Forge) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Quality Violation

Function names should be camel case. (...read more)

This rule mandates that function names in Kotlin should be written in camel case, starting with a lowercase letter using no underscores. This is convention in Kotlin and enhances readability and maintainability.

View in Datadog  Leave us feedback  Documentation

}

@Test
fun `M not leak the PendingTraces W dropSpan`(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Quality Violation

Function names should be camel case. (...read more)

This rule mandates that function names in Kotlin should be written in camel case, starting with a lowercase letter using no underscores. This is convention in Kotlin and enhances readability and maintainability.

View in Datadog  Leave us feedback  Documentation

Comment on lines +30 to +33
@Extensions(
ExtendWith(MockitoExtension::class),
ExtendWith(ForgeExtension::class)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Quality Violation

Annotations should be separated by a single line break. (...read more)

Each annotations should be placed on a separate line. This is important because it makes the code more readable and easier to understand. When multiple annotations are placed on the same line, it can become difficult to distinguish between them and understand their individual impacts on the associated code.

View in Datadog  Leave us feedback  Documentation

}

@Test
fun `M not leak the PendingTraces W finishSpan from different threads`(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Quality Violation

Function names should be camel case. (...read more)

This rule mandates that function names in Kotlin should be written in camel case, starting with a lowercase letter using no underscores. This is convention in Kotlin and enhances readability and maintainability.

View in Datadog  Leave us feedback  Documentation

assertThat(PendingTrace.getPendingTracesSize()).isEqualTo(0)
}

@Test

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Quality Violation

Annotations should be separated by a single line break. (...read more)

Each annotations should be placed on a separate line. This is important because it makes the code more readable and easier to understand. When multiple annotations are placed on the same line, it can become difficult to distinguish between them and understand their individual impacts on the associated code.

View in Datadog  Leave us feedback  Documentation

}

@Test
fun `M contain only one entry W add {same PendingTrace mutated, twice}`(forge: Forge) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Quality Violation

Function names should be camel case. (...read more)

This rule mandates that function names in Kotlin should be written in camel case, starting with a lowercase letter using no underscores. This is convention in Kotlin and enhances readability and maintainability.

View in Datadog  Leave us feedback  Documentation

// region concurrency

@Test
fun `M not leak any PendingTrace W add, mutate and remove { different threads }`(forge: Forge) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Quality Violation

Function names should be camel case. (...read more)

This rule mandates that function names in Kotlin should be written in camel case, starting with a lowercase letter using no underscores. This is convention in Kotlin and enhances readability and maintainability.

View in Datadog  Leave us feedback  Documentation

addCountDownLatch.countDown()
}.start()
}
pendingTraces.takeLast(pendingTraces.size / 2).forEach { pendingTrace ->

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Quality Violation

Suggested change
pendingTraces.takeLast(pendingTraces.size / 2).forEach { pendingTrace ->
pendingTraces.takeLast(pendingTraces.size / 2).forEach {
pendingTrace ->
Block body statement on the same line as curly brace (...read more)

When using a multi-line statement with a curly brace, you should always put the contained code on a separate line from the curly brace.

The one exception to this is when using a lambda expression.

View in Datadog  Leave us feedback  Documentation

Comment on lines +25 to +27
override fun createEqualInstance(source: IdentityKey, forge: Forge): IdentityKey {
return IdentityKey(source.key)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Quality Violation

Function return type should be preceded by a single space. (...read more)

Kotlin enforces consistent spacing around the function return type: one space before the return type.

View in Datadog  Leave us feedback  Documentation

// region `equals` contract

@Test
fun `M contain only one entry W add same PendingTrace twice`(forge: Forge) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Quality Violation

Function names should be camel case. (...read more)

This rule mandates that function names in Kotlin should be written in camel case, starting with a lowercase letter using no underscores. This is convention in Kotlin and enhances readability and maintainability.

View in Datadog  Leave us feedback  Documentation

@codecov-commenter

codecov-commenter commented Apr 22, 2025

Copy link
Copy Markdown

Codecov Report

Attention: Patch coverage is 68.82353% with 53 lines in your changes missing coverage. Please review.

Project coverage is 70.04%. Comparing base (c5b619f) to head (30caa79).
Report is 140 commits behind head on develop.

Files with missing lines Patch % Lines
...ain/java/com/datadog/opentracing/PendingTrace.java 68.82% 34 Missing and 19 partials ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #2607      +/-   ##
===========================================
+ Coverage    69.97%   70.04%   +0.07%     
===========================================
  Files          820      820              
  Lines        30554    30568      +14     
  Branches      5130     5133       +3     
===========================================
+ Hits         21379    21410      +31     
+ Misses        7747     7723      -24     
- Partials      1428     1435       +7     
Files with missing lines Coverage Δ
...ain/java/com/datadog/opentracing/PendingTrace.java 68.82% <68.82%> (+10.49%) ⬆️

... and 38 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@mariusc83
mariusc83 marked this pull request as ready for review April 22, 2025 17:01
@mariusc83
mariusc83 requested review from a team as code owners April 22, 2025 17:01
pendingTraces.forEach {
testeCleaner.addPendingTrace(it)
}
assertThat(testeCleaner.pendingTraces.values).containsExactlyInAnyOrderElementsOf(pendingTraces)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

should it be assume here as precondition?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Let me think...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I don't like assume as it doesn't have all the list assertions :(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

yes, but it is a different behavior - if assume fails, then test will be simply skipped and it won't be counted as test failure, unlike in the assert case. Do we want to fail the test on this pre-condition check?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

yup because that means the cleaner doesn't work correctly.

Comment on lines +140 to +169
if (forge.aBool()) {
repeat(forge.anInt(min = 1, max = 10)) {
pendingTrace.addSpan(mock())
}
}
testeCleaner.addPendingTrace(pendingTrace)
repeat(forge.anInt(min = 1, max = 10)) {
pendingTrace.addSpan(mock())
}
addCountDownLatch.countDown()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

there is a small difference with a block above, it is using if (forge.aBool()), is it intended?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

yeah I just wanted to test random case where the pending trace has something before was added, maybe should I add 2 tests here ? Isn't going to be too much ?

@mariusc83
mariusc83 force-pushed the mconstantin/rums-4305/fix-pendingtrace-memory-leak branch from d4ba379 to 22d8940 Compare April 23, 2025 09:11
addCountDownLatch.countDown()
}.start()
}
pendingTraces.subList(half, pendingTraces.size).forEach { pendingTrace ->

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Quality Violation

Suggested change
pendingTraces.subList(half, pendingTraces.size).forEach { pendingTrace ->
pendingTraces.subList(half, pendingTraces.size).forEach {
pendingTrace ->
Block body statement on the same line as curly brace (...read more)

When using a multi-line statement with a curly brace, you should always put the contained code on a separate line from the curly brace.

The one exception to this is when using a lambda expression.

View in Datadog  Leave us feedback  Documentation

assertThat(PendingTrace.getPendingTracesSize()).isZero()
}

@Test

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Quality Violation

Annotations should be separated by a single line break. (...read more)

Each annotations should be placed on a separate line. This is important because it makes the code more readable and easier to understand. When multiple annotations are placed on the same line, it can become difficult to distinguish between them and understand their individual impacts on the associated code.

View in Datadog  Leave us feedback  Documentation

val addCountDownLatch = CountDownLatch(pendingTraces.size)
val removeCountDownLatch = CountDownLatch(pendingTraces.size)
val half = pendingTraces.size / 2
pendingTraces.take(half).forEach { pendingTrace ->

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Quality Violation

Suggested change
pendingTraces.take(half).forEach { pendingTrace ->
pendingTraces.take(half).forEach {
pendingTrace ->
Block body statement on the same line as curly brace (...read more)

When using a multi-line statement with a curly brace, you should always put the contained code on a separate line from the curly brace.

The one exception to this is when using a lambda expression.

View in Datadog  Leave us feedback  Documentation

@mariusc83
mariusc83 force-pushed the mconstantin/rums-4305/fix-pendingtrace-memory-leak branch 2 times, most recently from 7b49bdb to 604b091 Compare April 23, 2025 11:37
assertThat(PendingTrace.getPendingTracesSize()).isEqualTo(0)
}

@Test

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Quality Violation

Annotations should be separated by a single line break. (...read more)

Each annotations should be placed on a separate line. This is important because it makes the code more readable and easier to understand. When multiple annotations are placed on the same line, it can become difficult to distinguish between them and understand their individual impacts on the associated code.

View in Datadog  Leave us feedback  Documentation

}

@Test
fun `M contain 2 entries W add {2 PendingTrace instances with same root span}`(forge: Forge) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Quality Violation

Function names should be camel case. (...read more)

This rule mandates that function names in Kotlin should be written in camel case, starting with a lowercase letter using no underscores. This is convention in Kotlin and enhances readability and maintainability.

View in Datadog  Leave us feedback  Documentation

ambushwork
ambushwork previously approved these changes Apr 23, 2025

@ambushwork ambushwork left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

@mariusc83
mariusc83 requested a review from 0xnm April 23, 2025 14:03
@mariusc83
mariusc83 force-pushed the mconstantin/rums-4305/fix-pendingtrace-memory-leak branch from 604b091 to ef743a1 Compare April 23, 2025 14:19
xgouchet
xgouchet previously approved these changes Apr 23, 2025

@xgouchet xgouchet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nice work

@mariusc83
mariusc83 requested a review from ambushwork April 23, 2025 15:37
}
if (count > 0) {
// TODO attempt to flatten and report if top level spans are finished. (for accurate metrics)
} else if (pendingReferenceCount.get() == 0) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm not quite sure about this change: let's say we created a new PendingTrace without any spans inside and we add spans only after first cycle of the SPAN_CLEANER is completed - with this change removePendingTrace will be called even before we manage to register first span, no? 🤔

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

hmm, you might be right, let me double check this. Idea is that I need to find a way to remove that trace somehow, without knowing all the code base it's hard :(.

"childSpan_$i",
internalLogger
)
pendingTrace.registerSpan(span)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

what is the need for this change?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

the registerSpan it is already called in the constructor of DdSpan

Assumptions.assumeTrue(testedCleaner.pendingTraces.values.containsAll(pendingTraces))

// When
pendingTraces.shuffled().forEach {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
pendingTraces.shuffled().forEach {
pendingTraces.shuffled(Random(forge.seed)).forEach {

but what is the need to call shuffled anyway? to have a different order between addPendingTrace and removePendingTrace calls?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

yup

@mariusc83
mariusc83 force-pushed the mconstantin/rums-4305/fix-pendingtrace-memory-leak branch from ef743a1 to 9528106 Compare April 24, 2025 10:45
@mariusc83
mariusc83 requested a review from 0xnm April 24, 2025 11:00
@mariusc83
mariusc83 force-pushed the mconstantin/rums-4305/fix-pendingtrace-memory-leak branch from 9528106 to 1388482 Compare April 24, 2025 12:18
@mariusc83
mariusc83 force-pushed the mconstantin/rums-4305/fix-pendingtrace-memory-leak branch from 1388482 to 30caa79 Compare April 24, 2025 14:55
@mariusc83
mariusc83 merged commit 2e24bf5 into develop Apr 24, 2025
@mariusc83
mariusc83 deleted the mconstantin/rums-4305/fix-pendingtrace-memory-leak branch April 24, 2025 17:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants