Async multi-map records cache reuse on first execution, preventing eviction of one-off SQL, occasionally leading to OOM
@Nacl-HCl arbeitet bereits daran.
Seit 15.10.2026.
Bewertung
- Schwierigkeit
- 3/5
- Geschätzter Aufwand
- 1-2 Tage
- Anfängerfreundlichkeit
- 58/100
Rechercherichtung
Start by tracing the async multi-map path through MultiMapAsync and MultiMapImpl, then inspect how GetCacheInfo records cache hits. Run the provided .NET 8 reproduction and compare the first-execution hit counts for single-type and multi-map queries. Done when a first async multi-map execution records zero reuse, repeated executions count as reuse, and one-off entries remain eligible for normal cleanup.
Vom Indexierungsmodell aus dem Issue-Text verfasst.
Beschreibung
Reproduced with Dapper 2.1.66 and the released NuGet package 2.1.89, targeting .NET 8 with Microsoft.Data.Sqlite 8.0.11.
Problem
An async multi-map query executed exactly once already has a cache hit count of 1. In contrast, a single-type async query executed once has a hit count of 0. Because CollectCacheGarbage only removes entries with hitCount <= 0, unique SQL statements executed once through async multi-map survive automatic cleanup. In some cases this may leade to Out-of-Memory issues (example when parametrizing manually or using string interpolation)
This report concerns cache reuse accounting, rather than requesting a cache size limit or automatic SQL normalization. Parameterizing changing values is an application workaround, but a single execution should not count as reuse.
Minimal reproduction
Create an isolated console project with the following files and run dotnet run -c Release.
Repro.csproj:
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net8.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings></PropertyGroup><ItemGroup><PackageReference Include="Dapper" Version="2.1.89"/><PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.11"/></ItemGroup></Project>
Program.cs:
using Dapper;
using Microsoft.Data.Sqlite;
using System.Reflection;
Console.WriteLine("Dapper " + typeof(SqlMapper).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion);
using var connection = new SqliteConnection("Data Source=:memory:");
connection.Open();
foreach (var mode in new[] { "single-async", "multi-async", "multi-async-stable" })
{
SqlMapper.PurgeQueryCache();
for (var i = 0; i < 1200; i++)
{
var sql = "SELECT 1 AS A, 2 AS B, 3 AS C WHERE 0 /*" + (mode == "multi-async-stable" ? 0 : i) + "*/";
if (mode == "single-async") await connection.QueryAsync<First>(sql);
else await connection.QueryAsync<First, Second, Third, int>(sql, (a,b,c) => a.A+b.B+c.C, splitOn: "B,C");
if (i == 0) Console.WriteLine($"{mode}: after ONE execution entries={SqlMapper.GetCachedSQLCount()}, hits={string.Join(',', SqlMapper.GetCachedSQL().Select(x => x.Item3))}");
}
Console.WriteLine($"{mode}: after 1200 executions entries={SqlMapper.GetCachedSQLCount()}, minHits={SqlMapper.GetCachedSQL().Min(x => x.Item3)}");
}
public class First { public int A { get; set; } }
public class Second { public int B { get; set; } }
public class Third { public int C { get; set; } }
The changing SQL comment deliberately creates distinct identities without changing query semantics. The query returns zero rows, so result retention is not involved.
Actual output with 2.1.89
Dapper 2.1.89+eb47546a40
single-async: after ONE execution entries=1, hits=0
single-async: after 1200 executions entries=201, minHits=0
multi-async: after ONE execution entries=1, hits=1
multi-async: after 1200 executions entries=1200, minHits=1
multi-async-stable: after ONE execution entries=1, hits=1
multi-async-stable: after 1200 executions entries=1, minHits=2399
The exact number of remaining single-type entries depends on the insertion counter; the important difference is that cleanup removes one-off single-type entries while all 1,200 one-off async multi-map entries remain with hitCount=1.
Expected behavior
The first execution should leave reuse count 0 for async multi-map too. A subsequent execution of the same identity should increment it once. One-off identities should remain eligible for normal cleanup.
Suspected cause
MultiMapAsync calls GetCacheInfo when preparing the command, then MultiMapImpl calls GetCacheInfo again for the same identity while mapping the reader. The second internal lookup invokes RecordHit even though the application has executed the SQL only once. This is two internal lookups producing one synthetic reuse count, not two database executions.
An isolated source experiment passing the already-obtained CacheInfo from MultiMapAsync into MultiMapImpl made first-execution hitCount zero and allowed one-off entries to be collected. Repeated execution of stable SQL retained its cache entry and counted actual reuse. This is a candidate approach, not a validated upstream patch.
Related reports
- #1934 reports query-cache memory growth, but does not demonstrate the async multi-map false reuse count.
- #1022 discusses whether the minimum hit threshold should collect one-off queries; the maintainer explains that the count is intended to represent reuse.
- #2218 identifies the same second lookup in the context of a different cache-eviction race and missing parameter readers. It may be useful to address both concerns together.
Could the async multi-map path reuse its existing CacheInfo without recording a second lookup as an application reuse?
- Vorherrschende Sprache
- C#
- Sterne
- 18.4k
- Forks
- 3.7k
- PR-Merge-Kennzahlen
- Keine gemergten PRs in 30 T.
Entwicklungsumgebung
Dieses Projekt bietet weder Dev-Container noch Dockerfile noch Beitragsleitfaden – die Einrichtung liegt bei Ihnen. Beginnen Sie mit der README; die allgemeinen Schritte stehen in unserem Leitfaden für den ersten Beitrag.
Erste Schritte
- Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
- Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
- Forken Sie das Repository und arbeiten Sie in einem Branch.
- Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.
Mehr aus DapperLib/Dapper
-
Schwierigkeit 1/5 Unter einer Stunde Anfängerfreundlichkeit 68/100
-
Dapper.Rainbow uses DbConnection on Init instead of IDbConnectionEvtl. vergeben @Khaos66 hat das vor 1589 Tagen übernommen. Offen
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 68/100
-
Schwierigkeit 1/5 Unter einer Stunde Anfängerfreundlichkeit 78/100
-
Dapper.StrongName 2.1.86 fails strong-name signature verification (all TFMs) — works in 2.1.79Offen
Schwierigkeit 3/5 1-2 Tage Anfängerfreundlichkeit 65/100
-
Schwierigkeit 5/5 Über eine Woche Anfängerfreundlichkeit 30/100
Alle Issues in DapperLib/Dapper
Ähnliche Issues
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 72/100
dotnet/wpf#12108 · 3 Reaktionen ·
Maintainer antworten meist innerhalb von 1 Tag
-
:watch: Not Triaged dotnet/svc
Schwierigkeit 1/5 Unter einer Stunde Anfängerfreundlichkeit 78/100
Maintainer antworten meist innerhalb von 1 Tag
-
area:build type:chore
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 76/100
Maintainer antworten meist innerhalb von 1 Tag
-
csharp good first issue
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 88/100
Maintainer antworten meist innerhalb von 1 Tag
-
python triage
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 78/100
microsoft/semantic-kernel#14753 ·
Maintainer antworten meist innerhalb von 5 Tagen