Skip to content

Commit a985cb8

Browse files
committed
RUM-15138: Move session sample rate receiver ownership to TracingFeature
Instead of each ApmNetworkInstrumentation instance creating and managing its own FeatureContextUpdateReceiver (with manual close()/synchronized lifecycle), TracingFeature now owns a single receiver that listens for RUM context changes and broadcasts the session sample rate to all registered AtomicReference<Float> instances. ApmNetworkInstrumentation sends a SessionSampleRateRegistrationEvent to TracingFeature on SDK resolution, which registers the ref and initializes it with the current rate. TracingFeature unregisters the receiver and clears all refs on onStop(), tying the lifecycle to the feature itself. This eliminates the need for close() on ApmNetworkInstrumentation, fixing the leak where the OkHttp integration path never called close() while the Cronet path did. # Conflicts: # features/dd-sdk-android-trace/src/test/kotlin/com/datadog/android/trace/internal/net/ApmNetworkInstrumentationTest.kt
1 parent b7272e5 commit a985cb8

12 files changed

Lines changed: 243 additions & 125 deletions

File tree

detekt_custom_safe_calls.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,7 @@ datadog:
597597
- "java.util.concurrent.atomic.AtomicReference.constructor(com.datadog.android.flags.model.FlagsClientState?)"
598598
- "java.util.concurrent.atomic.AtomicReference.constructor(com.datadog.android.flags.model.ProviderContext?)"
599599
- "java.util.concurrent.atomic.AtomicReference.constructor(com.datadog.android.rum.internal.domain.RumContext?)"
600+
- "java.util.concurrent.atomic.AtomicReference.constructor(kotlin.Float?)"
600601
- "java.util.concurrent.atomic.AtomicReference.constructor(kotlin.collections.Map?)"
601602
- "java.util.concurrent.atomic.AtomicReference.constructor(kotlin.String?)"
602603
- "java.util.concurrent.atomic.AtomicReference.constructor(kotlin.collections.Set?)"
@@ -613,6 +614,7 @@ datadog:
613614
- "java.util.concurrent.atomic.AtomicReference.set(com.datadog.android.flags.model.ProviderContext?)"
614615
- "java.util.concurrent.atomic.AtomicReference.set(com.datadog.trace.bootstrap.instrumentation.api.AgentTracer.TracerAPI?)"
615616
- "java.util.concurrent.atomic.AtomicReference.set(com.datadog.trace.core.CoreTracer?)"
617+
- "java.util.concurrent.atomic.AtomicReference.set(kotlin.Float?)"
616618
- "java.util.concurrent.atomic.AtomicReference.set(kotlin.collections.Map?)"
617619
- "java.util.concurrent.atomic.AtomicReference.set(kotlin.Nothing?)"
618620
- "java.util.concurrent.atomic.AtomicReference.set(kotlin.String?)"
@@ -997,6 +999,7 @@ datadog:
997999
- "kotlin.collections.MutableSet.add(com.datadog.android.sessionreplay.ExtensionSupport)"
9981000
- "kotlin.collections.MutableSet.add(com.datadog.android.telemetry.internal.TelemetryEventId)"
9991001
- "kotlin.collections.MutableSet.add(java.io.File)"
1002+
- "kotlin.collections.MutableSet.add(java.util.concurrent.atomic.AtomicReference)"
10001003
- "kotlin.collections.MutableSet.add(kotlin.String)"
10011004
- "kotlin.collections.MutableSet.add(kotlin.String?)"
10021005
- "kotlin.collections.MutableSet.addAll(kotlin.collections.Collection)"

features/dd-sdk-android-trace/api/apiSurface

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ class com.datadog.android.trace.internal.ApmNetworkInstrumentation
5959
fun onResponseFailed(com.datadog.android.trace.internal.net.RequestTracingState, Throwable)
6060
fun removeTracingHeaders(com.datadog.android.api.instrumentation.network.HttpRequestInfoBuilder)
6161
fun reportInstrumentationError(() -> String)
62-
fun close()
6362
class com.datadog.android.trace.internal.DatadogPropagationHelper
6463
fun isExtractedContext(com.datadog.android.trace.api.span.DatadogSpanContext): Boolean
6564
fun setTraceContext(com.datadog.android.api.instrumentation.network.HttpRequestInfoBuilder, String, String, Int)

features/dd-sdk-android-trace/api/dd-sdk-android-trace.api

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,6 @@ public abstract interface class com/datadog/android/trace/event/SpanEventMapper
9999
}
100100

101101
public final class com/datadog/android/trace/internal/ApmNetworkInstrumentation {
102-
public final fun close ()V
103102
public final fun getLocalHeaderTypes ()Ljava/util/Set;
104103
public final fun getNetworkTracingScope ()Lcom/datadog/android/trace/ApmNetworkTracingScope;
105104
public final fun getSampleRate ()Ljava/lang/Float;

features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/ApmNetworkInstrumentationConfiguration.kt

Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,13 @@ package com.datadog.android.trace
77

88
import androidx.annotation.FloatRange
99
import com.datadog.android.api.SdkCore
10-
import com.datadog.android.api.feature.Feature
11-
import com.datadog.android.api.feature.FeatureContextUpdateReceiver
1210
import com.datadog.android.core.configuration.HostsSanitizer
1311
import com.datadog.android.core.internal.net.DefaultFirstPartyHostHeaderTypeResolver
1412
import com.datadog.android.core.sampling.Sampler
15-
import com.datadog.android.log.LogAttributes
1613
import com.datadog.android.trace.api.span.DatadogSpan
1714
import com.datadog.android.trace.api.tracer.DatadogTracer
1815
import com.datadog.android.trace.internal.ApmNetworkInstrumentation
16+
import com.datadog.android.trace.internal.TracingFeature
1917
import com.datadog.android.trace.internal.net.TracerProvider
2018
import java.util.concurrent.atomic.AtomicReference
2119

@@ -242,11 +240,6 @@ class ApmNetworkInstrumentationConfiguration internal constructor(
242240
internal const val ALL_IN_SAMPLE_RATE: Double = 100.0
243241
private const val DEFAULT_TRACE_SAMPLE_RATE: Float = 100f
244242

245-
// Sentinel value meaning "assume all RUM sessions are sampled" so that no session-based
246-
// rebasing is applied until the RUM feature publishes its actual session sample rate.
247-
// With this value the effective rate is: traceSampleRate * 100 / 100 = traceSampleRate.
248-
private const val NO_SESSION_REBASING_RATE: Float = 100f
249-
250243
internal const val NETWORK_REQUESTS_TRACKING_FEATURE_NAME = "Network Requests"
251244

252245
internal fun ApmNetworkInstrumentationConfiguration.createInstrumentation(
@@ -259,22 +252,9 @@ class ApmNetworkInstrumentationConfiguration internal constructor(
259252
val tracerProvider = TracerProvider(localTracerFactory, globalTracerProvider)
260253

261254
@Suppress("UnsafeThirdPartyFunctionCall") // AtomicReference initialized with non-null Float constant
262-
val cachedSessionSampleRate = AtomicReference(NO_SESSION_REBASING_RATE)
263-
val sessionSampleRateReceiver: FeatureContextUpdateReceiver? =
264-
if (customTraceSampler == null) {
265-
FeatureContextUpdateReceiver { featureName, context ->
266-
if (featureName == Feature.RUM_FEATURE_NAME) {
267-
@Suppress(
268-
"UnsafeThirdPartyFunctionCall"
269-
) // resolveSessionSampleRate always returns non-null Float
270-
cachedSessionSampleRate.set(
271-
resolveSessionSampleRate(context[LogAttributes.RUM_SESSION_SAMPLE_RATE])
272-
)
273-
}
274-
}
275-
} else {
276-
null
277-
}
255+
val cachedSessionSampleRate = AtomicReference(TracingFeature.NO_SESSION_REBASING_RATE)
256+
val sessionSampleRateRef: AtomicReference<Float>? =
257+
if (customTraceSampler == null) cachedSessionSampleRate else null
278258
val effectiveSampler = customTraceSampler
279259
?: DeterministicTraceSampler(traceSampleRate) { cachedSessionSampleRate.get() }
280260

@@ -290,7 +270,7 @@ class ApmNetworkInstrumentationConfiguration internal constructor(
290270
tracedRequestListener = tracedRequestListener,
291271
redacted404ResourceName = redacted404ResourceName,
292272
localFirstPartyHostHeaderTypeResolver = localFirstPartyHostHeaderTypeResolver,
293-
sessionSampleRateReceiver = sessionSampleRateReceiver
273+
sessionSampleRateRef = sessionSampleRateRef
294274
)
295275
}
296276

@@ -308,10 +288,6 @@ class ApmNetworkInstrumentationConfiguration internal constructor(
308288

309289
private fun Map<String, Set<TracingHeaderType>>.deepCopy() = mapValues { (_, v) -> v.toSet() }
310290

311-
private fun resolveSessionSampleRate(rawSessionSampleRate: Any?): Float {
312-
return (rawSessionSampleRate as? Number)?.toFloat() ?: NO_SESSION_REBASING_RATE
313-
}
314-
315291
private val DEFAULT_LOCAL_TRACER_FACTORY: (SdkCore, Set<TracingHeaderType>) -> DatadogTracer =
316292
{ sdkCore, tracingHeaderTypes: Set<TracingHeaderType> ->
317293
DatadogTracing.newTracerBuilder(sdkCore)

features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/ApmNetworkInstrumentation.kt

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ package com.datadog.android.trace.internal
88

99
import com.datadog.android.api.InternalLogger
1010
import com.datadog.android.api.SdkCore
11-
import com.datadog.android.api.feature.FeatureContextUpdateReceiver
11+
import com.datadog.android.api.feature.Feature
1212
import com.datadog.android.api.instrumentation.network.HttpRequestInfo
1313
import com.datadog.android.api.instrumentation.network.HttpRequestInfoBuilder
1414
import com.datadog.android.api.instrumentation.network.HttpResponseInfo
@@ -38,6 +38,7 @@ import com.datadog.android.trace.internal.net.finishRumAware
3838
import com.datadog.android.trace.internal.net.sample
3939
import java.net.HttpURLConnection
4040
import java.util.Locale
41+
import java.util.concurrent.atomic.AtomicReference
4142

4243
/**
4344
* For internal usage only.
@@ -58,7 +59,11 @@ import java.util.Locale
5859
* @param localFirstPartyHostHeaderTypeResolver resolver for determining header types for first-party hosts.
5960
* @param networkingLibraryName the name identifying the network instrumentation (e.g., "OkHttp", "Cronet").
6061
* @param networkTracingScope Tracing scope for the instrumentation. See [ApmNetworkTracingScope] enum for more details.
61-
* @param sessionSampleRateReceiver optional receiver that caches the RUM session sample rate for sampling decisions. When provided, it is registered with the SDK on first resolution and updated whenever the RUM context changes.
62+
* @param sessionSampleRateRef optional [AtomicReference] that [TracingFeature] will keep
63+
* up-to-date with the latest RUM session sample rate. When the SDK core resolves,
64+
* this ref is sent to [TracingFeature] via `sendEvent` so that it is updated whenever
65+
* the RUM context changes. The corresponding [DeterministicTraceSampler] reads from
66+
* this ref on every request (volatile read, no locking).
6267
*/
6368
@Suppress("LongParameterList")
6469
@InternalApi
@@ -74,12 +79,9 @@ class ApmNetworkInstrumentation internal constructor(
7479
internal val localFirstPartyHostHeaderTypeResolver: DefaultFirstPartyHostHeaderTypeResolver,
7580
private val networkingLibraryName: String,
7681
val networkTracingScope: ApmNetworkTracingScope = ApmNetworkTracingScope.ALL,
77-
internal val sessionSampleRateReceiver: FeatureContextUpdateReceiver? = null
82+
internal val sessionSampleRateRef: AtomicReference<Float>? = null
7883
) {
7984
private val rumContextPropagator = RumContextPropagator { internalSdkCore }
80-
private val receiverLifecycleLock = Any()
81-
private var isClosed = false
82-
private var isRegistered = false
8385

8486
private val internalSdkCore: InternalSdkCore?
8587
get() = sdkCoreReference.get() as? InternalSdkCore
@@ -96,13 +98,9 @@ class ApmNetworkInstrumentation internal constructor(
9698
WARNING_TRACING_NO_HOSTS.format(Locale.US, networkingLibraryName)
9799
}
98100
}
99-
sessionSampleRateReceiver?.let { receiver ->
100-
synchronized(receiverLifecycleLock) {
101-
if (!isClosed && !isRegistered) {
102-
sdkCore.setContextUpdateReceiver(receiver)
103-
isRegistered = true
104-
}
105-
}
101+
sessionSampleRateRef?.let { ref ->
102+
sdkCore.getFeature(Feature.TRACING_FEATURE_NAME)
103+
?.sendEvent(SessionSampleRateRegistrationEvent(ref))
106104
}
107105
}
108106

@@ -234,20 +232,6 @@ class ApmNetworkInstrumentation internal constructor(
234232
)
235233
}
236234

237-
/**
238-
* Releases this instrumentation and unregisters context receivers from SDK core.
239-
*/
240-
fun close() {
241-
val receiver = sessionSampleRateReceiver ?: return
242-
synchronized(receiverLifecycleLock) {
243-
isClosed = true
244-
if (isRegistered) {
245-
isRegistered = false
246-
internalSdkCore?.removeContextUpdateReceiver(receiver)
247-
}
248-
}
249-
}
250-
251235
private fun RequestTracingState.onRequestIntercepted(
252236
response: HttpResponseInfo?,
253237
throwable: Throwable?
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2016-Present Datadog, Inc.
5+
*/
6+
7+
package com.datadog.android.trace.internal
8+
9+
import java.util.concurrent.atomic.AtomicReference
10+
11+
/**
12+
* Event sent by [ApmNetworkInstrumentation] to [TracingFeature] via `sendEvent`
13+
* to register an [AtomicReference] that should be kept up-to-date with the current
14+
* RUM session sample rate.
15+
*
16+
* TracingFeature updates all registered references whenever the RUM feature context
17+
* changes, ensuring that [com.datadog.android.trace.DeterministicTraceSampler] can
18+
* read the latest session sample rate with a simple volatile read (no locking).
19+
*
20+
* @param ref the [AtomicReference] that TracingFeature will update with the
21+
* latest RUM session sample rate.
22+
*/
23+
internal data class SessionSampleRateRegistrationEvent(val ref: AtomicReference<Float>)

features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/TracingFeature.kt

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@ package com.datadog.android.trace.internal
88

99
import android.content.Context
1010
import com.datadog.android.api.feature.Feature
11+
import com.datadog.android.api.feature.FeatureContextUpdateReceiver
12+
import com.datadog.android.api.feature.FeatureEventReceiver
1113
import com.datadog.android.api.feature.FeatureSdkCore
1214
import com.datadog.android.api.feature.StorageBackedFeature
1315
import com.datadog.android.api.net.RequestFactory
1416
import com.datadog.android.api.storage.FeatureStorageConfiguration
17+
import com.datadog.android.log.LogAttributes
1518
import com.datadog.android.trace.InternalCoreWriterProvider
1619
import com.datadog.android.trace.event.SpanEventMapper
1720
import com.datadog.android.trace.internal.data.CoreTraceWriter
@@ -22,6 +25,7 @@ import com.datadog.android.trace.internal.net.TracesRequestFactory
2225
import com.datadog.trace.common.writer.NoOpWriter
2326
import com.datadog.trace.common.writer.Writer
2427
import java.util.concurrent.atomic.AtomicBoolean
28+
import java.util.concurrent.atomic.AtomicReference
2529

2630
/**
2731
* Tracing feature class, which needs to be registered with Datadog SDK instance.
@@ -31,17 +35,23 @@ internal class TracingFeature(
3135
customEndpointUrl: String?,
3236
internal val spanEventMapper: SpanEventMapper,
3337
internal val networkInfoEnabled: Boolean
34-
) : InternalCoreWriterProvider, StorageBackedFeature {
38+
) : InternalCoreWriterProvider, StorageBackedFeature, FeatureContextUpdateReceiver, FeatureEventReceiver {
3539

3640
internal var coreTracerDataWriter: Writer = NoOpWriter()
3741
internal val initialized = AtomicBoolean(false)
3842

43+
internal val internalSessionSampleRate = AtomicReference(NO_SESSION_REBASING_RATE)
44+
private val sessionSampleRateRefs = mutableSetOf<AtomicReference<Float>>()
45+
internal val registrationLock = Any()
46+
3947
// region Feature
4048

4149
override val name: String = Feature.TRACING_FEATURE_NAME
4250

4351
override fun onInitialize(appContext: Context) {
4452
coreTracerDataWriter = createDataWriter(sdkCore)
53+
sdkCore.setContextUpdateReceiver(this)
54+
sdkCore.setEventReceiver(name, this)
4555
initialized.set(true)
4656
}
4757

@@ -56,17 +66,54 @@ internal class TracingFeature(
5666
FeatureStorageConfiguration.DEFAULT
5767

5868
override fun onStop() {
69+
sdkCore.removeContextUpdateReceiver(this)
70+
sdkCore.removeEventReceiver(name)
71+
resetSessionSampleRateTracking()
5972
initialized.set(false)
6073
}
6174

6275
// endregion
6376

77+
// region FeatureContextUpdateReceiver
78+
79+
override fun onContextUpdate(featureName: String, context: Map<String, Any?>) {
80+
if (featureName == Feature.RUM_FEATURE_NAME) {
81+
val rate = resolveSessionSampleRate(context[LogAttributes.RUM_SESSION_SAMPLE_RATE])
82+
synchronized(registrationLock) {
83+
internalSessionSampleRate.set(rate)
84+
sessionSampleRateRefs.forEach { it.set(rate) }
85+
}
86+
}
87+
}
88+
89+
// endregion
90+
91+
// region FeatureEventReceiver
92+
93+
override fun onReceive(event: Any) {
94+
if (event is SessionSampleRateRegistrationEvent) {
95+
synchronized(registrationLock) {
96+
sessionSampleRateRefs.add(event.ref)
97+
event.ref.set(internalSessionSampleRate.get())
98+
}
99+
}
100+
}
101+
102+
// endregion
103+
64104
// region InternalCoreWriterProvider
65105

66106
override fun getCoreTracerWriter() = DatadogSpanWriterWrapper(coreTracerDataWriter)
67107

68108
// endregion
69109

110+
private fun resetSessionSampleRateTracking() {
111+
synchronized(registrationLock) {
112+
sessionSampleRateRefs.clear()
113+
internalSessionSampleRate.set(NO_SESSION_REBASING_RATE)
114+
}
115+
}
116+
70117
private fun createDataWriter(sdkCore: FeatureSdkCore): Writer {
71118
val internalLogger = sdkCore.internalLogger
72119
return CoreTraceWriter(
@@ -77,4 +124,15 @@ internal class TracingFeature(
77124
internalLogger = internalLogger
78125
)
79126
}
127+
128+
internal companion object {
129+
// Sentinel value meaning "assume all RUM sessions are sampled" so that no session-based
130+
// rebasing is applied until the RUM feature publishes its actual session sample rate.
131+
// With this value the effective rate is: traceSampleRate * 100 / 100 = traceSampleRate.
132+
internal const val NO_SESSION_REBASING_RATE: Float = 100f
133+
134+
private fun resolveSessionSampleRate(rawSessionSampleRate: Any?): Float {
135+
return (rawSessionSampleRate as? Number)?.toFloat() ?: NO_SESSION_REBASING_RATE
136+
}
137+
}
80138
}

0 commit comments

Comments
 (0)