using System;
|
using System.Threading;
|
using System.Threading.Tasks;
|
using Aging.Infrastructure.Contracts;
|
using Aging.Infrastructure.Core;
|
using Aging.ResourceContracts;
|
using log4net;
|
|
namespace Aging.Infrastructure.Services
|
{
|
/// <summary>
|
/// 自动化设备门面实现。
|
///
|
/// 生命周期:
|
/// 1) ReserveAsync 申请资源占用 → 得到 ResourceOwnerToken
|
/// 2) DeviceService 分派命令 → 内部再拿操作租约
|
/// 3) ReleaseReservationAsync 释放资源
|
///
|
/// 两种调用模式:
|
/// - ExecuteAsync/QueryAsync:一次性执行(内部完成 1→2→3)
|
/// - AcquireAsync:作用域租约(1 手动触发;3 在 lease.Dispose 时触发;2 可多次)
|
/// </summary>
|
public sealed class AutoDeviceFacade : IAutoDeviceFacade
|
{
|
private static readonly ILog Log = LogManager.GetLogger(typeof(AutoDeviceFacade));
|
|
private readonly IResourceManager _resourceManager;
|
private readonly IDeviceService _deviceService;
|
|
/// <summary>
|
/// 默认租约申请超时(秒)
|
/// </summary>
|
public int DefaultLeaseTimeoutSeconds { get; set; } = 30;
|
|
/// <summary>
|
/// 构造函数
|
/// </summary>
|
public AutoDeviceFacade(IResourceManager resourceManager, IDeviceService deviceService)
|
{
|
_resourceManager = resourceManager ?? throw new ArgumentNullException(nameof(resourceManager));
|
_deviceService = deviceService ?? throw new ArgumentNullException(nameof(deviceService));
|
}
|
|
// ==================== 一次性执行模式 ====================
|
|
/// <inheritdoc/>
|
public async Task<DeviceCommandResult> ExecuteAsync(
|
string deviceId, string command, CancellationToken ct = default)
|
{
|
if (string.IsNullOrWhiteSpace(deviceId))
|
throw new ArgumentException("设备 ID 不能为空", nameof(deviceId));
|
if (string.IsNullOrWhiteSpace(command))
|
throw new ArgumentException("命令不能为空", nameof(command));
|
|
// 1. 申请资源占用
|
var owner = await ReserveCoreAsync(deviceId, "OneShot", ct).ConfigureAwait(false);
|
try
|
{
|
// 2. 通过 DeviceService 分派命令
|
return await _deviceService.ExecuteCommandAsync(deviceId, command, owner, ct)
|
.ConfigureAwait(false);
|
}
|
finally
|
{
|
// 3. 无论成功失败都释放资源
|
await ReleaseCoreAsync(deviceId, owner).ConfigureAwait(false);
|
}
|
}
|
|
/// <inheritdoc/>
|
public async Task<DeviceCommandResult> QueryAsync(
|
string deviceId, string query, CancellationToken ct = default)
|
{
|
if (string.IsNullOrWhiteSpace(deviceId))
|
throw new ArgumentException("设备 ID 不能为空", nameof(deviceId));
|
|
var owner = await ReserveCoreAsync(deviceId, "OneShotQuery", ct).ConfigureAwait(false);
|
try
|
{
|
return await _deviceService.ExecuteQueryAsync(deviceId, query, owner, ct)
|
.ConfigureAwait(false);
|
}
|
finally
|
{
|
await ReleaseCoreAsync(deviceId, owner).ConfigureAwait(false);
|
}
|
}
|
|
// ==================== 作用域租约模式 ====================
|
|
/// <inheritdoc/>
|
public async Task<IAutoDeviceLease> AcquireAsync(
|
string deviceId, string stepName, CancellationToken ct = default)
|
{
|
if (string.IsNullOrWhiteSpace(deviceId))
|
throw new ArgumentException("设备 ID 不能为空", nameof(deviceId));
|
|
var owner = await ReserveCoreAsync(deviceId, stepName ?? "Step", ct).ConfigureAwait(false);
|
var sessionId = Guid.NewGuid().ToString();
|
|
Log.Info($"[AutoFacade] 设备 {deviceId} 已获得作用域租约: Session={sessionId.Substring(0, 8)}, Step={stepName}");
|
|
var lease = new AutoDeviceLease(this, _deviceService, deviceId, sessionId, stepName ?? "Step", owner);
|
|
// 如果底层是 LayeredResourceManager,启动心跳续期
|
if (_resourceManager is LayeredResourceManager lrm)
|
{
|
if (lrm.TryGetGrants(owner.TokenId, out var grants))
|
{
|
lease.StartHeartbeat(lrm.Coordinator, grants);
|
}
|
}
|
|
return lease;
|
}
|
|
// ==================== 内部辅助 ====================
|
|
/// <summary>
|
/// 内部:申请资源占用。
|
/// 使用 OwnerType.Production(自动化测试视为生产流程),
|
/// 会与其他测试/维护会话互斥。
|
/// </summary>
|
private async Task<ResourceOwnerToken> ReserveCoreAsync(
|
string deviceId, string purpose, CancellationToken ct)
|
{
|
var owner = new ResourceOwnerToken(OwnerType.Production, $"auto_{Guid.NewGuid():N}");
|
var resourceKey = new ResourceKey(deviceId, "Generic");
|
|
// 使用超时包装
|
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(ct))
|
{
|
cts.CancelAfter(TimeSpan.FromSeconds(DefaultLeaseTimeoutSeconds));
|
try
|
{
|
await _resourceManager.ReserveAsync(owner, resourceKey, cts.Token).ConfigureAwait(false);
|
Log.Debug($"[AutoFacade] 设备 {deviceId} 已预约 (purpose={purpose}, owner={owner.OwnerId})");
|
}
|
catch (OperationCanceledException)
|
{
|
throw new TimeoutException($"设备 {deviceId} 租约申请超时({DefaultLeaseTimeoutSeconds}s),可能被生产或维护占用");
|
}
|
catch (ResourceConflictException ex)
|
{
|
throw new InvalidOperationException(
|
$"设备 {deviceId} 已被其他所有者占用: {ex.Message}", ex);
|
}
|
}
|
|
return owner;
|
}
|
|
/// <summary>
|
/// 内部:释放资源占用。
|
/// </summary>
|
internal async Task ReleaseCoreAsync(string deviceId, ResourceOwnerToken owner)
|
{
|
try
|
{
|
var resourceKey = new ResourceKey(deviceId, "Generic");
|
await _resourceManager.ReleaseReservationAsync(owner, resourceKey, CancellationToken.None)
|
.ConfigureAwait(false);
|
Log.Debug($"[AutoFacade] 设备 {deviceId} 已释放预约 (owner={owner.OwnerId})");
|
}
|
catch (Exception ex)
|
{
|
// 释放失败不阻塞主流程,仅记录
|
Log.Warn($"[AutoFacade] 释放设备 {deviceId} 预约异常: {ex.Message}");
|
}
|
}
|
}
|
|
/// <summary>
|
/// 自动化设备作用域租约实现。
|
/// Dispose 时自动释放底层资源占用。
|
/// 幂等:多次 Dispose 只释放一次。
|
/// </summary>
|
internal sealed class AutoDeviceLease : IAutoDeviceLease
|
{
|
private readonly AutoDeviceFacade _facade;
|
private readonly IDeviceService _deviceService;
|
private readonly ResourceOwnerToken _owner;
|
private bool _disposed;
|
|
// V3.0 心跳续期字段
|
private LeaseGrant[] _grants = new LeaseGrant[0];
|
private Timer? _beacon;
|
private volatile int _renewFailures;
|
private ILeaseCoordinator? _coordinator;
|
public event EventHandler? LeaseLost;
|
public bool HandoverRequested => false;
|
|
public string DeviceId { get; }
|
public string SessionId { get; }
|
public string StepName { get; }
|
public DateTime AcquiredAt { get; }
|
public bool IsReleased => _disposed;
|
|
internal AutoDeviceLease(
|
AutoDeviceFacade facade,
|
IDeviceService deviceService,
|
string deviceId,
|
string sessionId,
|
string stepName,
|
ResourceOwnerToken owner)
|
{
|
_facade = facade;
|
_deviceService = deviceService;
|
DeviceId = deviceId;
|
SessionId = sessionId;
|
StepName = stepName;
|
AcquiredAt = DateTime.Now;
|
_owner = owner;
|
}
|
|
internal void StartHeartbeat(ILeaseCoordinator coordinator, LeaseGrant[] grants)
|
{
|
_coordinator = coordinator;
|
_grants = grants;
|
_beacon = new Timer(_ =>
|
{
|
if (_disposed) return;
|
try
|
{
|
bool allOk = true;
|
foreach (var g in _grants)
|
{
|
if (!_coordinator.Renew(g, 30)) { allOk = false; break; }
|
}
|
if (allOk)
|
Interlocked.Exchange(ref _renewFailures, 0);
|
else
|
Interlocked.Increment(ref _renewFailures);
|
if (Volatile.Read(ref _renewFailures) >= 2) LeaseLost?.Invoke(this, EventArgs.Empty);
|
}
|
catch
|
{
|
if (Interlocked.Increment(ref _renewFailures) >= 2) LeaseLost?.Invoke(this, EventArgs.Empty);
|
}
|
}, null, 8000, 8000);
|
}
|
|
public Task<DeviceCommandResult> ExecuteAsync(string command, CancellationToken ct = default)
|
{
|
ThrowIfReleased();
|
ThrowIfStale();
|
return _deviceService.ExecuteCommandAsync(DeviceId, command, _owner, ct);
|
}
|
|
public Task<DeviceCommandResult> QueryAsync(string query, CancellationToken ct = default)
|
{
|
ThrowIfReleased();
|
ThrowIfStale();
|
return _deviceService.ExecuteQueryAsync(DeviceId, query, _owner, ct);
|
}
|
|
public Task ReleaseAsync(CancellationToken ct = default)
|
{
|
// Dispose() 内部已处理 _beacon?.Dispose() 和 _disposed 标记
|
Dispose();
|
return Task.CompletedTask;
|
}
|
|
public void Dispose()
|
{
|
if (_disposed) return;
|
_beacon?.Dispose();
|
_disposed = true;
|
// 同步释放(.NET 4.7.2 无 IAsyncDisposable),阻塞等待
|
_facade.ReleaseCoreAsync(DeviceId, _owner).GetAwaiter().GetResult();
|
GC.SuppressFinalize(this);
|
}
|
|
private void ThrowIfReleased()
|
{
|
if (_disposed)
|
throw new ObjectDisposedException(nameof(AutoDeviceLease),
|
$"设备 {DeviceId} 的租约已释放 (Session={SessionId.Substring(0, 8)})");
|
}
|
|
private void ThrowIfStale()
|
{
|
if (Volatile.Read(ref _renewFailures) >= 2)
|
throw new InvalidOperationException("LEASE_STALE: 租约已失效");
|
}
|
}
|
}
|