RUMS-4305 Fix the memory leak in the PendingTrace#cleaner#2607
Conversation
583cdda to
d4ba379
Compare
| } | ||
|
|
||
| @Test | ||
| fun `M cleanup random traces W add and close`(forge: Forge) { |
There was a problem hiding this comment.
| } | ||
|
|
||
| @Test | ||
| fun `M not leak the PendingTraces W dropSpan`( |
There was a problem hiding this comment.
| @Extensions( | ||
| ExtendWith(MockitoExtension::class), | ||
| ExtendWith(ForgeExtension::class) | ||
| ) |
There was a problem hiding this comment.
⚪ 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.
| } | ||
|
|
||
| @Test | ||
| fun `M not leak the PendingTraces W finishSpan from different threads`( |
There was a problem hiding this comment.
| assertThat(PendingTrace.getPendingTracesSize()).isEqualTo(0) | ||
| } | ||
|
|
||
| @Test |
There was a problem hiding this comment.
⚪ 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.
| } | ||
|
|
||
| @Test | ||
| fun `M contain only one entry W add {same PendingTrace mutated, twice}`(forge: Forge) { |
There was a problem hiding this comment.
| // region concurrency | ||
|
|
||
| @Test | ||
| fun `M not leak any PendingTrace W add, mutate and remove { different threads }`(forge: Forge) { |
There was a problem hiding this comment.
| addCountDownLatch.countDown() | ||
| }.start() | ||
| } | ||
| pendingTraces.takeLast(pendingTraces.size / 2).forEach { pendingTrace -> |
There was a problem hiding this comment.
⚪ Code Quality Violation
| 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.
| override fun createEqualInstance(source: IdentityKey, forge: Forge): IdentityKey { | ||
| return IdentityKey(source.key) | ||
| } |
| // region `equals` contract | ||
|
|
||
| @Test | ||
| fun `M contain only one entry W add same PendingTrace twice`(forge: Forge) { |
There was a problem hiding this comment.
Codecov ReportAttention: Patch coverage is
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
🚀 New features to boost your workflow:
|
| pendingTraces.forEach { | ||
| testeCleaner.addPendingTrace(it) | ||
| } | ||
| assertThat(testeCleaner.pendingTraces.values).containsExactlyInAnyOrderElementsOf(pendingTraces) |
There was a problem hiding this comment.
should it be assume here as precondition?
There was a problem hiding this comment.
I don't like assume as it doesn't have all the list assertions :(
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
yup because that means the cleaner doesn't work correctly.
| 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() |
There was a problem hiding this comment.
there is a small difference with a block above, it is using if (forge.aBool()), is it intended?
There was a problem hiding this comment.
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 ?
d4ba379 to
22d8940
Compare
| addCountDownLatch.countDown() | ||
| }.start() | ||
| } | ||
| pendingTraces.subList(half, pendingTraces.size).forEach { pendingTrace -> |
There was a problem hiding this comment.
⚪ Code Quality Violation
| 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.
| assertThat(PendingTrace.getPendingTracesSize()).isZero() | ||
| } | ||
|
|
||
| @Test |
There was a problem hiding this comment.
⚪ 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.
| val addCountDownLatch = CountDownLatch(pendingTraces.size) | ||
| val removeCountDownLatch = CountDownLatch(pendingTraces.size) | ||
| val half = pendingTraces.size / 2 | ||
| pendingTraces.take(half).forEach { pendingTrace -> |
There was a problem hiding this comment.
⚪ Code Quality Violation
| 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.
7b49bdb to
604b091
Compare
| assertThat(PendingTrace.getPendingTracesSize()).isEqualTo(0) | ||
| } | ||
|
|
||
| @Test |
There was a problem hiding this comment.
⚪ 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.
| } | ||
|
|
||
| @Test | ||
| fun `M contain 2 entries W add {2 PendingTrace instances with same root span}`(forge: Forge) { |
There was a problem hiding this comment.
604b091 to
ef743a1
Compare
| } | ||
| if (count > 0) { | ||
| // TODO attempt to flatten and report if top level spans are finished. (for accurate metrics) | ||
| } else if (pendingReferenceCount.get() == 0) { |
There was a problem hiding this comment.
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? 🤔
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
the registerSpan it is already called in the constructor of DdSpan
| Assumptions.assumeTrue(testedCleaner.pendingTraces.values.containsAll(pendingTraces)) | ||
|
|
||
| // When | ||
| pendingTraces.shuffled().forEach { |
There was a problem hiding this comment.
| 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?
ef743a1 to
9528106
Compare
9528106 to
1388482
Compare
1388482 to
30caa79
Compare
What does this PR do?
After analysing the borrowed code from the deprecated APM java tracer following some internal investigations
PendingTracewas actually leaking in 2 places:dropSpanthe cleaner could not actually clean thePendingTracereference and this was persisted forever in thependingTracesset. Basically when the cleaner was trying tocleanthat method was doing nothing as there was no pending Span to write and the reference was not removed.LinkedListas an entry key to aSet<PendingTrace>. Basically because the key is resolved from theLinkedList#hashCode()method which is dependent on the values contained in the list first time aPendingTraceis created thehashCode()is always1and when later we're trying to remove it throughcleanthehashCodesno longer match.The problem was not that visible when instrumenting the
httpRequestas thehashCodewhen adding the list was always1so only 1PendingTracewas persisted but it became more visible when using a very trivial example as:In this moment we're going to leak 200
PendingTracesbecause:PendingTracethe set will contain[1]->[(pendingTrace1, key1)]PendingTracethe set will check if key1exist and then will equal the keys by values but because at the lists are equaling the elements in theequalmethod 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)]hashCodebut because they all have elements in them thehashCodewill not longer match1key so the will all be leaked.This problem is not reproducible when using the
DatadogInterceptorbecause there we are usingdropinstead offinishin the Span and this will not add the span into thePendingTracea.k.aLinkedList. In that situation thependingTracesset will only contain one emptyPendingTrace:[1]->[(PendingTrace, key)]. Every time a newPendingTracewill be created it will match throughhashCodeandequalto the one in theSetand it will not be added. In this situation only the first addedPendingTracewill 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)