Skip to content

Commit b317a06

Browse files
committed
Implement Oracle deadlock test using cross-update pattern
Oracle can trigger deadlocks (ORA-00060) using the classic cross-update pattern. Unlike PostgreSQL/SQL Server which roll back the victim's entire transaction, Oracle only rolls back the victim's statement. This requires using Task.WhenAny instead of Task.WhenAll, then explicitly rolling back the victim's transaction to release earlier locks and unblock the other session. https://claude.ai/code/session_01WusLPsCyEsDpSbp2Nm6Tkx
1 parent 415916e commit b317a06

File tree

1 file changed

+47
-4
lines changed

1 file changed

+47
-4
lines changed

EntityFramework.Exceptions/Tests/OracleTests.cs

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
using System.Threading.Tasks;
1+
using System.Linq;
2+
using System.Threading.Tasks;
3+
using EntityFramework.Exceptions.Common;
24
using EntityFramework.Exceptions.Oracle;
35
using Microsoft.EntityFrameworkCore;
46
using Testcontainers.Oracle;
@@ -12,10 +14,51 @@ public OracleTests(OracleTestContextFixture fixture) : base(fixture.DemoContext)
1214
{
1315
}
1416

15-
[Fact(Skip = "Skipping as oracle can't trigger deadlock.")]
16-
public override Task Deadlock()
17+
[Fact]
18+
public override async Task Deadlock()
1719
{
18-
return Task.CompletedTask;
20+
var p1 = DemoContext.Products.Add(new() { Name = "Test1" });
21+
var p2 = DemoContext.Products.Add(new() { Name = "Test2" });
22+
await DemoContext.SaveChangesAsync();
23+
24+
var id1 = p1.Entity.Id;
25+
var id2 = p2.Entity.Id;
26+
27+
await using var controlContext = new DemoContext(DemoContext.Options);
28+
await using var transaction1 = await DemoContext.Database.BeginTransactionAsync();
29+
await using var transaction2 = await controlContext.Database.BeginTransactionAsync();
30+
31+
// Each transaction locks one row
32+
await DemoContext.Products.Where(c => c.Id == id1)
33+
.ExecuteUpdateAsync(c => c.SetProperty(p => p.Name, "Test11"));
34+
await controlContext.Products.Where(c => c.Id == id2)
35+
.ExecuteUpdateAsync(c => c.SetProperty(p => p.Name, "Test21"));
36+
37+
// Start both cross-updates concurrently to create a deadlock cycle
38+
var task1 = Task.Run(() => DemoContext.Products
39+
.Where(c => c.Id == id2)
40+
.ExecuteUpdateAsync(c => c.SetProperty(p => p.Name, "Test22")));
41+
var task2 = Task.Run(() => controlContext.Products
42+
.Where(c => c.Id == id1)
43+
.ExecuteUpdateAsync(c => c.SetProperty(p => p.Name, "Test12")));
44+
45+
// Oracle only rolls back the victim's statement, not its transaction,
46+
// so the non-victim remains blocked. Use WhenAny to catch the victim first.
47+
var completedTask = await Task.WhenAny(task1, task2);
48+
await Assert.ThrowsAsync<DeadlockException>(() => completedTask);
49+
50+
// Roll back the victim's transaction to release its earlier locks
51+
// and unblock the other session.
52+
if (completedTask == task1)
53+
{
54+
await transaction1.RollbackAsync();
55+
await task2;
56+
}
57+
else
58+
{
59+
await transaction2.RollbackAsync();
60+
await task1;
61+
}
1962
}
2063
}
2164

0 commit comments

Comments
 (0)