diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpResourceSerializer.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpResourceSerializer.cs index d6aa234837f..05480fbf233 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpResourceSerializer.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpResourceSerializer.cs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using System.Buffers; -using System.Collections.Concurrent; +using System.Runtime.CompilerServices; using OpenTelemetry.Resources; namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; @@ -12,7 +12,7 @@ internal static class ProtobufOtlpResourceSerializer private const int ReserveSizeForLength = 4; private const int InitialBufferSize = 2048; - private static readonly ConcurrentDictionary CachedResourceBytes = new(); + private static readonly ConditionalWeakTable CachedResourceBytes = new(); private static ReadOnlySpan EmptyResourceBytes => [0x0A, 0x80, 0x80, 0x80, 0x00]; @@ -24,7 +24,12 @@ internal static int WriteResource(byte[] buffer, int writePosition, Resource? re return writePosition + EmptyResourceBytes.Length; } - var cached = CachedResourceBytes.GetOrAdd(resource, static r => SerializeResourceToBytes(r)); +#if NET10_0_OR_GREATER + var cached = CachedResourceBytes.GetOrAdd(resource, SerializeResourceToBytes); +#else + var cached = CachedResourceBytes.GetValue(resource, SerializeResourceToBytes); +#endif + Buffer.BlockCopy(cached, 0, buffer, writePosition, cached.Length); return writePosition + cached.Length; } @@ -32,8 +37,8 @@ internal static int WriteResource(byte[] buffer, int writePosition, Resource? re private static byte[] SerializeResourceToBytes(Resource resource) { var pool = ArrayPool.Shared; - var buffer = pool.Rent(InitialBufferSize); + try { while (true) diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpResourceTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpResourceTests.cs index 5d139736b59..963f20b0689 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpResourceTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpResourceTests.cs @@ -1,6 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using System.Runtime.CompilerServices; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; using OpenTelemetry.Proto.Trace.V1; using OpenTelemetry.Resources; @@ -65,4 +66,30 @@ public void ToOtlpResourceTest(bool includeServiceNameInResource) Assert.DoesNotContain(otlpResource.Attributes, kvp => kvp.Key == ResourceSemanticConventions.AttributeServiceName); } } + + [Fact] + public void WriteResourceDoesNotKeepResourceAlive() + { + var reference = CreateSerializedResourceWeakReference(); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + Assert.False(reference.TryGetTarget(out _), "Resource should not be kept alive after serialization."); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static WeakReference CreateSerializedResourceWeakReference() + { + var resource = ResourceBuilder.CreateEmpty() + .AddAttributes([new("key", "value")]) + .Build(); + + var buffer = new byte[1024]; + + _ = ProtobufOtlpResourceSerializer.WriteResource(buffer, 0, resource); + + return new WeakReference(resource); + } }