diff --git a/docker-env2/site-x-node-a/appsettings.Site.json b/docker-env2/site-x-node-a/appsettings.Site.json
index 0cc7f03d..5c2ada30 100644
--- a/docker-env2/site-x-node-a/appsettings.Site.json
+++ b/docker-env2/site-x-node-a/appsettings.Site.json
@@ -25,7 +25,8 @@
"DataConnection": {
"ReconnectInterval": "00:00:05",
"TagResolutionRetryInterval": "00:00:10",
- "WriteTimeout": "00:00:30"
+ "WriteTimeout": "00:00:30",
+ "SeedReadTimeout": "00:00:30"
},
"StoreAndForward": {
"SqliteDbPath": "/app/data/store-and-forward.db",
diff --git a/docker-env2/site-x-node-b/appsettings.Site.json b/docker-env2/site-x-node-b/appsettings.Site.json
index a28728cf..b53344a1 100644
--- a/docker-env2/site-x-node-b/appsettings.Site.json
+++ b/docker-env2/site-x-node-b/appsettings.Site.json
@@ -25,7 +25,8 @@
"DataConnection": {
"ReconnectInterval": "00:00:05",
"TagResolutionRetryInterval": "00:00:10",
- "WriteTimeout": "00:00:30"
+ "WriteTimeout": "00:00:30",
+ "SeedReadTimeout": "00:00:30"
},
"StoreAndForward": {
"SqliteDbPath": "/app/data/store-and-forward.db",
diff --git a/docker/site-a-node-a/appsettings.Site.json b/docker/site-a-node-a/appsettings.Site.json
index 44f588fe..905ff385 100644
--- a/docker/site-a-node-a/appsettings.Site.json
+++ b/docker/site-a-node-a/appsettings.Site.json
@@ -26,7 +26,8 @@
"DataConnection": {
"ReconnectInterval": "00:00:05",
"TagResolutionRetryInterval": "00:00:10",
- "WriteTimeout": "00:00:30"
+ "WriteTimeout": "00:00:30",
+ "SeedReadTimeout": "00:00:30"
},
"StoreAndForward": {
"SqliteDbPath": "/app/data/store-and-forward.db",
diff --git a/docker/site-a-node-b/appsettings.Site.json b/docker/site-a-node-b/appsettings.Site.json
index 7a3a9dec..f1db4f2a 100644
--- a/docker/site-a-node-b/appsettings.Site.json
+++ b/docker/site-a-node-b/appsettings.Site.json
@@ -26,7 +26,8 @@
"DataConnection": {
"ReconnectInterval": "00:00:05",
"TagResolutionRetryInterval": "00:00:10",
- "WriteTimeout": "00:00:30"
+ "WriteTimeout": "00:00:30",
+ "SeedReadTimeout": "00:00:30"
},
"StoreAndForward": {
"SqliteDbPath": "/app/data/store-and-forward.db",
diff --git a/docker/site-b-node-a/appsettings.Site.json b/docker/site-b-node-a/appsettings.Site.json
index 88a7c8af..93018c13 100644
--- a/docker/site-b-node-a/appsettings.Site.json
+++ b/docker/site-b-node-a/appsettings.Site.json
@@ -26,7 +26,8 @@
"DataConnection": {
"ReconnectInterval": "00:00:05",
"TagResolutionRetryInterval": "00:00:10",
- "WriteTimeout": "00:00:30"
+ "WriteTimeout": "00:00:30",
+ "SeedReadTimeout": "00:00:30"
},
"StoreAndForward": {
"SqliteDbPath": "/app/data/store-and-forward.db",
diff --git a/docker/site-b-node-b/appsettings.Site.json b/docker/site-b-node-b/appsettings.Site.json
index 9334191d..755b0b0a 100644
--- a/docker/site-b-node-b/appsettings.Site.json
+++ b/docker/site-b-node-b/appsettings.Site.json
@@ -26,7 +26,8 @@
"DataConnection": {
"ReconnectInterval": "00:00:05",
"TagResolutionRetryInterval": "00:00:10",
- "WriteTimeout": "00:00:30"
+ "WriteTimeout": "00:00:30",
+ "SeedReadTimeout": "00:00:30"
},
"StoreAndForward": {
"SqliteDbPath": "/app/data/store-and-forward.db",
diff --git a/docker/site-c-node-a/appsettings.Site.json b/docker/site-c-node-a/appsettings.Site.json
index 33b17691..40d2269f 100644
--- a/docker/site-c-node-a/appsettings.Site.json
+++ b/docker/site-c-node-a/appsettings.Site.json
@@ -26,7 +26,8 @@
"DataConnection": {
"ReconnectInterval": "00:00:05",
"TagResolutionRetryInterval": "00:00:10",
- "WriteTimeout": "00:00:30"
+ "WriteTimeout": "00:00:30",
+ "SeedReadTimeout": "00:00:30"
},
"StoreAndForward": {
"SqliteDbPath": "/app/data/store-and-forward.db",
diff --git a/docker/site-c-node-b/appsettings.Site.json b/docker/site-c-node-b/appsettings.Site.json
index 5a69e034..93a6b292 100644
--- a/docker/site-c-node-b/appsettings.Site.json
+++ b/docker/site-c-node-b/appsettings.Site.json
@@ -26,7 +26,8 @@
"DataConnection": {
"ReconnectInterval": "00:00:05",
"TagResolutionRetryInterval": "00:00:10",
- "WriteTimeout": "00:00:30"
+ "WriteTimeout": "00:00:30",
+ "SeedReadTimeout": "00:00:30"
},
"StoreAndForward": {
"SqliteDbPath": "/app/data/store-and-forward.db",
diff --git a/docs/requirements/Component-DataConnectionLayer.md b/docs/requirements/Component-DataConnectionLayer.md
index bfedf58f..b5a7761f 100644
--- a/docs/requirements/Component-DataConnectionLayer.md
+++ b/docs/requirements/Component-DataConnectionLayer.md
@@ -174,6 +174,7 @@ These are configured via `DataConnectionOptions` in `appsettings.json`, not per-
| `ReconnectInterval` | 5s | Fixed interval between reconnection attempts |
| `TagResolutionRetryInterval` | 10s | Retry interval for unresolved tag paths |
| `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
diff --git a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs
index 4432cbc3..0cdd89ef 100644
--- a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs
@@ -780,9 +780,16 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
{
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
{
- var readResult = await adapter.ReadAsync(tagPath);
+ var readResult = await adapter.ReadAsync(tagPath, cts.Token);
if (readResult.Success && readResult.Value is { Value: not null } value)
{
seedValues.Add(new SeededValue(tagPath, value));
@@ -792,6 +799,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
catch
{
// Best-effort read — retried below, or logged once the budget is spent.
+ // Includes OperationCanceledException on SeedReadTimeout expiry.
}
}
diff --git a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/DataConnectionOptions.cs b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/DataConnectionOptions.cs
index d1558375..b4da3d55 100644
--- a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/DataConnectionOptions.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/DataConnectionOptions.cs
@@ -14,6 +14,16 @@ public class DataConnectionOptions
/// Timeout for synchronous write operations to devices.
public TimeSpan WriteTimeout { get; set; } = TimeSpan.FromSeconds(30);
+ ///
+ /// DataConnectionLayer-027: per-tag timeout applied to each
+ /// call on the initial-subscribe seed path (and reconnect re-seed). A hung device read would
+ /// otherwise delay SubscribeCompleted 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 , then left Uncertain until a change
+ /// notification arrives.
+ ///
+ public TimeSpan SeedReadTimeout { get; set; } = TimeSpan.FromSeconds(30);
+
///
/// Minimum time a connection must stay up before it is considered stable.
/// If a connection drops before this threshold, it counts as an unstable