Files
黄兆尉andCursor e4a245e644 fix: FASS2 状态机与 UDP 枢纽审查项
等锁超时、全路径签名、无锁窗口不下发、Fault 终态可等待、Pass+机构不再短路、Alarm 不杀单、catch-up 停在未完成站;VehicleCode=0/重复车号拒绝注册,Hub 已运行时不抢监听口;FileLogger 空目录可写。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-26 23:04:58 +08:00

298 lines
11 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}
}