Files
scadalink-design/deprecated/lmxproxy/docs/requirements/Component-Client.md
Joseph Doherty 9dccf8e72f deprecate(lmxproxy): move all LmxProxy code, tests, and docs to deprecated/
LmxProxy is no longer needed. Moved the entire lmxproxy/ workspace, DCL
adapter files, and related docs to deprecated/. Removed LmxProxy registration
from DataConnectionFactory, project reference from DCL, protocol option from
UI, and cleaned up all requirement docs.
2026-04-08 15:56:23 -04:00

201 lines
8.8 KiB
Markdown
Raw 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.
# Component: Client
## Purpose
A .NET 10 class library providing a typed gRPC client for consuming the LmxProxy service. Used by ScadaLink's Data Connection Layer to connect to AVEVA System Platform via the LmxProxy Host.
## Location
`src/ZB.MOM.WW.LmxProxy.Client/` — all files in this project.
Key files:
- `ILmxProxyClient.cs` — public interface.
- `LmxProxyClient.cs` — main implementation (partial class across multiple files).
- `LmxProxyClientBuilder.cs` — fluent builder for client construction.
- `ServiceCollectionExtensions.cs` — DI integration and options classes.
- `ILmxProxyClientFactory.cs` — factory interface and implementation.
- `StreamingExtensions.cs` — batch and parallel streaming helpers.
- `Domain/ScadaContracts.cs` — code-first gRPC contracts.
- `Security/GrpcChannelFactory.cs` — TLS channel creation.
## Responsibilities
- Connect to and communicate with the LmxProxy Host gRPC service.
- Manage session lifecycle (connect, keep-alive, disconnect).
- Execute read, write, and subscribe operations with retry and concurrency control.
- Provide a fluent builder and DI integration for configuration.
- Track client-side performance metrics.
- Support TLS and mutual TLS connections.
## 1. Public Interface (ILmxProxyClient)
| Method | Description |
|--------|-------------|
| `ConnectAsync(ct)` | Establish gRPC channel and session |
| `DisconnectAsync()` | Graceful disconnect |
| `IsConnectedAsync()` | Thread-safe connection state check |
| `ReadAsync(address, ct)` | Read single tag, returns Vtq |
| `ReadBatchAsync(addresses, ct)` | Read multiple tags, returns dictionary |
| `WriteAsync(address, value, ct)` | Write single tag value |
| `WriteBatchAsync(values, ct)` | Write multiple tag values |
| `SubscribeAsync(addresses, onUpdate, onStreamError, ct)` | Subscribe to tag updates with value and error callbacks |
| `GetMetrics()` | Return operation counts, errors, latency stats |
| `DefaultTimeout` | Configurable timeout (default 30s, range 1s10min) |
Implements `IDisposable` and `IAsyncDisposable`.
## 2. Connection Management
### 2.1 Connect
`ConnectAsync()`:
1. Creates a gRPC channel via `GrpcChannelFactory` (HTTP or HTTPS based on TLS config).
2. Creates a `protobuf-net.Grpc` client for `IScadaService`.
3. Calls the `Connect` RPC with a client ID (format: `ScadaBridge-{guid}`) and optional API key.
4. Stores the returned session ID.
5. Starts the keep-alive timer.
### 2.2 Keep-Alive
- Timer-based ping every **30 seconds** (hardcoded).
- Sends a lightweight `GetConnectionState` RPC.
- On failure: stops the timer, marks disconnected, triggers subscription cleanup.
### 2.3 Disconnect
`DisconnectAsync()`:
1. Stops keep-alive timer.
2. Calls `Disconnect` RPC.
3. Clears session ID.
4. Disposes gRPC channel.
### 2.4 Connection State
`IsConnected` property: `!_disposed && _isConnected && !string.IsNullOrEmpty(_sessionId)`.
## 3. Builder Pattern (LmxProxyClientBuilder)
| Method | Default | Constraint |
|--------|---------|-----------|
| `WithHost(string)` | Required | Non-null/non-empty |
| `WithPort(int)` | 5050 | 165535 |
| `WithApiKey(string?)` | null | Optional |
| `WithTimeout(TimeSpan)` | 30 seconds | > 0 and ≤ 10 minutes |
| `WithLogger(ILogger)` | NullLogger | Optional |
| `WithSslCredentials(string?)` | Disabled | Optional cert path |
| `WithTlsConfiguration(ClientTlsConfiguration)` | null | Full TLS config |
| `WithRetryPolicy(int, TimeSpan)` | 3 attempts, 1s delay | maxAttempts > 0, delay > 0 |
| `WithMetrics()` | Disabled | Enables metric collection |
| `WithCorrelationIdHeader(string)` | null | Custom header name |
## 4. Retry Policy
Polly-based exponential backoff:
- Default: **3 attempts** with **1-second** initial delay.
- Backoff sequence: `delay * 2^(retryAttempt - 1)` → 1s, 2s, 4s.
- Transient errors retried: `Unavailable`, `DeadlineExceeded`, `ResourceExhausted`, `Aborted`.
- Each retry is logged with correlation ID at Warning level.
## 5. Subscription
### 5.1 Subscribe API
`SubscribeAsync(addresses, onUpdate, onStreamError, ct)` returns an `ISubscription`:
- Calls the `Subscribe` RPC (server streaming) with the tag list and default sampling interval (**1000ms**).
- Processes streamed `VtqMessage` items asynchronously, invoking the `onUpdate(tag, vtq)` callback for each.
- On stream termination (server disconnect, gRPC error, or connection drop), invokes the `onStreamError` callback exactly once.
- On stream error, the client immediately nullifies its session ID, causing `IsConnected` to return `false`. This triggers the DCL adapter's `Disconnected` event and reconnection cycle.
- Errors are logged per-subscription.
### 5.2 ISubscription
- `Dispose()` — synchronous disposal with **5-second** timeout.
- Automatic callback on disposal for cleanup.
## 6. DI Integration
### 6.1 Service Collection Extensions
| Method | Lifetime | Description |
|--------|----------|-------------|
| `AddLmxProxyClient(IConfiguration)` | Singleton | Bind `LmxProxy` config section |
| `AddLmxProxyClient(IConfiguration, string)` | Singleton | Bind named config section |
| `AddLmxProxyClient(Action<Builder>)` | Singleton | Builder action |
| `AddScopedLmxProxyClient(IConfiguration)` | Scoped | Per-scope lifetime |
| `AddNamedLmxProxyClient(string, Action<Builder>)` | Keyed singleton | Named/keyed registration |
### 6.2 Configuration Options (LmxProxyClientOptions)
Bound from `appsettings.json`:
| Setting | Default | Description |
|---------|---------|-------------|
| Host | `localhost` | Server hostname |
| Port | 5050 | Server port |
| ApiKey | null | API key |
| Timeout | 30 seconds | Operation timeout |
| UseSsl | false | Enable TLS |
| CertificatePath | null | SSL certificate path |
| EnableMetrics | false | Enable client metrics |
| CorrelationIdHeader | null | Custom correlation header |
| Retry:MaxAttempts | 3 | Retry attempts |
| Retry:Delay | 1 second | Initial retry delay |
### 6.3 Factory Pattern
`ILmxProxyClientFactory` creates configured clients:
- `CreateClient()` — uses default `LmxProxy` config section.
- `CreateClient(string)` — uses named config section.
- `CreateClient(Action<Builder>)` — uses builder action.
Registered as singleton in DI.
## 7. Streaming Extensions
Helper methods for large-scale batch operations:
| Method | Default Batch Size | Description |
|--------|--------------------|-------------|
| `ReadStreamAsync` | 100 | Batched reads, 2 retries per batch, stops after 3 consecutive errors. Returns `IAsyncEnumerable<KeyValuePair<string, Vtq>>`. |
| `WriteStreamAsync` | 100 | Batched writes from async enumerable input. Returns total count written. |
| `ProcessInParallelAsync` | — | Parallel processing with max concurrency of **4** (configurable). Semaphore-based rate limiting. |
| `SubscribeStreamAsync` | — | Wraps callback-based subscription into `IAsyncEnumerable<Vtq>` via `System.Threading.Channels`. |
## 8. Client Metrics
When metrics are enabled (`WithMetrics()`):
- Per-operation tracking: counts, error counts, latency.
- Rolling buffer of **1000** latency samples per operation (prevents memory growth).
- Snapshot via `GetMetrics()` returns: `{op}_count`, `{op}_errors`, `{op}_avg_latency_ms`, `{op}_p95_latency_ms`, `{op}_p99_latency_ms`.
## 9. Value and Quality Handling
### 9.1 Values (TypedValue)
Read responses and subscription updates return values as `TypedValue` (protobuf oneof). The client extracts the value directly from the appropriate oneof field (e.g., `vtq.Value.DoubleValue`, `vtq.Value.BoolValue`). Write operations construct `TypedValue` with the correct oneof case for the value's native type. No string serialization or parsing is needed.
### 9.2 Quality (QualityCode)
Quality is received as a `QualityCode` message. Category checks use bitmask: `IsGood = (statusCode & 0xC0000000) == 0x00000000`, `IsBad = (statusCode & 0xC0000000) == 0x80000000`. The `symbolic_name` field provides human-readable quality for logging and display.
### 9.3 Current Implementation (V1 Legacy)
The current codebase still uses v1 string-based encoding. During v2 migration, the following will be removed:
- `ConvertToVtq()` — parses string values via heuristic (double → bool → null → raw string).
- `ConvertToString()` — serializes values via `.ToString()`.
## Dependencies
- **protobuf-net.Grpc** — code-first gRPC client.
- **Grpc.Net.Client** — HTTP/2 gRPC transport.
- **Polly** — retry policies.
- **Microsoft.Extensions.DependencyInjection** — DI integration.
- **Microsoft.Extensions.Configuration** — options binding.
- **Microsoft.Extensions.Logging** — logging abstraction.
## Interactions
- **ScadaLink Data Connection Layer** consumes the client library via `ILmxProxyClient`.
- **Protocol** — the client uses code-first contracts (`IScadaService`) that are wire-compatible with the Host's proto-generated service.
- **Security** — `GrpcChannelFactory` creates TLS-configured channels matching the Host's TLS configuration.