|
6 | 6 |
|
7 | 7 | #nullable enable |
8 | 8 | using System; |
| 9 | +using System.Collections.Generic; |
| 10 | +using System.Linq; |
| 11 | +using System.Threading; |
9 | 12 | using System.Threading.Tasks; |
10 | 13 | using Akka.Actor; |
11 | 14 | using Akka.Configuration; |
@@ -393,86 +396,40 @@ public void LeaseAcquireInReadingState() |
393 | 396 | // TODO this could accumulate senders and reply to all, atm it'll log saying previous action hasn't finished |
394 | 397 | } |
395 | 398 |
|
396 | | - // Regression test: when a write conflict occurs in Granting state and the blob has no owner, |
397 | | - // LeaseAcquired must NOT be sent until the retry write succeeds. Sending it prematurely causes |
398 | | - // the shard to start without localGranted=true and without heartbeats, leading to a shard-level |
399 | | - // split brain if another node takes the lease before the retry completes. |
400 | | - [Fact(DisplayName = "LeaseActor should not send LeaseAcquired prematurely on conflict retry in Granting state")] |
401 | | - public void ShouldNotSendLeaseAcquiredPrematurelyOnConflictRetry() |
| 399 | + // Regression test: when a CAS conflict occurs in Granting state and the blob is unowned, |
| 400 | + // the actor retries the write. If the retry is then stolen by another node, the caller |
| 401 | + // should receive exactly one message: LeaseTaken — not a premature LeaseAcquired. |
| 402 | + [Fact(DisplayName = "LeaseActor should not send premature LeaseAcquired when conflict retry is stolen")] |
| 403 | + public void ShouldNotSendPrematureLeaseAcquiredWhenConflictRetryIsStolen() |
402 | 404 | { |
403 | 405 | RunTest(() => |
404 | 406 | { |
405 | | - // Start acquiring: read returns empty lease |
406 | 407 | UnderTest.Tell(new LeaseActor.Acquire(), Sender); |
407 | 408 | LeaseProbe.ExpectMsg(LeaseName); |
408 | 409 | LeaseProbe.Reply(new LeaseResource("", CurrentVersion, CurrentTime)); |
409 | 410 |
|
410 | | - // First write attempt |
| 411 | + // First write: conflict with unowned blob — triggers retry |
411 | 412 | UpdateProbe.ExpectMsg((OwnerName, CurrentVersion)); |
412 | | - |
413 | | - // Conflict: version moved but no owner (another node wrote and released) |
414 | 413 | var conflictVersion = new ETag((CurrentVersionCount + 3).ToString()); |
415 | 414 | UpdateProbe.Reply( |
416 | 415 | new Left<LeaseResource, LeaseResource>( |
417 | 416 | new LeaseResource("", conflictVersion, CurrentTime))); |
418 | 417 |
|
419 | | - // LeaseAcquired must NOT have been sent yet — the retry hasn't completed |
420 | | - SenderProbe.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); |
421 | | - |
422 | | - // Retry write is issued with the new version |
| 418 | + // Retry write dispatched, then stolen by another node |
423 | 419 | UpdateProbe.ExpectMsg((OwnerName, conflictVersion)); |
424 | | - |
425 | | - // Retry also conflicts, but this time another node owns it |
426 | 420 | var stolenVersion = new ETag((CurrentVersionCount + 5).ToString()); |
427 | 421 | UpdateProbe.Reply( |
428 | 422 | new Left<LeaseResource, LeaseResource>( |
429 | 423 | new LeaseResource("another-node", stolenVersion, CurrentTime))); |
430 | 424 |
|
431 | | - // Now the caller should get LeaseTaken (not a stale LeaseAcquired) |
432 | | - SenderProbe.ExpectMsg<LeaseActor.LeaseTaken>(); |
| 425 | + // Single-sender ordering: if the premature LeaseAcquired was sent, |
| 426 | + // it arrives before LeaseTaken. So the first message tells us everything. |
| 427 | + SenderProbe.ExpectMsg<object>().Should().BeOfType<LeaseActor.LeaseTaken>( |
| 428 | + "caller should receive LeaseTaken, not a premature LeaseAcquired"); |
433 | 429 | Granted.Value.Should().BeFalse(); |
434 | 430 | }); |
435 | 431 | } |
436 | 432 |
|
437 | | - // Verify that conflict retry with no owner eventually succeeds and properly |
438 | | - // transitions to Granted state with heartbeats enabled |
439 | | - [Fact(DisplayName = "LeaseActor should acquire lease after conflict retry succeeds")] |
440 | | - public void ShouldAcquireLeaseAfterConflictRetrySucceeds() |
441 | | - { |
442 | | - RunTest(() => |
443 | | - { |
444 | | - UnderTest.Tell(new LeaseActor.Acquire(), Sender); |
445 | | - LeaseProbe.ExpectMsg(LeaseName); |
446 | | - LeaseProbe.Reply(new LeaseResource("", CurrentVersion, CurrentTime)); |
447 | | - |
448 | | - // First write attempt |
449 | | - UpdateProbe.ExpectMsg((OwnerName, CurrentVersion)); |
450 | | - |
451 | | - // Conflict: version moved but no owner |
452 | | - var conflictVersion = new ETag((CurrentVersionCount + 3).ToString()); |
453 | | - UpdateProbe.Reply( |
454 | | - new Left<LeaseResource, LeaseResource>( |
455 | | - new LeaseResource("", conflictVersion, CurrentTime))); |
456 | | - |
457 | | - // No premature LeaseAcquired |
458 | | - SenderProbe.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); |
459 | | - |
460 | | - // Retry write succeeds |
461 | | - UpdateProbe.ExpectMsg((OwnerName, conflictVersion)); |
462 | | - var grantedVersion = new ETag((CurrentVersionCount + 6).ToString()); |
463 | | - UpdateProbe.Reply( |
464 | | - new Right<LeaseResource, LeaseResource>( |
465 | | - new LeaseResource(OwnerName, grantedVersion, CurrentTime))); |
466 | | - |
467 | | - // NOW LeaseAcquired should be sent |
468 | | - SenderProbe.ExpectMsg<LeaseActor.LeaseAcquired>(); |
469 | | - Granted.Value.Should().BeTrue(); |
470 | | - |
471 | | - // Heartbeat should be running (proves we're in Granted state) |
472 | | - UpdateProbe.ExpectMsg((OwnerName, grantedVersion)); |
473 | | - }); |
474 | | - } |
475 | | - |
476 | 433 | [Fact(DisplayName = "LeaseActor should return lease taken if conflict when updating lease")] |
477 | 434 | public void ReturnLeaseTakenIfConflictWhenUpdatingLease() |
478 | 435 | { |
|
0 commit comments