You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Classify upstream failures before retrying, enforce one end-to-end
request budget, and preserve Modbus exception responses. Reads keep one
safe transport retry while ambiguous writes fail without being repeated.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2fa557c2-9b1f-4cf7-8e68-3a5141cd5261
The end-to-end request budget always caps each individual attempt. A full read
45
+
retry budget needs room for two attempt timeouts, two connect delays, request
46
+
pacing, and any dial time. Pacing is a context-aware pre-wire wait charged to
47
+
the next request's budget; it never delays an already received response.
48
+
Existing configurations may keep using `MODBUS_TIMEOUT` during migration.
49
+
When both attempt timeout variables are set, their parsed durations must match
50
+
or startup fails. Neither setting changes `MODBUS_REQUEST_TIMEOUT`.
51
+
52
+
Downstream exceptions preserve genuine upstream Modbus exception responses with
53
+
nonzero exception codes. Upstream transport, framing or malformed exception
54
+
failures and total request deadlines map to gateway target failed to respond
55
+
(`0x0B`). Local internal failures map to server failure (`0x04`), while local
56
+
validation uses the standard validation exception codes.
57
+
42
58
`/mbproxy -health` performs an internal upstream connectivity check and does not open a separate local TCP health port.
43
59
44
60
### Read-Only Modes
@@ -77,7 +93,8 @@ services:
77
93
MODBUS_CACHE_TTL: "10s"
78
94
MODBUS_CACHE_SERVE_STALE: "false"
79
95
MODBUS_READONLY: "true"
80
-
MODBUS_TIMEOUT: "10s"
96
+
MODBUS_ATTEMPT_TIMEOUT: "10s"
97
+
MODBUS_REQUEST_TIMEOUT: "30s"
81
98
MODBUS_REQUEST_DELAY: "0"
82
99
MODBUS_CONNECT_DELAY: "0"
83
100
MODBUS_SHUTDOWN_TIMEOUT: "30s"
@@ -137,9 +154,9 @@ docker run --rm -v $(pwd):/app -w /app golang:1.24 go test ./...
137
154
-**Key format**: values are cached per register/coil as `{slave_id}:{function_code}:{address}`
138
155
-**Read requests**: Served from cache only if every register/coil in the requested range is present and not expired
139
156
-**Cache misses**: If any value in the requested range is missing or expired, the full range is fetched from upstream and decomposed into per-register/coil cache entries
140
-
-**Write requests**: Forwarded to upstream (if allowed), then invalidate the written address range so overlapping cached reads cannot return stale values
141
-
-**Request coalescing**: Multiple identical range requests during a cache miss share a single upstream fetch using `{slave_id}:{function_code}:{start_address}:{quantity}` as the coalescing key
142
-
-**Stale fallback**: If enabled, expired entries are retained and can be served when upstream requests fail
157
+
-**Write requests**: Before an allowed write is forwarded, its generation is incremented and the written range is invalidated. The generation is incremented and the range invalidated again after every outcome, so neither older reads nor reads that execute in the write scheduling window can leave pre-write values cached.
158
+
-**Request coalescing**: Multiple identical range requests in the same write generation share a single upstream fetch using `{write_generation}:{slave_id}:{function_code}:{start_address}:{quantity}` as the coalescing key
159
+
-**Stale fallback**: If enabled, expired entries are retained and can be served when upstream transport requests fail. Upstream Modbus exceptions are never replaced with stale data.
-**Read Operations**: Check the per-register/coil cache first. Return from cache only if every value in the requested range is present and not expired.
74
-
-**Cache Misses**: If any value in the requested range is missing or expired, fetch the full requested range from upstream, then decompose the response into per-register/coil cache entries.
75
-
-**Write Operations**: Always forward to the device when writes are allowed, then invalidate each cached register/coil in the written address range. This prevents overlapping cached read ranges from serving stale values after frequent writes.
75
+
-**Cache Misses**: If any value in the requested range is missing or expired, fetch the full requested range from upstream, then decompose the response into per-register/coil cache entries only if the write generation is unchanged.
76
+
-**Write Operations**: Before forwarding an allowed write, increment the write generation and invalidate each cached register/coil in the written address range. After every write outcome, increment and invalidate again so a read that entered the new generation but executed before the write cannot leave a pre-write value cached. This preserves ambiguous-write invalidation without holding the cache state lock across upstream I/O.
76
77
-**TTL**: Configurable (default: 10 seconds)
77
78
-**Cleanup**: Time-based expiration. Expired entries are removed during cleanup unless stale serving is enabled.
78
79
-**Staleness**: Option to serve stale data on upstream failure (default: off). When enabled, expired entries are retained so they remain available for fallback.
79
80
80
81
### Request Coalescing
81
-
- Identical in-flight range requests are coalesced (same slave_id, function, address, quantity)
82
+
- Identical in-flight range requests are coalesced within the same write generation
82
83
- Second request arriving while first is pending will wait for and share the first's response
83
84
- Prevents thundering herd on cache miss
84
85
85
86
### Request Pacing
86
-
- Configurable delay after each successful upstream request
87
+
- Configurable minimum interval measured from each successful upstream response
87
88
- Protects slow Modbus devices that cannot handle rapid-fire requests
88
-
- Delay is context-aware: cancelled if the request context is cancelled
89
-
- Only applied after successful requests (not during error recovery/reconnection)
90
-
- Logged at DEBUG level when applied
89
+
- Enforced as a context-aware pre-wire wait for the next request
90
+
- Consumes the next request's end-to-end budget and never delays or reclassifies the completed request
91
+
- Not reapplied between a failed read attempt and its retry
92
+
- Logged at DEBUG level when a request waits for its slot
0 commit comments