forked from npgsql/npgsql
-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathPreparedStatementManager.cs
More file actions
304 lines (261 loc) · 12.5 KB
/
PreparedStatementManager.cs
File metadata and controls
304 lines (261 loc) · 12.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using Microsoft.Extensions.Logging;
using Npgsql.Internal;
namespace Npgsql;
sealed class PreparedStatementManager
{
internal int MaxAutoPrepared { get; }
internal int UsagesBeforePrepare { get; }
internal Dictionary<string, PreparedStatement> BySql { get; } = new();
internal PreparedStatement?[] AutoPrepared { get; }
readonly PreparedStatement?[] _candidates;
/// <summary>
/// Total number of current prepared statements (whether explicit or automatic).
/// </summary>
internal int NumPrepared;
readonly NpgsqlConnector _connector;
internal string NextPreparedStatementName() => "_p" + (++_preparedStatementIndex);
ulong _preparedStatementIndex;
readonly ILogger _commandLogger;
internal const int CandidateCount = 100;
internal PreparedStatementManager(NpgsqlConnector connector)
{
_connector = connector;
_commandLogger = connector.LoggingConfiguration.CommandLogger;
MaxAutoPrepared = connector.Settings.MaxAutoPrepare;
UsagesBeforePrepare = connector.Settings.AutoPrepareMinUsages;
if (MaxAutoPrepared > 0)
{
if (MaxAutoPrepared > 256)
_commandLogger.LogWarning($"{nameof(MaxAutoPrepared)} is over 256, performance degradation may occur. Please report via an issue.");
AutoPrepared = new PreparedStatement[MaxAutoPrepared];
_candidates = new PreparedStatement[CandidateCount];
}
else
{
AutoPrepared = null!;
_candidates = null!;
}
}
internal PreparedStatement? GetOrAddExplicit(NpgsqlBatchCommand batchCommand)
{
var sql = batchCommand.FinalCommandText!;
PreparedStatement? statementBeingReplaced = null;
if (BySql.TryGetValue(sql, out var pStatement))
{
Debug.Assert(pStatement.State != PreparedState.Unprepared);
// If statement is invalidated, fall through below where we replace it with another
if (pStatement.IsExplicit && pStatement.State != PreparedState.Invalidated)
{
// Great, we've found an explicit prepared statement.
// We just need to check that the parameter types correspond, since prepared statements are
// only keyed by SQL (to prevent pointless allocations). If we have a mismatch, simply run unprepared.
return pStatement.DoParametersMatch(batchCommand.CurrentParametersReadOnly)
? pStatement
: null;
}
// We've found an autoprepare statement (candidate or otherwise)
switch (pStatement.State)
{
case PreparedState.NotPrepared:
// Found a candidate for autopreparation. Remove it and prepare explicitly.
RemoveCandidate(pStatement);
break;
// The statement is invalidated. Just replace it with a new one.
case PreparedState.Invalidated:
// The statement has already been autoprepared. We need to "promote" it to explicit.
case PreparedState.Prepared:
statementBeingReplaced = pStatement;
break;
case PreparedState.Unprepared:
throw new InvalidOperationException($"Found unprepared statement in {nameof(PreparedStatementManager)}");
default:
throw new ArgumentOutOfRangeException();
}
}
// Statement hasn't been prepared yet
return BySql[sql] = PreparedStatement.CreateExplicit(this, sql, NextPreparedStatementName(), batchCommand.CurrentParametersReadOnly, statementBeingReplaced);
}
internal PreparedStatement? TryGetAutoPrepared(NpgsqlBatchCommand batchCommand)
{
var sql = batchCommand.FinalCommandText!;
// We could also test for PreparedState.BeingPrepared as it's handled the exact same way as PreparedState.Prepared
// But since it's so rare we'll just go through the slow path
if (!BySql.TryGetValue(sql, out var pStatement) || pStatement.State != PreparedState.Prepared)
return TryGetAutoPreparedSlow(batchCommand, pStatement);
// The statement has already been prepared (explicitly or automatically)
// We just need to check that the parameter types correspond, since prepared statements are
// only keyed by SQL (to prevent pointless allocations). If we have a mismatch, simply run unprepared.
if (!pStatement.DoParametersMatch(batchCommand.CurrentParametersReadOnly))
return null;
// Prevent this statement from being replaced within this batch
pStatement.LastUsed = long.MaxValue;
return pStatement;
PreparedStatement? TryGetAutoPreparedSlow(NpgsqlBatchCommand batchCommand, PreparedStatement? pStatement)
{
var sql = batchCommand.FinalCommandText!;
if (pStatement is null)
{
// New candidate. Find an empty candidate slot or eject a least-used one.
int slotIndex = -1, leastUsages = int.MaxValue;
var lastUsed = long.MaxValue;
for (var i = 0; i < _candidates.Length; i++)
{
var candidate = _candidates[i];
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
// ReSharper disable HeuristicUnreachableCode
if (candidate == null) // Found an unused candidate slot, return immediately
{
slotIndex = i;
break;
}
// ReSharper restore HeuristicUnreachableCode
if (candidate.Usages < leastUsages)
{
leastUsages = candidate.Usages;
slotIndex = i;
lastUsed = candidate.LastUsed;
}
else if (candidate.Usages == leastUsages && candidate.LastUsed < lastUsed)
{
slotIndex = i;
lastUsed = candidate.LastUsed;
}
}
var leastUsed = _candidates[slotIndex];
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
if (leastUsed != null)
BySql.Remove(leastUsed.Sql);
pStatement = BySql[sql] = _candidates[slotIndex] = PreparedStatement.CreateAutoPrepareCandidate(this, sql);
}
switch (pStatement.State)
{
case PreparedState.NotPrepared:
case PreparedState.Invalidated:
break;
// We shouldn't ever get PreparedState.Prepared since it's handled above but handle it here just in case
case PreparedState.Prepared:
case PreparedState.BeingPrepared:
// The statement has already been prepared (explicitly or automatically), or has been selected
// for preparation (earlier identical statement in the same command).
// We just need to check that the parameter types correspond, since prepared statements are
// only keyed by SQL (to prevent pointless allocations). If we have a mismatch, simply run unprepared.
if (!pStatement.DoParametersMatch(batchCommand.CurrentParametersReadOnly))
return null;
// Prevent this statement from being replaced within this batch
pStatement.LastUsed = long.MaxValue;
return pStatement;
case PreparedState.BeingUnprepared:
// The statement is being replaced by an earlier statement in this same batch.
return null;
default:
Debug.Fail($"Unexpected {nameof(PreparedState)} in auto-preparation: {pStatement.State}");
break;
}
if (++pStatement.Usages < UsagesBeforePrepare)
{
// Statement still hasn't passed the usage threshold, no automatic preparation.
// Return null for unprepared execution.
pStatement.RefreshLastUsed();
return null;
}
// Bingo, we've just passed the usage threshold, statement should get prepared
LogMessages.AutoPreparingStatement(_commandLogger, sql, _connector.Id);
// Look for either an empty autoprepare slot, or the least recently used prepared statement which we'll replace it.
var oldestLastUsed = long.MaxValue;
var selectedIndex = -1;
for (var i = 0; i < AutoPrepared.Length; i++)
{
var slot = AutoPrepared[i];
if (slot is null or { State: PreparedState.Invalidated })
{
// We found a free or invalidated slot, exit the loop immediately
selectedIndex = i;
break;
}
switch (slot.State)
{
case PreparedState.Prepared:
if (slot.LastUsed < oldestLastUsed)
{
selectedIndex = i;
oldestLastUsed = slot.LastUsed;
}
break;
case PreparedState.BeingPrepared:
// Slot has already been selected for preparation by an earlier statement in this batch. Skip it.
continue;
default:
ThrowHelper.ThrowInvalidOperationException($"Invalid {nameof(PreparedState)} state {slot.State} encountered when scanning prepared statement slots");
return null;
}
}
if (selectedIndex < 0)
{
// We're here if we couldn't find a free slot or a prepared statement to replace - this means all slots are taken by
// statements being prepared in this batch.
return null;
}
if (pStatement.State != PreparedState.Invalidated)
RemoveCandidate(pStatement);
var oldPreparedStatement = AutoPrepared[selectedIndex];
if (oldPreparedStatement is null)
{
pStatement.Name = Encoding.ASCII.GetBytes("_auto" + selectedIndex);
}
else
{
// When executing an invalidated prepared statement, the old and the new statements are the same instance.
// Create a copy so that we have two distinct instances with their own states.
if (oldPreparedStatement == pStatement)
{
oldPreparedStatement = new PreparedStatement(this, oldPreparedStatement.Sql, isExplicit: false)
{
Name = oldPreparedStatement.Name
};
}
pStatement.Name = oldPreparedStatement.Name;
pStatement.State = PreparedState.NotPrepared;
pStatement.StatementBeingReplaced = oldPreparedStatement;
oldPreparedStatement.State = PreparedState.BeingUnprepared;
}
pStatement.AutoPreparedSlotIndex = selectedIndex;
AutoPrepared[selectedIndex] = pStatement;
// Make sure this statement isn't replaced by a later statement in the same batch.
pStatement.LastUsed = long.MaxValue;
// Note that the parameter types are only set at the moment of preparation - in the candidate phase
// there's no differentiation between overloaded statements, which are a pretty rare case, saving
// allocations.
pStatement.SetParamTypes(batchCommand.CurrentParametersReadOnly);
return pStatement;
}
}
void RemoveCandidate(PreparedStatement candidate)
{
var i = 0;
for (; i < _candidates.Length; i++)
{
if (_candidates[i] == candidate)
{
_candidates[i] = null;
return;
}
}
Debug.Assert(i < _candidates.Length);
}
internal void ClearAll()
{
BySql.Clear();
NumPrepared = 0;
_preparedStatementIndex = 0;
if (AutoPrepared is not null)
for (var i = 0; i < AutoPrepared.Length; i++)
AutoPrepared[i] = null;
if (_candidates != null)
for (var i = 0; i < _candidates.Length; i++)
_candidates[i] = null;
}
}