feat(dcl): bound per-tag seed ReadAsync with SeedReadTimeout (#232, DCL-027)
This commit is contained in:
@@ -25,7 +25,8 @@
|
|||||||
"DataConnection": {
|
"DataConnection": {
|
||||||
"ReconnectInterval": "00:00:05",
|
"ReconnectInterval": "00:00:05",
|
||||||
"TagResolutionRetryInterval": "00:00:10",
|
"TagResolutionRetryInterval": "00:00:10",
|
||||||
"WriteTimeout": "00:00:30"
|
"WriteTimeout": "00:00:30",
|
||||||
|
"SeedReadTimeout": "00:00:30"
|
||||||
},
|
},
|
||||||
"StoreAndForward": {
|
"StoreAndForward": {
|
||||||
"SqliteDbPath": "/app/data/store-and-forward.db",
|
"SqliteDbPath": "/app/data/store-and-forward.db",
|
||||||
|
|||||||
@@ -25,7 +25,8 @@
|
|||||||
"DataConnection": {
|
"DataConnection": {
|
||||||
"ReconnectInterval": "00:00:05",
|
"ReconnectInterval": "00:00:05",
|
||||||
"TagResolutionRetryInterval": "00:00:10",
|
"TagResolutionRetryInterval": "00:00:10",
|
||||||
"WriteTimeout": "00:00:30"
|
"WriteTimeout": "00:00:30",
|
||||||
|
"SeedReadTimeout": "00:00:30"
|
||||||
},
|
},
|
||||||
"StoreAndForward": {
|
"StoreAndForward": {
|
||||||
"SqliteDbPath": "/app/data/store-and-forward.db",
|
"SqliteDbPath": "/app/data/store-and-forward.db",
|
||||||
|
|||||||
@@ -26,7 +26,8 @@
|
|||||||
"DataConnection": {
|
"DataConnection": {
|
||||||
"ReconnectInterval": "00:00:05",
|
"ReconnectInterval": "00:00:05",
|
||||||
"TagResolutionRetryInterval": "00:00:10",
|
"TagResolutionRetryInterval": "00:00:10",
|
||||||
"WriteTimeout": "00:00:30"
|
"WriteTimeout": "00:00:30",
|
||||||
|
"SeedReadTimeout": "00:00:30"
|
||||||
},
|
},
|
||||||
"StoreAndForward": {
|
"StoreAndForward": {
|
||||||
"SqliteDbPath": "/app/data/store-and-forward.db",
|
"SqliteDbPath": "/app/data/store-and-forward.db",
|
||||||
|
|||||||
@@ -26,7 +26,8 @@
|
|||||||
"DataConnection": {
|
"DataConnection": {
|
||||||
"ReconnectInterval": "00:00:05",
|
"ReconnectInterval": "00:00:05",
|
||||||
"TagResolutionRetryInterval": "00:00:10",
|
"TagResolutionRetryInterval": "00:00:10",
|
||||||
"WriteTimeout": "00:00:30"
|
"WriteTimeout": "00:00:30",
|
||||||
|
"SeedReadTimeout": "00:00:30"
|
||||||
},
|
},
|
||||||
"StoreAndForward": {
|
"StoreAndForward": {
|
||||||
"SqliteDbPath": "/app/data/store-and-forward.db",
|
"SqliteDbPath": "/app/data/store-and-forward.db",
|
||||||
|
|||||||
@@ -26,7 +26,8 @@
|
|||||||
"DataConnection": {
|
"DataConnection": {
|
||||||
"ReconnectInterval": "00:00:05",
|
"ReconnectInterval": "00:00:05",
|
||||||
"TagResolutionRetryInterval": "00:00:10",
|
"TagResolutionRetryInterval": "00:00:10",
|
||||||
"WriteTimeout": "00:00:30"
|
"WriteTimeout": "00:00:30",
|
||||||
|
"SeedReadTimeout": "00:00:30"
|
||||||
},
|
},
|
||||||
"StoreAndForward": {
|
"StoreAndForward": {
|
||||||
"SqliteDbPath": "/app/data/store-and-forward.db",
|
"SqliteDbPath": "/app/data/store-and-forward.db",
|
||||||
|
|||||||
@@ -26,7 +26,8 @@
|
|||||||
"DataConnection": {
|
"DataConnection": {
|
||||||
"ReconnectInterval": "00:00:05",
|
"ReconnectInterval": "00:00:05",
|
||||||
"TagResolutionRetryInterval": "00:00:10",
|
"TagResolutionRetryInterval": "00:00:10",
|
||||||
"WriteTimeout": "00:00:30"
|
"WriteTimeout": "00:00:30",
|
||||||
|
"SeedReadTimeout": "00:00:30"
|
||||||
},
|
},
|
||||||
"StoreAndForward": {
|
"StoreAndForward": {
|
||||||
"SqliteDbPath": "/app/data/store-and-forward.db",
|
"SqliteDbPath": "/app/data/store-and-forward.db",
|
||||||
|
|||||||
@@ -26,7 +26,8 @@
|
|||||||
"DataConnection": {
|
"DataConnection": {
|
||||||
"ReconnectInterval": "00:00:05",
|
"ReconnectInterval": "00:00:05",
|
||||||
"TagResolutionRetryInterval": "00:00:10",
|
"TagResolutionRetryInterval": "00:00:10",
|
||||||
"WriteTimeout": "00:00:30"
|
"WriteTimeout": "00:00:30",
|
||||||
|
"SeedReadTimeout": "00:00:30"
|
||||||
},
|
},
|
||||||
"StoreAndForward": {
|
"StoreAndForward": {
|
||||||
"SqliteDbPath": "/app/data/store-and-forward.db",
|
"SqliteDbPath": "/app/data/store-and-forward.db",
|
||||||
|
|||||||
@@ -26,7 +26,8 @@
|
|||||||
"DataConnection": {
|
"DataConnection": {
|
||||||
"ReconnectInterval": "00:00:05",
|
"ReconnectInterval": "00:00:05",
|
||||||
"TagResolutionRetryInterval": "00:00:10",
|
"TagResolutionRetryInterval": "00:00:10",
|
||||||
"WriteTimeout": "00:00:30"
|
"WriteTimeout": "00:00:30",
|
||||||
|
"SeedReadTimeout": "00:00:30"
|
||||||
},
|
},
|
||||||
"StoreAndForward": {
|
"StoreAndForward": {
|
||||||
"SqliteDbPath": "/app/data/store-and-forward.db",
|
"SqliteDbPath": "/app/data/store-and-forward.db",
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ These are configured via `DataConnectionOptions` in `appsettings.json`, not per-
|
|||||||
| `ReconnectInterval` | 5s | Fixed interval between reconnection attempts |
|
| `ReconnectInterval` | 5s | Fixed interval between reconnection attempts |
|
||||||
| `TagResolutionRetryInterval` | 10s | Retry interval for unresolved tag paths |
|
| `TagResolutionRetryInterval` | 10s | Retry interval for unresolved tag paths |
|
||||||
| `WriteTimeout` | 30s | Timeout for write operations |
|
| `WriteTimeout` | 30s | Timeout for write operations |
|
||||||
|
| `SeedReadTimeout` | 30s | Per-tag timeout for seed reads on the initial-subscribe (and reconnect re-seed) path. A hung device read is treated as a failed seed: retried up to `SeedReadMaxAttempts`, then the tag stays Uncertain until a change notification arrives. |
|
||||||
|
|
||||||
## Subscription Management
|
## Subscription Management
|
||||||
|
|
||||||
|
|||||||
@@ -780,9 +780,16 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
|||||||
{
|
{
|
||||||
foreach (var tagPath in pending.ToList())
|
foreach (var tagPath in pending.ToList())
|
||||||
{
|
{
|
||||||
|
// DataConnectionLayer-027: bound each per-tag read with SeedReadTimeout so a
|
||||||
|
// hung device read cannot delay SubscribeCompleted/the ack indefinitely.
|
||||||
|
// On timeout the catch block treats it identically to any other failed seed
|
||||||
|
// read — the tag stays in pending, is retried up to SeedReadMaxAttempts, and
|
||||||
|
// left Uncertain if still empty after the budget. Same CancellationTokenSource
|
||||||
|
// mechanism used by HandleWrite for WriteTimeout (DataConnectionLayer-005).
|
||||||
|
using var cts = new CancellationTokenSource(_options.SeedReadTimeout);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var readResult = await adapter.ReadAsync(tagPath);
|
var readResult = await adapter.ReadAsync(tagPath, cts.Token);
|
||||||
if (readResult.Success && readResult.Value is { Value: not null } value)
|
if (readResult.Success && readResult.Value is { Value: not null } value)
|
||||||
{
|
{
|
||||||
seedValues.Add(new SeededValue(tagPath, value));
|
seedValues.Add(new SeededValue(tagPath, value));
|
||||||
@@ -792,6 +799,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
|
|||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// Best-effort read — retried below, or logged once the budget is spent.
|
// Best-effort read — retried below, or logged once the budget is spent.
|
||||||
|
// Includes OperationCanceledException on SeedReadTimeout expiry.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,16 @@ public class DataConnectionOptions
|
|||||||
/// <summary>Timeout for synchronous write operations to devices.</summary>
|
/// <summary>Timeout for synchronous write operations to devices.</summary>
|
||||||
public TimeSpan WriteTimeout { get; set; } = TimeSpan.FromSeconds(30);
|
public TimeSpan WriteTimeout { get; set; } = TimeSpan.FromSeconds(30);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DataConnectionLayer-027: per-tag timeout applied to each <see cref="IDataConnection.ReadAsync"/>
|
||||||
|
/// call on the initial-subscribe seed path (and reconnect re-seed). A hung device read would
|
||||||
|
/// otherwise delay <c>SubscribeCompleted</c> and its acknowledgement indefinitely. On timeout
|
||||||
|
/// the tag is treated the same as any other failed seed read — it stays in the pending set and
|
||||||
|
/// is retried up to <see cref="SeedReadMaxAttempts"/>, then left Uncertain until a change
|
||||||
|
/// notification arrives.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan SeedReadTimeout { get; set; } = TimeSpan.FromSeconds(30);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Minimum time a connection must stay up before it is considered stable.
|
/// Minimum time a connection must stay up before it is considered stable.
|
||||||
/// If a connection drops before this threshold, it counts as an unstable
|
/// If a connection drops before this threshold, it counts as an unstable
|
||||||
|
|||||||
Reference in New Issue
Block a user