fix(concurrency): close 8 race / thread-safety findings across CD, DCL, SR
CD-015: rewrite NotificationOutboxRepository.InsertIfNotExistsAsync as raw-SQL IF NOT EXISTS … INSERT with SqlException 2601/2627 catch, ending the at-least-once livelock on the site→central notification handoff. DCL-018/019/020/021/022: add _subscribesInFlight guard so concurrent same-tag subscribes don't orphan an adapter handle; delete the latent dead _subscriptionHandles dictionary; stop double-counting _totalSubscribed when an unresolved tag is promoted via another instance; release adapter handles on mid-flight unsubscribe; gate the tag-resolution retry timer with IsTimerActive so subscribe bursts don't reset it into starvation. SR-020: add _terminatingActorsByName shadow so a third deploy arriving during a pending redeploy doesn't crash on InvalidActorNameException — displaced senders get a Failed/superseded response and the latest command wins on Terminated. SR-024: split OperationTrackingStore reads from writes (fresh SqliteConnection per GetStatusAsync) so long writes don't block status queries; rewrite Dispose to drop the sync-over-async bridge that could deadlock on a non-reentrant SyncContext; Interlocked.Exchange makes the dispose-once flag race-safe across both paths.
This commit is contained in:
@@ -419,32 +419,10 @@ public class NotificationOutboxRepositoryTests : IDisposable
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InsertIfNotExistsAsync_NewRow_InsertsAndReturnsTrue()
|
||||
{
|
||||
var id = Guid.NewGuid().ToString();
|
||||
|
||||
var inserted = await _repository.InsertIfNotExistsAsync(MakeNotification(id));
|
||||
|
||||
Assert.True(inserted);
|
||||
_context.ChangeTracker.Clear();
|
||||
Assert.NotNull(await _context.Notifications.FindAsync(id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InsertIfNotExistsAsync_DuplicateId_ReturnsFalseAndLeavesExistingRow()
|
||||
{
|
||||
var id = Guid.NewGuid().ToString();
|
||||
await _repository.InsertIfNotExistsAsync(MakeNotification(id, subject: "Original"));
|
||||
_context.ChangeTracker.Clear();
|
||||
|
||||
var inserted = await _repository.InsertIfNotExistsAsync(MakeNotification(id, subject: "Changed"));
|
||||
|
||||
Assert.False(inserted);
|
||||
_context.ChangeTracker.Clear();
|
||||
var loaded = await _context.Notifications.FindAsync(id);
|
||||
Assert.Equal("Original", loaded!.Subject);
|
||||
}
|
||||
// InsertIfNotExistsAsync coverage lives in
|
||||
// tests/ScadaLink.ConfigurationDatabase.Tests/Repositories/NotificationOutboxRepositoryIntegrationTests.cs
|
||||
// — the method is raw-SQL (IF NOT EXISTS … INSERT) so it must execute against
|
||||
// SQL Server, not the SQLite in-memory provider this class uses.
|
||||
|
||||
[Fact]
|
||||
public async Task GetDueAsync_ReturnsPendingAndDueRetrying_OrderedByCreatedAt_CappedAtBatchSize()
|
||||
|
||||
Reference in New Issue
Block a user