Fence body commits by demand generation
This commit is contained in:
parent
21fd4e07eb
commit
3cec5463b2
2 changed files with 62 additions and 0 deletions
|
|
@ -357,6 +357,26 @@ namespace GameCult.Mesh
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes one synchronous capability commit only while the exact routed consumer
|
||||
/// generation remains current. Demand transitions serialize against the callback, so
|
||||
/// either this work linearizes before the transition or it is rejected without running.
|
||||
/// </summary>
|
||||
public bool TryExecuteAtGeneration(string bodyId, long generation, Action action)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(bodyId)) throw new ArgumentException("Body identity is required.", nameof(bodyId));
|
||||
if (generation < 0) throw new ArgumentOutOfRangeException(nameof(generation));
|
||||
if (action == null) throw new ArgumentNullException(nameof(action));
|
||||
lock (_gate)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
var current = _bodyGenerations.TryGetValue(bodyId, out var value) ? value : 0;
|
||||
if (current != generation) return false;
|
||||
action();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private string RoutedConsumerSignature(string bodyId) => string.Join(
|
||||
"\u001e",
|
||||
_demands.Values
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
|
|
@ -347,6 +348,47 @@ public sealed class CultMeshBodyTransportTests
|
|||
tracker.Plan("eve:entity-soa:aetheria.daemon:pilot").Generation.Should().Be(5);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task BodyDemand_GenerationGuardLinearizesCommitAgainstWithdrawal()
|
||||
{
|
||||
const string bodyId = "eve:entity-soa:aetheria.daemon:pilot";
|
||||
using var tracker = new CultMeshBodyDemandTracker();
|
||||
tracker.Observe(Demand("unity-local", "local", sameMachine: true));
|
||||
var activeGeneration = tracker.Plan(bodyId).Generation;
|
||||
using var commitEntered = new ManualResetEventSlim();
|
||||
using var releaseCommit = new ManualResetEventSlim();
|
||||
var committed = false;
|
||||
|
||||
var commit = Task.Run(() => tracker.TryExecuteAtGeneration(
|
||||
bodyId,
|
||||
activeGeneration,
|
||||
() =>
|
||||
{
|
||||
commitEntered.Set();
|
||||
releaseCommit.Wait();
|
||||
committed = true;
|
||||
}));
|
||||
commitEntered.Wait(TimeSpan.FromSeconds(5)).Should().BeTrue();
|
||||
|
||||
var withdrawal = Task.Run(() =>
|
||||
tracker.Observe(Demand("unity-local", "local", sameMachine: true, active: false)));
|
||||
await Task.Delay(50);
|
||||
withdrawal.IsCompleted.Should().BeFalse(
|
||||
"a withdrawal must serialize after an already-linearized synchronous commit");
|
||||
|
||||
releaseCommit.Set();
|
||||
(await commit).Should().BeTrue();
|
||||
await withdrawal;
|
||||
committed.Should().BeTrue();
|
||||
tracker.Plan(bodyId).Generation.Should().BeGreaterThan(activeGeneration);
|
||||
|
||||
var staleCallbackRan = false;
|
||||
tracker.TryExecuteAtGeneration(bodyId, activeGeneration, () => staleCallbackRan = true)
|
||||
.Should().BeFalse();
|
||||
staleCallbackRan.Should().BeFalse(
|
||||
"work prepared for a withdrawn demand generation must be discarded before commit");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BodyDemand_UnknownLocalityFailsTowardNetworkAndUnsupportedDemandCreatesNoRoute()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue