等锁超时、全路径签名、无锁窗口不下发、Fault 终态可等待、Pass+机构不再短路、Alarm 不杀单、catch-up 停在未完成站;VehicleCode=0/重复车号拒绝注册,Hub 已运行时不抢监听口;FileLogger 空目录可写。 Co-authored-by: Cursor <cursoragent@cursor.com>
298 lines
11 KiB
C#
298 lines
11 KiB
C#
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++;
|
||
}
|
||
|
||
_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);
|
||
}
|
||
}
|
||
}
|