Skip to content

Commit 925d016

Browse files
jaffinitonrcventuratippmar-nr
authored
summary: Resolves multiple issues with StackExchange.Redis v2+ profiling (#1504)
fix: Allow StackExchange.Redis v2+ profiling to start outside of a transaction. (#1501 ) fix: Fix a memory leak when using StackExchange.Redis v2+. (#1473) chore: Add more units tests chore: Update logic to make better use of ConcurrentDictionary features. chore: Mark weakTransaction as nullable. --------- Co-authored-by: Chris Ventura <[email protected]> Co-authored-by: Marty Tippin <[email protected]>
1 parent 53affa2 commit 925d016

File tree

6 files changed

+123
-44
lines changed

6 files changed

+123
-44
lines changed

src/Agent/NewRelic/Agent/Core/Agent.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public class Agent : IAgent // any changes to api, update the interface in exten
6363
private readonly ILogEventAggregator _logEventAggregator;
6464
private readonly ILogContextDataFilter _logContextDataFilter;
6565
private Extensions.Logging.ILogger _logger;
66-
private IStackExchangeRedisCache _stackExchangeRedisCache;
66+
private volatile IStackExchangeRedisCache _stackExchangeRedisCache;
6767

6868
public Agent(ITransactionService transactionService, ITransactionTransformer transactionTransformer,
6969
IThreadPoolStatic threadPoolStatic, ITransactionMetricNameMaker transactionMetricNameMaker, IPathHashMaker pathHashMaker,

src/Agent/NewRelic/Agent/Core/Segments/Segment.cs

+8-2
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,16 @@ public Segment(TimeSpan relativeStartTime, TimeSpan? duration, Segment segment,
115115
public bool IsExternal => Data.SpanCategory == SpanCategory.Http;
116116

117117
private string _spanId;
118+
/// <summary>
119+
/// Gets the SpanId for the segment. If SpanId has not been set it will create one.
120+
/// This call could potentially generate more than one Id if GET is called from multiple threads at the same time.
121+
/// Current usage of this property does not do this.
122+
/// </summary>
118123
public string SpanId
119124
{
120125
get
121126
{
127+
// If _spanId is null and this is called rapidly from different threads, the returned value could be different for each.
122128
return _spanId ?? (_spanId = GuidGenerator.GenerateNewRelicGuid());
123129
}
124130
set
@@ -135,9 +141,9 @@ public void End()
135141
var endTime = _transactionSegmentState.GetRelativeTime();
136142
RelativeEndTime = endTime;
137143

138-
if (Agent.Instance.StackExchangeRedisCache != null && Agent.Instance.StackExchangeRedisCache.Count > 0)
144+
if (Agent.Instance.StackExchangeRedisCache != null)
139145
{
140-
Agent.Instance.StackExchangeRedisCache.Harvest(SpanId);
146+
Agent.Instance.StackExchangeRedisCache.Harvest(this);
141147
}
142148

143149
Finish();

src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Helpers/IStackExchangeRedisCache.cs

+2-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
using System;
5+
using NewRelic.Agent.Api;
56

67
namespace NewRelic.Agent.Extensions.Helpers
78
{
@@ -11,8 +12,6 @@ namespace NewRelic.Agent.Extensions.Helpers
1112
/// </summary>
1213
public interface IStackExchangeRedisCache : IDisposable
1314
{
14-
void Harvest(string spanId);
15-
16-
int Count { get; }
15+
void Harvest(ISegment segment);
1716
}
1817
}

src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/StackExchangeRedis2Plus/ConnectWrapperV2.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace NewRelic.Providers.Wrapper.StackExchangeRedis2Plus
1111
{
1212
public class ConnectWrapperV2 : IWrapper
1313
{
14-
public bool IsTransactionRequired => true;
14+
public bool IsTransactionRequired => false;
1515

1616
private const string WrapperName = "stackexchangeredis-connect";
1717

src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/StackExchangeRedis2Plus/SessionCache.cs

+13-37
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public class SessionCache : IStackExchangeRedisCache
1919
{
2020
private readonly EventWaitHandle _stopHandle = new EventWaitHandle(false, EventResetMode.ManualReset);
2121

22-
private readonly ConcurrentDictionary<string, ProfilingSession> _sessionCache = new ConcurrentDictionary<string, ProfilingSession>();
22+
private readonly ConcurrentDictionary<ISegment, ProfilingSession> _sessionCache = new ConcurrentDictionary<ISegment, ProfilingSession>();
2323

2424
private readonly IAgent _agent;
2525

@@ -34,28 +34,26 @@ public SessionCache(IAgent agent, int invocationTargetHashCode)
3434
}
3535

3636
/// <summary>
37-
/// Finishes a profiling session for the segment indicated by the span id and creates a child DataStoreSegment for each command in the session.
37+
/// Finishes a profiling session for the segment and creates a child DataStoreSegment for each command in the session.
3838
/// </summary>
39-
/// <param name="spanId">Span ID of the segment being finalized.</param>
40-
public void Harvest(string spanId)
39+
/// <param name="hostSegment">Segment being finalized.</param>
40+
public void Harvest(ISegment hostSegment)
4141
{
4242
// If we can't remove the session, it doesn't exist, so do nothing and return.
43-
if (!_sessionCache.TryRemove(spanId, out var sessionData))
43+
if (!_sessionCache.TryRemove(hostSegment, out var sessionData))
4444
{
4545
return;
4646
}
4747

4848
// Get the transaction from the session
49-
var transaction = sessionData.UserToken as ITransaction;
50-
51-
// We want to make sure to finish the session even if the transaction is done so that it is not orphaned.
52-
var commands = sessionData.FinishProfiling();
53-
if (transaction.IsFinished)
49+
var weakTransaction = sessionData.UserToken as WeakReference<ITransaction>;
50+
if (!(weakTransaction?.TryGetTarget(out var transaction) ?? false) || transaction.IsFinished)
5451
{
5552
return;
5653
}
5754

5855
var xTransaction = (ITransactionExperimental)transaction;
56+
var commands = sessionData.FinishProfiling();
5957
foreach (var command in commands)
6058
{
6159
// We need to build the relative start and stop time based on the transaction start time.
@@ -107,7 +105,7 @@ public Func<ProfilingSession> GetProfilingSession()
107105

108106
// Don't want to save data to a session outside of a transaction or to a NoOp - no way to clean it up easily or reliably.
109107
var transaction = _agent.CurrentTransaction;
110-
if (!transaction.IsValid)
108+
if (transaction.IsFinished || !transaction.IsValid)
111109
{
112110
return null;
113111
}
@@ -119,43 +117,21 @@ public Func<ProfilingSession> GetProfilingSession()
119117
return null;
120118
}
121119

122-
// Use the spanid of the segment as the key for the cache.
123-
var spanId = segment.SpanId;
124-
if (string.IsNullOrWhiteSpace(spanId))
125-
{
126-
return null;
127-
}
128-
129-
// During async operations, the transaction can get lost and report as NoOp so we store a reference to it in the session.
130-
if (!_sessionCache.TryGetValue(spanId, out var sessionData))
131-
{
132-
sessionData = new ProfilingSession(transaction);
133-
_sessionCache.TryAdd(spanId, sessionData);
134-
}
135-
136-
return sessionData;
120+
return _sessionCache.GetOrAdd(segment, GetProfilingSession);
137121
};
138122
}
139123

140-
public int Count
124+
private ProfilingSession GetProfilingSession(ISegment segment)
141125
{
142-
get
143-
{
144-
return _sessionCache.Count;
145-
}
126+
return new ProfilingSession(new WeakReference<ITransaction>(_agent.CurrentTransaction, false));
146127
}
147128

148129
// Clean up the handles, sessions, and wipe the dictionary.
149130
public void Dispose()
150131
{
151132
_stopHandle.Set();
152-
_stopHandle.Dispose();
153-
foreach (var cachedSession in _sessionCache.Values)
154-
{
155-
_ = cachedSession.FinishProfiling();
156-
}
157-
158133
_sessionCache.Clear();
134+
_stopHandle.Dispose();
159135
}
160136
}
161137
}

tests/Agent/UnitTests/CompositeTests/SegmentsTests.cs

+98
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,82 @@ public void SegmentEndWithExceptionCapturesErrorAttributes()
792792
SpanAssertions.HasAttributes(expectedSpanErrorAttributes, AttributeClassification.AgentAttributes, spanWithError);
793793
}
794794

795+
[Test]
796+
public void SegmentEnd_StackExchangeRedis_EndCreatesSegment()
797+
{
798+
var tx = _agent.CreateTransaction(
799+
isWeb: false,
800+
category: "testing",
801+
transactionDisplayName: "test",
802+
doNotTrackAsUnitOfWork: true);
803+
var segment = _agent.StartStackExchangeRedisDatastoreRequestSegmentOrThrow("set", DatastoreVendor.Redis, TimeSpan.Zero, TimeSpan.FromMilliseconds(1));
804+
segment.EndStackExchangeRedis();
805+
tx.End();
806+
807+
_compositeTestAgent.Harvest();
808+
809+
var spanEvents = _compositeTestAgent.SpanEvents.ToArray();
810+
Assert.AreEqual(2, spanEvents.Length);
811+
812+
var expectedSpanErrorAttributes = new List<ExpectedAttribute>
813+
{
814+
new ExpectedAttribute { Key = "name", Value = "Datastore/operation/Redis/set" },
815+
new ExpectedAttribute { Key = "category", Value = "datastore" },
816+
};
817+
818+
var datastoreSpan= spanEvents[1];
819+
SpanAssertions.HasAttributes(expectedSpanErrorAttributes, AttributeClassification.Intrinsics, datastoreSpan);
820+
}
821+
822+
[Test]
823+
public void SegmentEnd_StackExchangeRedis_SessionCacheIsNull_HarvestNotCalled()
824+
{
825+
_agent.StackExchangeRedisCache = null;
826+
var tx = _agent.CreateTransaction(
827+
isWeb: false,
828+
category: "testing",
829+
transactionDisplayName: "test",
830+
doNotTrackAsUnitOfWork: true);
831+
var segment = _agent.StartCustomSegmentOrThrow("parentSegment");
832+
833+
segment.End();
834+
tx.End();
835+
836+
_compositeTestAgent.Harvest();
837+
838+
var spanEvents = _compositeTestAgent.SpanEvents.ToArray();
839+
Assert.AreEqual(2, spanEvents.Length);
840+
}
841+
842+
[Test]
843+
public void SegmentEnd_StackExchangeRedis_SessionCacheNotNull_HarvestCalled()
844+
{
845+
_agent.StackExchangeRedisCache = new StackExchangeRedisCacheMock(_agent);
846+
var tx = _agent.CreateTransaction(
847+
isWeb: false,
848+
category: "testing",
849+
transactionDisplayName: "test",
850+
doNotTrackAsUnitOfWork: true);
851+
var segment = _agent.StartCustomSegmentOrThrow("parentSegment");
852+
853+
segment.End();
854+
tx.End();
855+
856+
_compositeTestAgent.Harvest();
857+
858+
var spanEvents = _compositeTestAgent.SpanEvents.ToArray();
859+
Assert.AreEqual(3, spanEvents.Length);
860+
861+
var expectedSpanErrorAttributes = new List<ExpectedAttribute>
862+
{
863+
new ExpectedAttribute { Key = "name", Value = "Datastore/operation/Redis/set" },
864+
new ExpectedAttribute { Key = "category", Value = "datastore" },
865+
};
866+
867+
var datastoreSpan = spanEvents[2];
868+
SpanAssertions.HasAttributes(expectedSpanErrorAttributes, AttributeClassification.Intrinsics, datastoreSpan);
869+
}
870+
795871
#region Helper methods
796872

797873
private static TransactionTraceSegment GetFirstSegmentOrThrow()
@@ -810,4 +886,26 @@ private static TransactionTraceSegment GetFirstSegmentOrThrow()
810886

811887
#endregion Helper methods
812888
}
889+
890+
public class StackExchangeRedisCacheMock : NewRelic.Agent.Extensions.Helpers.IStackExchangeRedisCache
891+
{
892+
private IAgent _agent;
893+
894+
public StackExchangeRedisCacheMock(IAgent agent)
895+
{
896+
_agent = agent;
897+
}
898+
899+
public void Dispose()
900+
{
901+
// Do nothing
902+
}
903+
904+
public void Harvest(ISegment segment)
905+
{
906+
// Create a segment if callled so we can test it
907+
var testSegment = _agent.StartStackExchangeRedisDatastoreRequestSegmentOrThrow("set", DatastoreVendor.Redis, TimeSpan.Zero, TimeSpan.FromMilliseconds(1));
908+
testSegment.EndStackExchangeRedis();
909+
}
910+
}
813911
}

0 commit comments

Comments
 (0)