2.0协议完善加简单交管配置
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
using SimpleCore;
|
||||
using SimpleCore.Compiler;
|
||||
using SimpleCore.PropType;
|
||||
using SimpleCore.Traffic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace StandardScene.Magnetic.Tasking
|
||||
{
|
||||
/// <summary>
|
||||
/// 方案 B′:用任务下发的 siteIds 经 SegmentPlan.Forecast 建立 seqScope/pendingLocks,再段级 TryLock/Leave。
|
||||
/// </summary>
|
||||
internal sealed class Fass2TrafficLocker
|
||||
{
|
||||
private readonly AbstractCar _car;
|
||||
private readonly Action<string> _log;
|
||||
|
||||
public Fass2TrafficLocker(AbstractCar car, Action<string> log)
|
||||
{
|
||||
_car = car ?? throw new ArgumentNullException(nameof(car));
|
||||
_log = log ?? (_ => { });
|
||||
}
|
||||
|
||||
public void PrepareSequence(IReadOnlyList<int> siteIds, int fromIndex)
|
||||
{
|
||||
if (siteIds == null || siteIds.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("traffic plan siteIds is empty");
|
||||
}
|
||||
|
||||
fromIndex = Math.Max(0, Math.Min(fromIndex, siteIds.Count - 1));
|
||||
var startSiteId = siteIds[fromIndex];
|
||||
var startSite = SimpleLib.GetSite(startSiteId);
|
||||
if (startSite == null)
|
||||
{
|
||||
throw new InvalidOperationException($"traffic start site invalid: {startSiteId}");
|
||||
}
|
||||
|
||||
// TrafficReset 会 programs.Clear() 把 now 置空;TryLock 在真正占点前要求 programs.now != null,
|
||||
// 否则抛 Program obsoleted。环线拦截路径仍处在 move 脚本的 actualSendScript 中,需保留/重建锚点。
|
||||
var keepProgram = _car.status.programs.now;
|
||||
|
||||
_log($"prepare reset start={startSiteId}, goal={siteIds[siteIds.Count - 1]}, fromIndex={fromIndex}, route=[{string.Join(",", siteIds)}]");
|
||||
try
|
||||
{
|
||||
_car.TrafficReset(startSite, makeAvailable: true, strict: false);
|
||||
}
|
||||
catch (Exception ex) when (IsStartOccupiedByOtherCar(ex))
|
||||
{
|
||||
// 起点被他车占用时不能 Reset;若本车已持有该站则跳过 Reset 继续建序,否则交给上层等待。
|
||||
if (Array.IndexOf(_car.status.holdingLocks, startSiteId) < 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"traffic start site {startSiteId} occupied by another car", ex);
|
||||
}
|
||||
|
||||
_log($"prepare reset skipped, already holding start={startSiteId}, ex={ex.Message}");
|
||||
}
|
||||
|
||||
EnsureProgramAnchor(keepProgram);
|
||||
|
||||
if (fromIndex >= siteIds.Count - 1)
|
||||
{
|
||||
LogTrafficState("prepare single-site");
|
||||
return;
|
||||
}
|
||||
|
||||
Fass2RouteHelper.ForecastTrafficSequence(_car, siteIds, fromIndex);
|
||||
LogTrafficState("prepare forecast ok");
|
||||
}
|
||||
|
||||
public void RebaseFrom(IReadOnlyList<int> siteIds, int fromIndex, int windowSize)
|
||||
{
|
||||
PrepareSequence(siteIds, fromIndex);
|
||||
EnsureWindowLocked(siteIds, fromIndex, Math.Max(1, windowSize));
|
||||
}
|
||||
|
||||
private static bool IsStartOccupiedByOtherCar(Exception ex)
|
||||
{
|
||||
for (var cur = ex; cur != null; cur = cur.InnerException)
|
||||
{
|
||||
var msg = cur.Message ?? string.Empty;
|
||||
if (msg.IndexOf("already locks it", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
msg.IndexOf("unavailable site", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryLockNext(int siteId)
|
||||
{
|
||||
if (!_car.status.usage.Get().scheduling)
|
||||
{
|
||||
throw new InvalidOperationException("abandoned");
|
||||
}
|
||||
|
||||
if (Array.IndexOf(_car.status.holdingLocks, siteId) >= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_car.status.pendingLocks.Length > 0 && _car.status.pendingLocks[0] != siteId)
|
||||
{
|
||||
// 窗口重入时可能再次请求已锁过的更早站点;交给 EnsureWindowLocked 跳过,避免 UDP 线程抛异常。
|
||||
_log($"lock skip site={siteId}, expect pending0={_car.status.pendingLocks[0]}, holding=[{FormatInts(_car.status.holdingLocks)}]");
|
||||
return false;
|
||||
}
|
||||
|
||||
EnsureProgramAnchor(keepProgram: null);
|
||||
|
||||
if (TrafficControl.TryLock(_car, siteId))
|
||||
{
|
||||
_log($"lock ok site={siteId}, holding=[{FormatInts(_car.status.holdingLocks)}]");
|
||||
return true;
|
||||
}
|
||||
|
||||
_log($"lock wait site={siteId}, blockedBy={FormatBlocked()}, TCStat={FormatTcStat()}");
|
||||
return false;
|
||||
}
|
||||
|
||||
public void LeavePassed(int siteId)
|
||||
{
|
||||
if (_car.status.holdingLocks.Length <= 1)
|
||||
{
|
||||
_log($"leave skip site={siteId}, holding count={_car.status.holdingLocks.Length}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.IndexOf(_car.status.holdingLocks, siteId) < 0)
|
||||
{
|
||||
_log($"leave skip site={siteId}, not in holding");
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureProgramAnchor(keepProgram: null);
|
||||
TrafficControl.Leave(_car, siteId);
|
||||
_log($"leave site={siteId}, holding=[{FormatInts(_car.status.holdingLocks)}]");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放路径上严格位于 keepFromIndex 之前的持锁站点(当前站及前方窗口保留)。
|
||||
/// </summary>
|
||||
public void ReleaseLocksBehind(IReadOnlyList<int> siteIds, int keepFromIndex)
|
||||
{
|
||||
if (siteIds == null || siteIds.Count == 0 || _car.status.holdingLocks.Length <= 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
keepFromIndex = Math.Max(0, Math.Min(keepFromIndex, siteIds.Count - 1));
|
||||
var holdingSnapshot = _car.status.holdingLocks.ToArray();
|
||||
foreach (var siteId in holdingSnapshot)
|
||||
{
|
||||
if (_car.status.holdingLocks.Length <= 1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var index = IndexOfSite(siteIds, siteId);
|
||||
if (index < 0 || index >= keepFromIndex)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
LeavePassed(siteId);
|
||||
}
|
||||
}
|
||||
|
||||
private static int IndexOfSite(IReadOnlyList<int> siteIds, int siteId)
|
||||
{
|
||||
for (var i = 0; i < siteIds.Count; i++)
|
||||
{
|
||||
if (siteIds[i] == siteId)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public int EnsureWindowLocked(IReadOnlyList<int> siteIds, int fromIndex, int windowSize)
|
||||
{
|
||||
if (siteIds == null || siteIds.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
fromIndex = Math.Max(0, Math.Min(fromIndex, siteIds.Count - 1));
|
||||
windowSize = Math.Max(1, Math.Min(windowSize, 10));
|
||||
var maxIndex = Math.Min(siteIds.Count - 1, fromIndex + windowSize - 1);
|
||||
|
||||
// 返回值必须是从 fromIndex 起的连续已占/新锁站数,供 0xB1 窗口切片使用。
|
||||
// UDP Tick 会反复进入:已在 holding 的站直接计数,禁止再次对已消费的 pending 站 TryLock。
|
||||
var lockedCount = 0;
|
||||
for (var i = fromIndex; i <= maxIndex; i++)
|
||||
{
|
||||
var siteId = siteIds[i];
|
||||
if (Array.IndexOf(_car.status.holdingLocks, siteId) >= 0)
|
||||
{
|
||||
lockedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_car.status.pendingLocks.Length == 0 || _car.status.pendingLocks[0] != siteId)
|
||||
{
|
||||
_log(
|
||||
$"window gap site={siteId}, pending0={(_car.status.pendingLocks.Length > 0 ? _car.status.pendingLocks[0].ToString() : "-")}, holding=[{FormatInts(_car.status.holdingLocks)}]");
|
||||
break;
|
||||
}
|
||||
|
||||
if (!TryLockNext(siteId))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
lockedCount++;
|
||||
}
|
||||
|
||||
if (lockedCount == 0)
|
||||
{
|
||||
lockedCount = 1;
|
||||
_log($"window warn: no holding overlap index={fromIndex}, site={siteIds[fromIndex]}, holding=[{FormatInts(_car.status.holdingLocks)}]");
|
||||
}
|
||||
|
||||
_log($"window locked={lockedCount}/{windowSize}, fromIndex={fromIndex}, holding=[{FormatInts(_car.status.holdingLocks)}], pending=[{FormatInts(_car.status.pendingLocks)}]");
|
||||
return lockedCount;
|
||||
}
|
||||
|
||||
public void FinalizeAtSite(int siteId)
|
||||
{
|
||||
_log($"finalize site={siteId}, holding=[{FormatInts(_car.status.holdingLocks)}], pending=[{FormatInts(_car.status.pendingLocks)}]");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SimpleCore.TryLock 在写锁前检查 programs.now;为空则抛 Program obsoleted。
|
||||
/// </summary>
|
||||
private void EnsureProgramAnchor(CarProgram keepProgram)
|
||||
{
|
||||
if (_car.status.programs.now != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (keepProgram != null)
|
||||
{
|
||||
_car.status.programs.now = keepProgram;
|
||||
_log($"restore programs.now after TrafficReset, name={keepProgram.name}, state={keepProgram.status.state}");
|
||||
return;
|
||||
}
|
||||
|
||||
var dummy = new CarProgram
|
||||
{
|
||||
name = $"fass2-traffic-anchor:{_car.id}",
|
||||
plans = new[]
|
||||
{
|
||||
new SegmentPlan { usingCar = _car }
|
||||
}
|
||||
};
|
||||
dummy.status.state = CarProgram.StatusEnum.Dummy;
|
||||
_car.status.programs.now = dummy;
|
||||
_log($"install dummy programs.now for TryLock, name={dummy.name}");
|
||||
}
|
||||
|
||||
private void LogTrafficState(string prefix)
|
||||
{
|
||||
_log(
|
||||
$"{prefix}, holding=[{FormatInts(_car.status.holdingLocks)}], pending=[{FormatInts(_car.status.pendingLocks)}], seqScope=[{FormatInts(_car.status.seqScope)}], seqPtr={_car.status.seqPtr}");
|
||||
}
|
||||
|
||||
private string FormatBlocked()
|
||||
{
|
||||
if (_car.status.blockedBy == null || _car.status.blockedBy.Length == 0)
|
||||
{
|
||||
return "-";
|
||||
}
|
||||
|
||||
return string.Join(",", _car.status.blockedBy.Select(p => $"{p.Item1}:{p.Item2}"));
|
||||
}
|
||||
|
||||
private string FormatTcStat()
|
||||
{
|
||||
try
|
||||
{
|
||||
var tc = _car.status.TCStat;
|
||||
return string.IsNullOrWhiteSpace(tc) ? "-" : tc;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "-";
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatInts(int[] values)
|
||||
{
|
||||
return values == null || values.Length == 0 ? string.Empty : string.Join(",", values);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user