Skip to content

Commit bad93ef

Browse files
committed
ownership-lab H-29: census-only orphaned-awaitable seam (OWEN_H29_CENSUS: un-awaited awaitable-returning invocations assigned to locals with their later-reference count, discards and statements, families A owned result / B lifecycle, async / try / lambda flags; 61 lines, no lowering change); fixture Orphan.cs with its output; the Npgsql runtime falsifier program and output (F1-F6); the analyzer reproduction extended with the IDE0059 controls and shapes; runner and record scripts
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Am9eQwzNfbugH72eVKetC2
1 parent 7a13f35 commit bad93ef

13 files changed

Lines changed: 314 additions & 1 deletion

File tree

‎corpus/ownership-lab/h28/analyzers/vt/.editorconfig‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@ dotnet_diagnostic.CA2012.severity = warning
44
dotnet_diagnostic.VSTHRD110.severity = warning
55
dotnet_diagnostic.MA0134.severity = warning
66
dotnet_diagnostic.CS4014.severity = warning
7+
dotnet_diagnostic.IDE0059.severity = warning
8+
csharp_style_unused_value_assignment_preference = discard_variable
9+
dotnet_diagnostic.IDE0058.severity = warning

‎corpus/ownership-lab/h28/analyzers/vt/Repro.cs‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,17 @@ public async Task ScheduleRetryAsync(object envelope, CancellationToken cancella
2020
}
2121
static Task scheduleMessageAsync(object envelope, CancellationToken ct, NpgsqlConnection conn) => Task.CompletedTask;
2222
}
23+
public class Ide0059Controls
24+
{
25+
public void C1() { var unused = 5; } // IDE0059 control: a plain unused assignment
26+
public void C2() { var t2 = Task.CompletedTask; } // IDE0059 control: an unused Task-typed local
27+
public void C3() { var vt3 = new ValueTask<int>(5); } // IDE0059 control: an unused ValueTask struct local
28+
public void C4(NpgsqlConnection conn) { var tx = conn.BeginTransactionAsync(); } // the orphan shape outside async / try
29+
}
30+
public class Ide0059Shapes
31+
{
32+
public async Task C5_async_no_try(NpgsqlConnection conn) { var tx = conn.BeginTransactionAsync(); await Task.Yield(); } // async, no try
33+
public void C6_sync_try_finally(NpgsqlConnection conn) { try { var tx = conn.BeginTransactionAsync(); } finally { conn.Close(); } } // sync, try/finally
34+
public async Task C7_async_try_no_await_after(NpgsqlConnection conn) { try { var tx = conn.BeginTransactionAsync(); } finally { await conn.CloseAsync(); } } // async, try, nothing awaited inside the try
35+
public async Task C8_async_try_await_after(NpgsqlConnection conn) { try { var tx = conn.BeginTransactionAsync(); await Task.Yield(); } finally { await conn.CloseAsync(); } } // the exact shape: async, try, an await after the orphan
36+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Repro.cs(25,28): warning CS0219: The variable 'unused' is assigned but its value is never used
2+
Repro.cs(25,28): warning IDE0059: Unnecessary assignment of a value to 'unused'
3+
Repro.cs(26,28): warning IDE0059: Unnecessary assignment of a value to 't2'
4+
Repro.cs(27,28): warning IDE0059: Unnecessary assignment of a value to 'vt3'
5+
Repro.cs(28,49): warning IDE0059: Unnecessary assignment of a value to 'tx'
6+
Repro.cs(32,68): warning IDE0059: Unnecessary assignment of a value to 'tx'
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>net8.0</TargetFramework><Nullable>disable</Nullable><ImplicitUsings>enable</ImplicitUsings><AnalysisLevel>latest-all</AnalysisLevel><EnforceCodeStyleInBuild>false</EnforceCodeStyleInBuild></PropertyGroup><ItemGroup><PackageReference Include="Npgsql" Version="10.0.3" /><PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15" PrivateAssets="all" /><PackageReference Include="Meziantou.Analyzer" Version="3.0.290" PrivateAssets="all" /></ItemGroup></Project>
1+
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>net8.0</TargetFramework><Nullable>disable</Nullable><ImplicitUsings>enable</ImplicitUsings><AnalysisLevel>latest-all</AnalysisLevel><EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild></PropertyGroup><ItemGroup><PackageReference Include="Npgsql" Version="10.0.3" /><PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15" PrivateAssets="all" /><PackageReference Include="Meziantou.Analyzer" Version="3.0.290" PrivateAssets="all" /></ItemGroup></Project>
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
using Npgsql;
2+
// H-29 runtime falsifier (h29-prereg-v1.json, runtime_falsifier_frozen): what an assigned-but-never-observed BeginTransactionAsync does.
3+
var cs = "Host=/tmp/pgsc;Username=postgres;Database=postgres;Port=5432";
4+
await using var ds = NpgsqlDataSource.Create(cs);
5+
await using (var c0 = await ds.OpenConnectionAsync()) { await using var cmd0 = c0.CreateCommand(); cmd0.CommandText = "drop table if exists h29_test; create table h29_test(value int)"; await cmd0.ExecuteNonQueryAsync(); }
6+
async Task<long> Count() { await using var c = await ds.OpenConnectionAsync(); await using var k = c.CreateCommand(); k.CommandText = "select count(*) from h29_test"; return (long)(await k.ExecuteScalarAsync()); }
7+
// F3 control: a plain insert persists
8+
{
9+
await using var conn = await ds.OpenConnectionAsync();
10+
await using var cmd = conn.CreateCommand(); cmd.CommandText = "insert into h29_test(value) values (1)"; await cmd.ExecuteNonQueryAsync();
11+
await conn.CloseAsync();
12+
}
13+
Console.WriteLine($"F3 control: rows = {await Count()} (expected 1)");
14+
// F1 data loss: the demanding shape (wolverine PostgresqlQueueSender.cs:119): the ValueTask is assigned and never awaited / read
15+
{
16+
await using var conn = await ds.OpenConnectionAsync();
17+
var ignored = conn.BeginTransactionAsync();
18+
Console.WriteLine($"F4 completion: ignored.IsCompleted = {ignored.IsCompleted} (the state change happened inline?)");
19+
await using var cmd = conn.CreateCommand(); cmd.CommandText = "insert into h29_test(value) values (2)"; await cmd.ExecuteNonQueryAsync();
20+
await conn.CloseAsync();
21+
}
22+
Console.WriteLine($"F1 after the orphaned BeginTransactionAsync: rows = {await Count()} (2 = persisted, 1 = the insert was swallowed by the orphaned transaction)");
23+
// F1b the same with a second command before close (is every later command inside the orphan?)
24+
{
25+
await using var conn = await ds.OpenConnectionAsync();
26+
var ignored = conn.BeginTransactionAsync();
27+
await using var cmd = conn.CreateCommand(); cmd.CommandText = "insert into h29_test(value) values (3)"; await cmd.ExecuteNonQueryAsync();
28+
await using var cmd2 = conn.CreateCommand(); cmd2.CommandText = "insert into h29_test(value) values (4)"; await cmd2.ExecuteNonQueryAsync();
29+
await conn.CloseAsync();
30+
}
31+
Console.WriteLine($"F1b two inserts after the orphan: rows = {await Count()} (3 = persisted, 1 = both swallowed)");
32+
// F2 side effect: a second BeginTransactionAsync after the orphan
33+
{
34+
await using var conn = await ds.OpenConnectionAsync();
35+
var ignored = conn.BeginTransactionAsync();
36+
try { await using var tx2 = await conn.BeginTransactionAsync(); Console.WriteLine("F2 second BeginTransactionAsync = OK (no visible side effect)"); }
37+
catch (Exception e) { Console.WriteLine($"F2 second BeginTransactionAsync threw {e.GetType().Name}: {e.Message}"); }
38+
}
39+
// F5 the fix shape: awaited and disposed without commit -> rollback is explicit and expected
40+
{
41+
await using var conn = await ds.OpenConnectionAsync();
42+
await using (var tx = await conn.BeginTransactionAsync()) { await using var cmd = conn.CreateCommand(); cmd.CommandText = "insert into h29_test(value) values (5)"; await cmd.ExecuteNonQueryAsync(); }
43+
await conn.CloseAsync();
44+
}
45+
Console.WriteLine($"F5 awaited transaction disposed without commit: rows = {await Count()} (unchanged = explicit rollback, as documented)");
46+
// F6 the exact shape of the real site (wolverine PostgresqlQueueSender.ScheduleRetryAsync:107-125): an autocommitted delete before the try,
47+
// the orphaned BeginTransactionAsync inside the try, one insert on the same connection, CloseAsync in finally, `await using` disposal after
48+
{
49+
await using (var c1 = await ds.OpenConnectionAsync()) { await using var k1 = c1.CreateCommand(); k1.CommandText = "drop table if exists h29_incoming; create table h29_incoming(id int); insert into h29_incoming values (7)"; await k1.ExecuteNonQueryAsync(); }
50+
await using var conn = await ds.OpenConnectionAsync();
51+
await using (var del = conn.CreateCommand()) { del.CommandText = "delete from h29_incoming where id = 7"; await del.ExecuteNonQueryAsync(); }
52+
try
53+
{
54+
var tx = conn.BeginTransactionAsync(CancellationToken.None);
55+
await using var ins = conn.CreateCommand(); ins.CommandText = "insert into h29_test(value) values (6)"; await ins.ExecuteNonQueryAsync();
56+
}
57+
finally
58+
{
59+
await conn.CloseAsync();
60+
}
61+
}
62+
{
63+
await using var c2 = await ds.OpenConnectionAsync(); await using var k2 = c2.CreateCommand(); k2.CommandText = "select count(*) from h29_incoming"; var incoming = (long)(await k2.ExecuteScalarAsync());
64+
Console.WriteLine($"F6 exact site shape: incoming rows = {incoming} (0 = the autocommitted delete persisted), scheduled rows = {await Count()} (unchanged = the scheduled insert was swallowed: the message is gone from both tables)");
65+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
F3 control: rows = 1 (expected 1)
2+
F4 completion: ignored.IsCompleted = True (the state change happened inline?)
3+
F1 after the orphaned BeginTransactionAsync: rows = 1 (2 = persisted, 1 = the insert was swallowed by the orphaned transaction)
4+
F1b two inserts after the orphan: rows = 1 (3 = persisted, 1 = both swallowed)
5+
F2 second BeginTransactionAsync threw InvalidOperationException: A transaction is already in progress; nested/concurrent transactions aren't supported.
6+
F5 awaited transaction disposed without commit: rows = 1 (unchanged = explicit rollback, as documented)
7+
F6 exact site shape: incoming rows = 0 (0 = the autocommitted delete persisted), scheduled rows = 1 (unchanged = the scheduled insert was swallowed: the message is gone from both tables)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net8.0</TargetFramework><Nullable>disable</Nullable><ImplicitUsings>enable</ImplicitUsings></PropertyGroup><ItemGroup><PackageReference Include="Npgsql" Version="10.0.3" /></ItemGroup></Project>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Data.Common;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Npgsql;
7+
// H-29 fixture: orphaned awaitables and their twins; the method name carries the expectation (family / form / later references).
8+
public class Orphan
9+
{
10+
Task _kept; Stream _s = null;
11+
public async Task O01_B_orphan(NpgsqlConnection conn, CancellationToken ct) { var tx = conn.BeginTransactionAsync(ct); await Task.Yield(); } // local, B, refs 0 (PRIMARY)
12+
public async Task O02_A_orphan(NpgsqlCommand cmd) { var r = cmd.ExecuteReaderAsync(); await Task.Yield(); } // local, A, refs 0 (PRIMARY)
13+
public async Task O03_observed_later(NpgsqlConnection conn, CancellationToken ct) { var tx = conn.BeginTransactionAsync(ct); await using var t = await tx; } // refs 1 (not a candidate)
14+
public void O04_stored(NpgsqlCommand cmd) { var t = cmd.ExecuteNonQueryAsync(); _kept = t; } // refs 1
15+
public async Task O05_passed(NpgsqlCommand cmd) { var t = cmd.ExecuteNonQueryAsync(); await Task.WhenAll(t); } // refs 1
16+
public void O06_discard(NpgsqlConnection conn, CancellationToken ct) { _ = conn.BeginTransactionAsync(ct); } // discard, B
17+
public void O07_statement(NpgsqlConnection conn, CancellationToken ct) { conn.BeginTransactionAsync(ct); } // statement, B
18+
public async Task O08_other_delay() { var d = Task.Delay(1); await Task.Yield(); } // local, OTHER, refs 0
19+
public async Task O09_other_nonquery(NpgsqlCommand cmd) { var n = cmd.ExecuteNonQueryAsync(); await Task.Yield(); } // local, OTHER, refs 0
20+
public async Task O10_awaited(NpgsqlCommand cmd) { var r = await cmd.ExecuteReaderAsync(); await r.DisposeAsync(); } // not an awaitable local (awaited)
21+
public async Task O11_B_configureawait(NpgsqlConnection conn, CancellationToken ct) { var tx = conn.BeginTransactionAsync(ct).ConfigureAwait(false); await Task.Yield(); } // local, B via ConfigureAwait, refs 0 (PRIMARY)
22+
public async Task O12_in_try(NpgsqlConnection conn, CancellationToken ct) { try { var tx = conn.BeginTransactionAsync(ct); await Task.Yield(); } finally { await conn.CloseAsync(); } } // local, B, refs 0, in_try (PRIMARY, the real shape)
23+
public void O13_B_sync_commit(NpgsqlTransaction t) { var c = t.CommitAsync(); } // local, B (non-generic Task), refs 0 (PRIMARY)
24+
public async Task O14_lambda(NpgsqlConnection conn, CancellationToken ct) { Func<Task> f = async () => { var tx = conn.BeginTransactionAsync(ct); await Task.Yield(); }; await f(); } // in_lambda flag
25+
public async Task O15_A_stream(Stream s) { var r = s.ReadAsync(new byte[1], 0, 1); await Task.Yield(); } // local, OTHER (int result), refs 0
26+
public async Task O16_using_local(NpgsqlCommand cmd) { await using var r = await cmd.ExecuteReaderAsync(); } // using local: skipped
27+
}

0 commit comments

Comments
 (0)