|
| 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 | +} |
0 commit comments