Skip to content

Commit 4957352

Browse files
authored
cached-command should use a backing pool, in particular for async APIs (#150)
1 parent 693b0aa commit 4957352

File tree

6 files changed

+106
-23
lines changed

6 files changed

+106
-23
lines changed

src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -579,30 +579,34 @@ private static void WriteCommandFactory(in GenerateState ctx, string baseFactory
579579
if (additionalCommandState is not null && additionalCommandState.HasCommandProperties)
580580
{
581581
sb.Indent()
582-
.NewLine().Append("var cmd = TryReuse(ref Storage, sql, commandType, args);")
582+
.NewLine().Append("var cmd = TryReuseThreadStatic(ref Storage, sql, commandType, args, _cmdPool);")
583583
.NewLine().Append("if (cmd is null)").Indent()
584584
.NewLine().Append("cmd = base.GetCommand(connection, sql, commandType, args);");
585585
WriteCommandProperties(ctx, sb, "cmd", additionalCommandState.CommandProperties);
586586
sb.Outdent().NewLine().Append("return cmd;").Outdent();
587587
}
588588
else
589589
{
590-
sb.Indent(false).NewLine().Append(" => TryReuse(ref Storage, sql, commandType, args) ?? base.GetCommand(connection, sql, commandType, args);").Outdent(false);
590+
sb.Indent(false).NewLine().Append(" => TryReuseThreadStatic(ref Storage, sql, commandType, args, _cmdPool) ?? base.GetCommand(connection, sql, commandType, args);").Outdent(false);
591591
}
592-
sb.NewLine().NewLine().Append("public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycle(ref Storage, command);").NewLine();
592+
sb.NewLine().NewLine().Append("public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycleThreadStatic(ref Storage, command, _cmdPool);").NewLine();
593593

594594
if (cacheCount == 1)
595595
{
596+
sb.Append("private static readonly DbCommandCache _cmdPool = new();").NewLine();
597+
sb.Append("[global::System.ThreadStatic] // note this works correctly with ref").NewLine();
596598
sb.Append("private static global::System.Data.Common.DbCommand? Storage;").NewLine();
597599
}
598600
else
599601
{
602+
sb.Append("private readonly DbCommandCache _cmdPool = new(); // note: per cache instance").NewLine();
600603
sb.Append("protected abstract ref global::System.Data.Common.DbCommand? Storage {get;}").NewLine().NewLine();
601604

602605
for (int i = 0; i < cacheCount; i++)
603606
{
604607
sb.Append("internal sealed class Cached").Append(i).Append(" : CommandFactory").Append(index).Indent().NewLine()
605608
.Append("protected override ref global::System.Data.Common.DbCommand? Storage => ref s_Storage;").NewLine()
609+
.Append("[global::System.ThreadStatic] // note this works correctly with ref-return").NewLine()
606610
.Append("private static global::System.Data.Common.DbCommand? s_Storage;").NewLine()
607611
.Outdent().NewLine();
608612
}

src/Dapper.AOT/CommandFactory.cs

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Dapper.Internal;
22
using System;
3+
using System.Collections.Concurrent;
34
using System.Data;
45
using System.Data.Common;
56
using System.Diagnostics;
@@ -175,12 +176,53 @@ protected static void SetValueWithDefaultSize(DbParameter parameter, string? val
175176
/// <summary>
176177
/// Provides an opportunity to recycle and reuse command instances
177178
/// </summary>
178-
protected static bool TryRecycle(ref DbCommand? storage, DbCommand command)
179+
protected static bool TryRecycleInterlocked(ref DbCommand? storage, DbCommand command, DbCommandCache? cache = null)
179180
{
180181
// detach and recycle
181182
command.Connection = null;
182183
command.Transaction = null;
183-
return Interlocked.CompareExchange(ref storage, command, null) is null;
184+
if (Interlocked.CompareExchange(ref storage, command, null) is null)
185+
{
186+
return true;
187+
}
188+
return cache is not null && cache.TryPut(command);
189+
}
190+
191+
/// <summary>
192+
/// Provides an opportunity to recycle and reuse command instances
193+
/// </summary>
194+
protected static bool TryRecycleThreadStatic(ref DbCommand? storage, DbCommand command, DbCommandCache? cache = null)
195+
{
196+
// detach and recycle
197+
command.Connection = null;
198+
command.Transaction = null;
199+
if (storage is null)
200+
{
201+
storage = command;
202+
return true;
203+
}
204+
return cache is not null && cache.TryPut(command);
205+
}
206+
207+
208+
/// <summary>
209+
/// A simple store for command re-use.
210+
/// </summary>
211+
protected sealed class DbCommandCache(int capacity = 16)
212+
{
213+
private readonly ConcurrentQueue<DbCommand> store = [];
214+
internal bool TryPut(DbCommand command)
215+
{
216+
if (store.Count < capacity) // not exact - inherent race condition
217+
{
218+
store.Enqueue(command);
219+
return true;
220+
}
221+
return false;
222+
}
223+
224+
internal DbCommand? TryTake()
225+
=> store.TryDequeue(out var cmd) ? cmd : null;
184226
}
185227
}
186228

@@ -252,9 +294,26 @@ public virtual void UpdateParameters(in UnifiedCommand command, T args)
252294
/// <summary>
253295
/// Provides an opportunity to recycle and reuse command instances
254296
/// </summary>
255-
protected DbCommand? TryReuse(ref DbCommand? storage, string sql, CommandType commandType, T args)
297+
protected DbCommand? TryReuseThreadStatic(ref DbCommand? storage, string sql, CommandType commandType, T args, DbCommandCache? cache = null)
298+
{
299+
var cmd = storage ?? cache?.TryTake();
300+
storage = null;
301+
if (cmd is not null)
302+
{
303+
// try to avoid any dirty detection in the setters
304+
if (cmd.CommandText != sql) cmd.CommandText = sql;
305+
if (cmd.CommandType != commandType) cmd.CommandType = commandType;
306+
UpdateParameters(new(cmd), args);
307+
}
308+
return cmd;
309+
}
310+
311+
/// <summary>
312+
/// Provides an opportunity to recycle and reuse command instances
313+
/// </summary>
314+
protected DbCommand? TryReuseInterlocked(ref DbCommand? storage, string sql, CommandType commandType, T args, DbCommandCache? cache = null)
256315
{
257-
var cmd = Interlocked.Exchange(ref storage, null);
316+
var cmd = Interlocked.Exchange(ref storage, null) ?? cache?.TryTake();
258317
if (cmd is not null)
259318
{
260319
// try to avoid any dirty detection in the setters

test/Dapper.AOT.Test/Interceptors/CacheCommand.output.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,20 +118,23 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje
118118

119119
public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection,
120120
string sql, global::System.Data.CommandType commandType, object? args)
121-
=> TryReuse(ref Storage, sql, commandType, args) ?? base.GetCommand(connection, sql, commandType, args);
121+
=> TryReuseThreadStatic(ref Storage, sql, commandType, args, _cmdPool) ?? base.GetCommand(connection, sql, commandType, args);
122122

123-
public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycle(ref Storage, command);
123+
public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycleThreadStatic(ref Storage, command, _cmdPool);
124+
private readonly DbCommandCache _cmdPool = new(); // note: per cache instance
124125
protected abstract ref global::System.Data.Common.DbCommand? Storage {get;}
125126

126127
internal sealed class Cached0 : CommandFactory0
127128
{
128129
protected override ref global::System.Data.Common.DbCommand? Storage => ref s_Storage;
130+
[global::System.ThreadStatic] // note this works correctly with ref-return
129131
private static global::System.Data.Common.DbCommand? s_Storage;
130132

131133
}
132134
internal sealed class Cached1 : CommandFactory0
133135
{
134136
protected override ref global::System.Data.Common.DbCommand? Storage => ref s_Storage;
137+
[global::System.ThreadStatic] // note this works correctly with ref-return
135138
private static global::System.Data.Common.DbCommand? s_Storage;
136139

137140
}
@@ -165,9 +168,11 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje
165168

166169
public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection,
167170
string sql, global::System.Data.CommandType commandType, object? args)
168-
=> TryReuse(ref Storage, sql, commandType, args) ?? base.GetCommand(connection, sql, commandType, args);
171+
=> TryReuseThreadStatic(ref Storage, sql, commandType, args, _cmdPool) ?? base.GetCommand(connection, sql, commandType, args);
169172

170-
public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycle(ref Storage, command);
173+
public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycleThreadStatic(ref Storage, command, _cmdPool);
174+
private static readonly DbCommandCache _cmdPool = new();
175+
[global::System.ThreadStatic] // note this works correctly with ref
171176
private static global::System.Data.Common.DbCommand? Storage;
172177

173178
}

test/Dapper.AOT.Test/Interceptors/CacheCommand.output.netfx.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,20 +118,23 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje
118118

119119
public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection,
120120
string sql, global::System.Data.CommandType commandType, object? args)
121-
=> TryReuse(ref Storage, sql, commandType, args) ?? base.GetCommand(connection, sql, commandType, args);
121+
=> TryReuseThreadStatic(ref Storage, sql, commandType, args, _cmdPool) ?? base.GetCommand(connection, sql, commandType, args);
122122

123-
public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycle(ref Storage, command);
123+
public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycleThreadStatic(ref Storage, command, _cmdPool);
124+
private readonly DbCommandCache _cmdPool = new(); // note: per cache instance
124125
protected abstract ref global::System.Data.Common.DbCommand? Storage {get;}
125126

126127
internal sealed class Cached0 : CommandFactory0
127128
{
128129
protected override ref global::System.Data.Common.DbCommand? Storage => ref s_Storage;
130+
[global::System.ThreadStatic] // note this works correctly with ref-return
129131
private static global::System.Data.Common.DbCommand? s_Storage;
130132

131133
}
132134
internal sealed class Cached1 : CommandFactory0
133135
{
134136
protected override ref global::System.Data.Common.DbCommand? Storage => ref s_Storage;
137+
[global::System.ThreadStatic] // note this works correctly with ref-return
135138
private static global::System.Data.Common.DbCommand? s_Storage;
136139

137140
}
@@ -165,9 +168,11 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje
165168

166169
public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection,
167170
string sql, global::System.Data.CommandType commandType, object? args)
168-
=> TryReuse(ref Storage, sql, commandType, args) ?? base.GetCommand(connection, sql, commandType, args);
171+
=> TryReuseThreadStatic(ref Storage, sql, commandType, args, _cmdPool) ?? base.GetCommand(connection, sql, commandType, args);
169172

170-
public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycle(ref Storage, command);
173+
public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycleThreadStatic(ref Storage, command, _cmdPool);
174+
private static readonly DbCommandCache _cmdPool = new();
175+
[global::System.ThreadStatic] // note this works correctly with ref
171176
private static global::System.Data.Common.DbCommand? Storage;
172177

173178
}

test/Dapper.AOT.Test/Interceptors/CommandProperties.output.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje
199199
public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection,
200200
string sql, global::System.Data.CommandType commandType, object? args)
201201
{
202-
var cmd = TryReuse(ref Storage, sql, commandType, args);
202+
var cmd = TryReuseThreadStatic(ref Storage, sql, commandType, args, _cmdPool);
203203
if (cmd is null)
204204
{
205205
cmd = base.GetCommand(connection, sql, commandType, args);
@@ -212,7 +212,9 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje
212212
return cmd;
213213
}
214214

215-
public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycle(ref Storage, command);
215+
public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycleThreadStatic(ref Storage, command, _cmdPool);
216+
private readonly DbCommandCache _cmdPool = new();
217+
[global::System.ThreadStatic] // note this works correctly with ref
216218
private static global::System.Data.Common.DbCommand? Storage;
217219

218220
}
@@ -256,7 +258,7 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje
256258
public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection,
257259
string sql, global::System.Data.CommandType commandType, object? args)
258260
{
259-
var cmd = TryReuse(ref Storage, sql, commandType, args);
261+
var cmd = TryReuseThreadStatic(ref Storage, sql, commandType, args, _cmdPool);
260262
if (cmd is null)
261263
{
262264
cmd = base.GetCommand(connection, sql, commandType, args);
@@ -269,18 +271,21 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje
269271
return cmd;
270272
}
271273

272-
public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycle(ref Storage, command);
274+
public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycleThreadStatic(ref Storage, command, _cmdPool);
275+
private readonly DbCommandCache _cmdPool = new();
273276
protected abstract ref global::System.Data.Common.DbCommand? Storage {get;}
274277

275278
internal sealed class Cached0 : CommandFactory1
276279
{
277280
protected override ref global::System.Data.Common.DbCommand? Storage => ref s_Storage;
281+
[global::System.ThreadStatic] // note this works correctly with ref-return
278282
private static global::System.Data.Common.DbCommand? s_Storage;
279283

280284
}
281285
internal sealed class Cached1 : CommandFactory1
282286
{
283287
protected override ref global::System.Data.Common.DbCommand? Storage => ref s_Storage;
288+
[global::System.ThreadStatic] // note this works correctly with ref-return
284289
private static global::System.Data.Common.DbCommand? s_Storage;
285290

286291
}

test/Dapper.AOT.Test/Interceptors/CommandProperties.output.netfx.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje
199199
public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection,
200200
string sql, global::System.Data.CommandType commandType, object? args)
201201
{
202-
var cmd = TryReuse(ref Storage, sql, commandType, args);
202+
var cmd = TryReuseThreadStatic(ref Storage, sql, commandType, args, _cmdPool);
203203
if (cmd is null)
204204
{
205205
cmd = base.GetCommand(connection, sql, commandType, args);
@@ -212,7 +212,9 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje
212212
return cmd;
213213
}
214214

215-
public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycle(ref Storage, command);
215+
public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycleThreadStatic(ref Storage, command, _cmdPool);
216+
private readonly DbCommandCache _cmdPool = new();
217+
[global::System.ThreadStatic] // note this works correctly with ref
216218
private static global::System.Data.Common.DbCommand? Storage;
217219

218220
}
@@ -256,7 +258,7 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje
256258
public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection,
257259
string sql, global::System.Data.CommandType commandType, object? args)
258260
{
259-
var cmd = TryReuse(ref Storage, sql, commandType, args);
261+
var cmd = TryReuseThreadStatic(ref Storage, sql, commandType, args, _cmdPool);
260262
if (cmd is null)
261263
{
262264
cmd = base.GetCommand(connection, sql, commandType, args);
@@ -269,18 +271,21 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje
269271
return cmd;
270272
}
271273

272-
public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycle(ref Storage, command);
274+
public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycleThreadStatic(ref Storage, command, _cmdPool);
275+
private readonly DbCommandCache _cmdPool = new();
273276
protected abstract ref global::System.Data.Common.DbCommand? Storage {get;}
274277

275278
internal sealed class Cached0 : CommandFactory1
276279
{
277280
protected override ref global::System.Data.Common.DbCommand? Storage => ref s_Storage;
281+
[global::System.ThreadStatic] // note this works correctly with ref-return
278282
private static global::System.Data.Common.DbCommand? s_Storage;
279283

280284
}
281285
internal sealed class Cached1 : CommandFactory1
282286
{
283287
protected override ref global::System.Data.Common.DbCommand? Storage => ref s_Storage;
288+
[global::System.ThreadStatic] // note this works correctly with ref-return
284289
private static global::System.Data.Common.DbCommand? s_Storage;
285290

286291
}

0 commit comments

Comments
 (0)