403 lines
13 KiB
C#
403 lines
13 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Polly;
|
|
using ZB.MOM.WW.LmxProxy.Host.Domain;
|
|
using ZB.MOM.WW.LmxProxy.Host.Services;
|
|
|
|
namespace ZB.MOM.WW.LmxProxy.Host.Implementation
|
|
{
|
|
/// <summary>
|
|
/// Read and write operations for MxAccessClient.
|
|
/// </summary>
|
|
public sealed partial class MxAccessClient
|
|
{
|
|
/// <inheritdoc />
|
|
public async Task<Vtq> ReadAsync(string address, CancellationToken ct = default)
|
|
{
|
|
// Apply retry policy for read operations
|
|
IAsyncPolicy<Vtq> policy = RetryPolicies.CreateReadPolicy<Vtq>();
|
|
return await policy.ExecuteWithRetryAsync(async () =>
|
|
{
|
|
ValidateConnection();
|
|
return await ReadSingleValueAsync(address, ct);
|
|
}, $"Read-{address}");
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IReadOnlyDictionary<string, Vtq>> ReadBatchAsync(IEnumerable<string> addresses,
|
|
CancellationToken ct = default)
|
|
{
|
|
var addressList = addresses.ToList();
|
|
var results = new Dictionary<string, Vtq>(addressList.Count);
|
|
|
|
// Create tasks for parallel reading
|
|
IEnumerable<Task> tasks =
|
|
addressList.Select(address => ReadAddressWithSemaphoreAsync(address, results, ct));
|
|
|
|
await Task.WhenAll(tasks);
|
|
return results;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task WriteAsync(string address, object value, CancellationToken ct = default)
|
|
{
|
|
// Apply retry policy for write operations
|
|
IAsyncPolicy policy = RetryPolicies.CreateWritePolicy();
|
|
await policy.ExecuteWithRetryAsync(async () => { await WriteInternalAsync(address, value, ct); },
|
|
$"Write-{address}");
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task WriteBatchAsync(IReadOnlyDictionary<string, object> values, CancellationToken ct = default)
|
|
{
|
|
// Create tasks for parallel writing
|
|
IEnumerable<Task> tasks = values.Select(kvp => WriteAddressWithSemaphoreAsync(kvp.Key, kvp.Value, ct));
|
|
|
|
await Task.WhenAll(tasks);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<bool> WriteBatchAndWaitAsync(
|
|
IReadOnlyDictionary<string, object> values,
|
|
string flagAddress,
|
|
object flagValue,
|
|
string responseAddress,
|
|
object responseValue,
|
|
CancellationToken ct = default)
|
|
{
|
|
// Write the batch values
|
|
await WriteBatchAsync(values, ct);
|
|
|
|
// Write the flag
|
|
await WriteAsync(flagAddress, flagValue, ct);
|
|
|
|
// Wait for the response
|
|
return await WaitForResponseAsync(responseAddress, responseValue, ct);
|
|
}
|
|
|
|
#region Private Helper Methods
|
|
|
|
/// <summary>
|
|
/// Validates that the client is connected.
|
|
/// </summary>
|
|
private void ValidateConnection()
|
|
{
|
|
if (!IsConnected)
|
|
{
|
|
throw new InvalidOperationException("Not connected to MxAccess");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads a single value from the specified address.
|
|
/// </summary>
|
|
private async Task<Vtq> ReadSingleValueAsync(string address, CancellationToken ct)
|
|
{
|
|
// MxAccess doesn't support direct read - we need to subscribe, get the value, then unsubscribe
|
|
var tcs = new TaskCompletionSource<Vtq>();
|
|
IAsyncDisposable? subscription = null;
|
|
|
|
try
|
|
{
|
|
subscription = await SubscribeAsync(new[] { address }, (addr, vtq) => { tcs.TrySetResult(vtq); }, ct);
|
|
|
|
return await WaitForReadResultAsync(tcs, ct);
|
|
}
|
|
finally
|
|
{
|
|
if (subscription != null)
|
|
{
|
|
await subscription.DisposeAsync();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Waits for a read result with timeout.
|
|
/// </summary>
|
|
private async Task<Vtq> WaitForReadResultAsync(TaskCompletionSource<Vtq> tcs, CancellationToken ct)
|
|
{
|
|
using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(_configuration.ReadTimeoutSeconds)))
|
|
{
|
|
using (ct.Register(() => cts.Cancel()))
|
|
{
|
|
cts.Token.Register(() => tcs.TrySetException(new TimeoutException("Read timeout")));
|
|
return await tcs.Task;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads an address with semaphore protection for batch operations.
|
|
/// </summary>
|
|
private async Task ReadAddressWithSemaphoreAsync(string address, Dictionary<string, Vtq> results,
|
|
CancellationToken ct)
|
|
{
|
|
await _readSemaphore.WaitAsync(ct);
|
|
try
|
|
{
|
|
Vtq vtq = await ReadAsync(address, ct);
|
|
lock (results)
|
|
{
|
|
results[address] = vtq;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.Warning(ex, "Failed to read {Address}", address);
|
|
lock (results)
|
|
{
|
|
results[address] = Vtq.Bad();
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_readSemaphore.Release();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Internal write implementation.
|
|
/// </summary>
|
|
private async Task WriteInternalAsync(string address, object value, CancellationToken ct)
|
|
{
|
|
var tcs = new TaskCompletionSource<bool>();
|
|
int itemHandle = await SetupWriteOperationAsync(address, value, tcs, ct);
|
|
|
|
try
|
|
{
|
|
await WaitForWriteCompletionAsync(tcs, itemHandle, address, ct);
|
|
}
|
|
catch
|
|
{
|
|
await CleanupWriteOperationAsync(itemHandle);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets up a write operation and returns the item handle.
|
|
/// </summary>
|
|
private async Task<int> SetupWriteOperationAsync(string address, object value, TaskCompletionSource<bool> tcs,
|
|
CancellationToken ct)
|
|
{
|
|
return await Task.Run(() =>
|
|
{
|
|
lock (_lock)
|
|
{
|
|
ValidateConnectionLocked();
|
|
return InitiateWriteOperation(address, value, tcs);
|
|
}
|
|
}, ct);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates connection while holding the lock.
|
|
/// </summary>
|
|
private void ValidateConnectionLocked()
|
|
{
|
|
if (!IsConnected || _lmxProxy == null)
|
|
{
|
|
throw new InvalidOperationException("Not connected to MxAccess");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initiates a write operation and returns the item handle.
|
|
/// </summary>
|
|
private int InitiateWriteOperation(string address, object value, TaskCompletionSource<bool> tcs)
|
|
{
|
|
int itemHandle = 0;
|
|
try
|
|
{
|
|
if (_lmxProxy == null)
|
|
{
|
|
throw new InvalidOperationException("MxAccess proxy is not initialized");
|
|
}
|
|
|
|
// Add the item if not already added
|
|
itemHandle = _lmxProxy.AddItem(_connectionHandle, address);
|
|
|
|
// Advise the item to enable writing
|
|
_lmxProxy.AdviseSupervisory(_connectionHandle, itemHandle);
|
|
|
|
// Track the pending write operation
|
|
TrackPendingWrite(address, itemHandle, tcs);
|
|
|
|
// Write the value
|
|
_lmxProxy.Write(_connectionHandle, itemHandle, value, -1); // -1 for no security
|
|
|
|
return itemHandle;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
CleanupFailedWrite(itemHandle);
|
|
Logger.Error(ex, "Failed to write value to {Address}", address);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tracks a pending write operation.
|
|
/// </summary>
|
|
private void TrackPendingWrite(string address, int itemHandle, TaskCompletionSource<bool> tcs)
|
|
{
|
|
var writeOp = new WriteOperation
|
|
{
|
|
Address = address,
|
|
ItemHandle = itemHandle,
|
|
CompletionSource = tcs,
|
|
StartTime = DateTime.UtcNow
|
|
};
|
|
_pendingWrites[itemHandle] = writeOp;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cleans up a failed write operation.
|
|
/// </summary>
|
|
private void CleanupFailedWrite(int itemHandle)
|
|
{
|
|
if (itemHandle > 0 && _lmxProxy != null)
|
|
{
|
|
try
|
|
{
|
|
_lmxProxy.UnAdvise(_connectionHandle, itemHandle);
|
|
_lmxProxy.RemoveItem(_connectionHandle, itemHandle);
|
|
_pendingWrites.Remove(itemHandle);
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Waits for write completion with timeout.
|
|
/// </summary>
|
|
private async Task WaitForWriteCompletionAsync(TaskCompletionSource<bool> tcs, int itemHandle, string address,
|
|
CancellationToken ct)
|
|
{
|
|
using (ct.Register(() => tcs.TrySetCanceled()))
|
|
{
|
|
var timeoutTask = Task.Delay(TimeSpan.FromSeconds(_configuration.WriteTimeoutSeconds), ct);
|
|
Task? completedTask = await Task.WhenAny(tcs.Task, timeoutTask);
|
|
|
|
if (completedTask == timeoutTask)
|
|
{
|
|
await HandleWriteTimeoutAsync(itemHandle, address);
|
|
}
|
|
|
|
await tcs.Task; // This will throw if the write failed
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles write timeout by cleaning up resources.
|
|
/// </summary>
|
|
private async Task HandleWriteTimeoutAsync(int itemHandle, string address)
|
|
{
|
|
await CleanupWriteOperationAsync(itemHandle);
|
|
throw new TimeoutException($"Write operation to {address} timed out");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cleans up a write operation.
|
|
/// </summary>
|
|
private async Task CleanupWriteOperationAsync(int itemHandle)
|
|
{
|
|
await Task.Run(() =>
|
|
{
|
|
lock (_lock)
|
|
{
|
|
if (_pendingWrites.ContainsKey(itemHandle))
|
|
{
|
|
_pendingWrites.Remove(itemHandle);
|
|
if (_lmxProxy != null)
|
|
{
|
|
try
|
|
{
|
|
_lmxProxy.UnAdvise(_connectionHandle, itemHandle);
|
|
_lmxProxy.RemoveItem(_connectionHandle, itemHandle);
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes an address with semaphore protection for batch operations.
|
|
/// </summary>
|
|
private async Task WriteAddressWithSemaphoreAsync(string address, object value, CancellationToken ct)
|
|
{
|
|
await _writeSemaphore.WaitAsync(ct);
|
|
try
|
|
{
|
|
await WriteAsync(address, value, ct);
|
|
}
|
|
finally
|
|
{
|
|
_writeSemaphore.Release();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Waits for a specific response value.
|
|
/// </summary>
|
|
private async Task<bool> WaitForResponseAsync(string responseAddress, object responseValue,
|
|
CancellationToken ct)
|
|
{
|
|
var tcs = new TaskCompletionSource<bool>();
|
|
IAsyncDisposable? subscription = null;
|
|
|
|
try
|
|
{
|
|
subscription = await SubscribeAsync(new[] { responseAddress }, (addr, vtq) =>
|
|
{
|
|
if (Equals(vtq.Value, responseValue))
|
|
{
|
|
tcs.TrySetResult(true);
|
|
}
|
|
}, ct);
|
|
|
|
// Wait for the response value
|
|
using (ct.Register(() => tcs.TrySetResult(false)))
|
|
{
|
|
return await tcs.Task;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (subscription != null)
|
|
{
|
|
await subscription.DisposeAsync();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a human-readable error message for a write error code.
|
|
/// </summary>
|
|
/// <param name="errorCode">The error code.</param>
|
|
/// <returns>The error message.</returns>
|
|
private static string GetWriteErrorMessage(int errorCode)
|
|
{
|
|
return errorCode switch
|
|
{
|
|
1008 => "User lacks proper security for write operation",
|
|
1012 => "Secured write required",
|
|
1013 => "Verified write required",
|
|
_ => $"Unknown error code: {errorCode}"
|
|
};
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|