Fence body commits by demand generation

This commit is contained in:
Meta Vens 2026-08-19 14:27:22 +02:00
commit 3cec5463b2
2 changed files with 62 additions and 0 deletions

View file

@ -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

View file

@ -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()
{