Skip to content

Commit 12e1743

Browse files
authored
fix: When sql explain plan generation fails, don't attempt to generate an explain plan on future invocations of the same query. (#3075)
1 parent 27e6946 commit 12e1743

10 files changed

Lines changed: 543 additions & 6 deletions

File tree

src/Agent/NewRelic/Agent/Core/Database/CacheByDatastoreVendor.cs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace NewRelic.Agent.Core.Database
1010
{
1111
public class CacheByDatastoreVendor<TKey, TValue> where TValue : class
1212
{
13-
// The capcity defaults to 1000 but can be configured using the SqlStatementCacheMaxSize setting in the local newrelic.config.
13+
// The capacity defaults to 1000 but can be configured using the SqlStatementCacheMaxSize setting in the local newrelic.config.
1414
private int _capacity = 1000;
1515
private readonly SimpleCache<TKey, TValue>[] _caches;
1616

@@ -29,13 +29,22 @@ public TValue GetOrAdd(DatastoreVendor vendor, TKey key, Func<TValue> valueFunc)
2929
return _caches[(int)vendor].GetOrAdd(key, valueFunc);
3030
}
3131

32+
public bool TryAdd(DatastoreVendor vendor, TKey key, Func<TValue> valueFunc)
33+
{
34+
return _caches[(int)vendor].TryAdd(key, valueFunc);
35+
}
36+
public bool Contains(DatastoreVendor vendor, TKey key)
37+
{
38+
return _caches[(int)vendor].Contains(key);
39+
}
40+
3241
public void SetCapacity(int capacity)
3342
{
3443
if (capacity != Capacity)
3544
{
3645
var oldCapacity = Capacity;
3746
Capacity = capacity;
38-
Log.Info($"The capcity of cache type ({GetType()}) has been modified from {oldCapacity} to {Capacity}. Agent's memory allocation will be affected by this change so use with precaution.");
47+
Log.Info($"The capacity of cache type ({GetType()}) has been modified from {oldCapacity} to {Capacity}. Agent's memory allocation will be affected by this change so use with caution.");
3948
}
4049
}
4150

@@ -51,5 +60,13 @@ private int Capacity
5160
}
5261
}
5362
}
63+
64+
public void Reset()
65+
{
66+
for (var i = 0; i < Enum.GetValues(typeof(DatastoreVendor)).Length; i++)
67+
{
68+
_caches[i].Reset();
69+
}
70+
}
5471
}
5572
}

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

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
using System;
1818
using System.Collections.Generic;
1919
using System.Text;
20+
using NewRelic.Agent.Extensions.Collections;
2021
using static NewRelic.Agent.Core.WireModels.MetricWireModel;
2122

2223
namespace NewRelic.Agent.Core.Segments
@@ -51,8 +52,17 @@ public class DatastoreSegmentData : AbstractSegmentData, IDatastoreSegmentData
5152
public ExplainPlan ExplainPlan => _explainPlan;
5253

5354
private readonly IDatabaseService _databaseService;
55+
private static CacheByDatastoreVendor<string, string> _failedExplainPlanQueryCacheByDatastoreVendor;
5456

55-
public DatastoreSegmentData(IDatabaseService databaseService, ParsedSqlStatement parsedSqlStatement, string commandText = null, ConnectionInfo connectionInfo = null, IDictionary<string, IConvertible> queryParameters = null)
57+
static DatastoreSegmentData()
58+
{
59+
_failedExplainPlanQueryCacheByDatastoreVendor = new CacheByDatastoreVendor<string, string>("FailedExplainPlanQueryCache");
60+
_failedExplainPlanQueryCacheByDatastoreVendor.SetCapacity(1000);
61+
}
62+
63+
public DatastoreSegmentData(IDatabaseService databaseService,
64+
ParsedSqlStatement parsedSqlStatement, string commandText = null, ConnectionInfo connectionInfo = null,
65+
IDictionary<string, IConvertible> queryParameters = null)
5666
{
5767
_databaseService = databaseService;
5868
_connectionInfo = connectionInfo ?? EmptyConnectionInfo;
@@ -120,11 +130,15 @@ internal override IEnumerable<KeyValuePair<string, object>> Finish()
120130
}
121131

122132

123-
public void ExecuteExplainPlan(SqlObfuscator obfuscator)
133+
public bool ExecuteExplainPlan(SqlObfuscator obfuscator)
124134
{
135+
// don't attempt to generate an explain plan if this query has failed previously
136+
if (_failedExplainPlanQueryCacheByDatastoreVendor.Contains(DatastoreVendorName, CommandText))
137+
return false;
138+
125139
// Don't re-run an explain plan if one already exists
126140
if (_explainPlan != null)
127-
return;
141+
return false;
128142

129143
try
130144
{
@@ -145,10 +159,15 @@ public void ExecuteExplainPlan(SqlObfuscator obfuscator)
145159
_explainPlan = new ExplainPlan(explainPlan.ExplainPlanHeaders, explainPlan.ExplainPlanDatas, explainPlan.ObfuscatedHeaders);
146160
}
147161
}
162+
163+
return true;
148164
}
149165
catch (Exception exception)
150166
{
151-
Log.Debug(exception, "Unable to execute explain plan");
167+
Log.Debug(exception, "Unable to execute explain plan for query: {Query}. This query will be ignored for future explain plan execution.",
168+
obfuscator.GetObfuscatedSql(CommandText, DatastoreVendorName) ?? "[redacted]");
169+
_failedExplainPlanQueryCacheByDatastoreVendor.TryAdd(DatastoreVendorName, CommandText, () => string.Empty); // all we really want to cache is the query text
170+
return false;
152171
}
153172
}
154173

@@ -239,5 +258,21 @@ public void SetConnectionInfo(ConnectionInfo connInfo)
239258
{
240259
_connectionInfo = connInfo;
241260
}
261+
262+
/// <summary>
263+
/// FOR UNIT TESTING ONLY
264+
/// </summary>
265+
public static void ClearFailedExplainPlanCache()
266+
{
267+
_failedExplainPlanQueryCacheByDatastoreVendor.Reset();
268+
}
269+
270+
/// <summary>
271+
/// FOR UNIT TESTING ONLY
272+
/// </summary>
273+
public CacheByDatastoreVendor<string, string> GetFailedExplainPlanCache()
274+
{
275+
return _failedExplainPlanQueryCacheByDatastoreVendor;
276+
}
242277
}
243278
}

src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Caching/SimpleCache.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ public TValue Peek(TKey key)
6969
return node;
7070
}
7171

72+
/// <summary>
73+
/// Checks whether the specified key exists in the cache without updating stats or affecting the priority of items in the cache.
74+
/// </summary>
75+
public bool Contains(TKey key) => PeekInternal(key) != null;
76+
7277
/// <summary>
7378
/// Allows searching of the cache. If found, returns the existing item and updates the statistics.
7479
/// If not found, returns NULL.
@@ -106,6 +111,17 @@ public TValue GetOrAdd(TKey key, Func<TValue> valueFx)
106111
return result ?? _cacheMap.GetOrAdd(key, x => valueFx());
107112
}
108113

114+
/// <summary>
115+
/// Attempts to add an item to the cache. If the item already exists, returns false. Otherwise, returns true.
116+
/// </summary>
117+
/// <param name="key"></param>
118+
/// <param name="valueFunc">Function to call to obtain the value if the key is not present in the cache.</param>
119+
/// <returns></returns>
120+
public bool TryAdd(TKey key, Func<TValue> valueFunc)
121+
{
122+
return _cacheMap.TryAdd(key, valueFunc());
123+
}
124+
109125
/// <summary>
110126
/// Allows resetting of the Hit, Miss, and Ejection counters
111127
/// </summary>

tests/Agent/IntegrationTests/IntegrationTestHelpers/AgentLogBase.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ public abstract class AgentLogBase
8686
public const string WrapperExceptionLogLineRegex = ErrorLogLinePrefixRegex + "An exception occurred in a wrapper";
8787
public const string ApplicationErrorLogLineRegex = DebugLogLinePrefixRegex + "Noticed application error";
8888

89+
// explain plan failure
90+
public const string ExplainPlainFailureLogLineRegex = DebugLogLinePrefixRegex + "Unable to execute explain plan for query: (.*)";
91+
8992
public AgentLogBase(ITestOutputHelper testLogger)
9093
{
9194
_testLogger = testLogger;

tests/Agent/IntegrationTests/SharedApplications/Common/MultiFunctionApplicationHelpers/NetStandardLibraries/MsSql/MicrosoftDataSqlClientExerciser.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using NewRelic.Agent.IntegrationTests.Shared.ReflectionHelpers;
1313
using NewRelic.Api.Agent;
1414
using System.Threading;
15+
using System;
1516

1617
namespace MultiFunctionApplicationHelpers.NetStandardLibraries.MsSql
1718
{
@@ -265,6 +266,63 @@ public void Wait(int millisecondsTimeOut)
265266
Thread.Sleep(millisecondsTimeOut);
266267
}
267268

269+
#if NET9_0
270+
[LibraryMethod]
271+
public async Task MsSqlCreateStoredProcWithTempTable(string procedureName)
272+
{
273+
var createProcedure = $@"
274+
CREATE OR ALTER PROCEDURE {procedureName}
275+
AS
276+
BEGIN
277+
-- Create a temporary table and insert data into it using SELECT INTO
278+
SELECT
279+
Id,
280+
FirstName
281+
INTO #TempTable
282+
FROM
283+
TeamMembers;
284+
285+
-- Select all rows from the temporary table
286+
SELECT * FROM #TempTable;
287+
END;";
288+
289+
await using var connection = new SqlConnection(_connectionString);
290+
await connection.OpenAsync();
291+
292+
await using var command = new SqlCommand(createProcedure, connection);
293+
await command.ExecuteNonQueryAsync();
294+
295+
}
296+
297+
[LibraryMethod]
298+
[Transaction]
299+
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
300+
public async Task MsSqlStoredProcWithTempTable(string procedureName)
301+
{
302+
await using var connection = new SqlConnection(_connectionString);
303+
await connection.OpenAsync();
304+
305+
await using var command = connection.CreateCommand();
306+
command.CommandText = procedureName;
307+
command.CommandType = System.Data.CommandType.StoredProcedure;
308+
309+
// execute the command
310+
await using var reader = await command.ExecuteReaderAsync();
311+
312+
}
313+
314+
[LibraryMethod]
315+
public async Task MsSqlDropStoredProcWithTempTable(string procedureName)
316+
{
317+
var dropProcedureSql = string.Format(DropProcedureSql, procedureName);
318+
await using var connection = new SqlConnection(_connectionString);
319+
await connection.OpenAsync();
320+
321+
await using var command = new SqlCommand(dropProcedureSql, connection);
322+
await command.ExecuteNonQueryAsync();
323+
}
324+
#endif
325+
268326
private void EnsureProcedure(string procedureName, DbParameter[] dbParameters)
269327
{
270328
var parameters = string.Join(", ", dbParameters.Select(x => $"{x.ParameterName} {x.DbTypeName}"));
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright 2020 New Relic, Inc. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Text;
8+
using System.Threading.Tasks;
9+
using Microsoft.VisualStudio.TestPlatform.Utilities;
10+
using NewRelic.Agent.IntegrationTestHelpers;
11+
using NewRelic.Agent.IntegrationTestHelpers.RemoteServiceFixtures;
12+
using Xunit;
13+
using Xunit.Abstractions;
14+
15+
namespace NewRelic.Agent.UnboundedIntegrationTests.MsSql
16+
{
17+
public class MsSqlFailedExplainPlanTests : NewRelicIntegrationTest<ConsoleDynamicMethodFixtureCoreLatest>
18+
{
19+
private readonly ConsoleDynamicMethodFixtureCoreLatest _fixture;
20+
21+
public MsSqlFailedExplainPlanTests(ConsoleDynamicMethodFixtureCoreLatest fixture, ITestOutputHelper outputHelper) : base(fixture)
22+
{
23+
var exerciserName = "MicrosoftDataSqlClientExerciser";
24+
25+
_fixture = fixture;
26+
_fixture.TestLogger = outputHelper;
27+
28+
var procedureName = Utilities.GenerateProcedureName();
29+
30+
_fixture.AddCommand($"{exerciserName} MsSqlCreateStoredProcWithTempTable {procedureName}");
31+
_fixture.AddCommand($"{exerciserName} MsSqlStoredProcWithTempTable {procedureName}");
32+
_fixture.AddCommand($"{exerciserName} MsSqlStoredProcWithTempTable {procedureName}");
33+
_fixture.AddCommand($"{exerciserName} MsSqlDropStoredProcWithTempTable {procedureName}");
34+
35+
_fixture.AddActions
36+
(
37+
setupConfiguration: () =>
38+
{
39+
var configPath = fixture.DestinationNewRelicConfigFilePath;
40+
var configModifier = new NewRelicConfigModifier(configPath);
41+
configModifier.ConfigureFasterMetricsHarvestCycle(15);
42+
configModifier.ConfigureFasterTransactionTracesHarvestCycle(15);
43+
configModifier.ConfigureFasterSqlTracesHarvestCycle(15);
44+
45+
configModifier.ForceTransactionTraces();
46+
configModifier.SetLogLevel("finest"); //This has to stay at finest to ensure parameter check security
47+
48+
CommonUtils.ModifyOrCreateXmlAttributeInNewRelicConfig(configPath, new[] { "configuration", "transactionTracer" }, "explainEnabled", "true");
49+
CommonUtils.ModifyOrCreateXmlAttributeInNewRelicConfig(configPath, new[] { "configuration", "transactionTracer" }, "recordSql", "raw");
50+
CommonUtils.ModifyOrCreateXmlAttributeInNewRelicConfig(configPath, new[] { "configuration", "transactionTracer" }, "explainThreshold", "1");
51+
CommonUtils.ModifyOrCreateXmlAttributeInNewRelicConfig(configPath, new[] { "configuration", "datastoreTracer", "queryParameters" }, "enabled", "true");
52+
},
53+
exerciseApplication: () =>
54+
{
55+
_fixture.AgentLog.WaitForLogLine(AgentLogBase.ExplainPlainFailureLogLineRegex, TimeSpan.FromMinutes(1));
56+
}
57+
);
58+
59+
_fixture.Initialize();
60+
61+
}
62+
63+
[Fact]
64+
public void Test()
65+
{
66+
// verify that the log contains one explain plan failure log line - second call to the stored proc doesn't try to generate an explain plan
67+
Assert.Single(_fixture.AgentLog.TryGetLogLines(AgentLogBase.ExplainPlainFailureLogLineRegex));
68+
}
69+
}
70+
}

0 commit comments

Comments
 (0)