modbus: limit link error-recovery sleep+flush to confirmations#862
Open
JacksongXu wants to merge 1 commit into
Open
modbus: limit link error-recovery sleep+flush to confirmations#862JacksongXu wants to merge 1 commit into
JacksongXu wants to merge 1 commit into
Conversation
ETIMEDOUT triggers _sleep_response_timeout() + modbus_flush() regardless of msg_type. On the indication path this stalls a non-blocking server for response_timeout and can flush a client request that arrives during the sleep window, dropping it silently. Scope the recovery to MSG_CONFIRMATION, matching the client-side retry intent it was written for.
|
We require contributors to sign our Contributor License Agreement. In order for us to review and merge your code, please fill https://forms.gle/5635zjphDo5JEJQSA to get added. Your document will be manually checked by the maintainer. Be patient... |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Limit MODBUS_ERROR_RECOVERY_LINK's sleep+flush to MSG_CONFIRMATION
Symptom
Running a server (both RTU and TCP backends tested) on a non-blocking,
select()-driven receive loop withMODBUS_ERROR_RECOVERY_LINKenabled, client requests would occasionally go completely unanswered —
no error surfaced on either side, just a missing response. See
"Testing" below for how this was traced back to
_modbus_receive_msg().Root cause
In
_modbus_receive_msg(), whenselect()times out (errno == ETIMEDOUT)and
MODBUS_ERROR_RECOVERY_LINKis enabled, the library unconditionallyruns
_sleep_response_timeout()followed bymodbus_flush():This runs for both
msg_typevalues —MSG_CONFIRMATION(a clientwaiting for a response) and
MSG_INDICATION(a server/slave waiting forthe next request). That's a problem specifically for the indication case:
_sleep_response_timeout()blocks synchronously for up toresponse_timeout. A server built around a non-blocking,select()-driven event loop (servicing an RTU bus alongside otherfds/timers) gets stalled for that whole window, which defeats the
purpose of a non-blocking design.
modbus_flush()then discards the OS receive buffer. On a sharedRS485 bus, if the master already started sending a new request while
the server was sleeping, that request is silently dropped instead of
being picked up on the next loop iteration.
A timeout while waiting for an indication is routine, expected behavior
for a server that simply has no traffic yet. It isn't a link-layer
failure that warrants a reconnect-style sleep/flush sequence. That
sequence makes sense for a client that just sent a request and got no
response: sleeping for
response_timeoutand flushing stale bytesbefore retrying is a reasonable recovery there.
The docs already flag this indirectly:
This change is meant to make that recommendation unnecessary, rather
than something users have to remember.
Change
Scope the sleep+flush branch to
MSG_CONFIRMATIONonly:EBADFhandling is left unconditional — a bad file descriptor is agenuine link failure regardless of client/server role.
Compatibility
error_recoveryisMODBUS_ERROR_RECOVERY_NONE, so this is ano-op for anyone not already opting into
MODBUS_ERROR_RECOVERY_LINK.MODBUS_ERROR_RECOVERY_LINKclient users(
MSG_CONFIRMATIONpath), behavior is unchanged.wasn't documented as intentional and was already discouraged in the
docs — it doesn't change what a caller receives from
modbus_receive()on timeout (
-1/errno == ETIMEDOUT, same as before).Minimal reproduction
This reproduces reliably, not just intermittently under load, given:
response_timeout = 500ms(library default),indication_timeoutexplicitly set to 1s (default is unset, which blocks indefinitely —
see note below on why this matters),
MODBUS_ERROR_RECOVERY_LINKenabled, non-blocking
select()-driven receive loop.server's idle cycle — no special timing needed on the client side.
Root cause of why this reproduces so easily:
_sleep_response_timeout()always sleeps for
ctx->response_timeout, regardless of which timeoutactually fired the
ETIMEDOUT:With the settings above: every ~1s of idle time, the server's indication
wait times out (
indication_timeoutexpiring is the whole point ofsetting it explicitly for a non-blocking design — it's what lets the
event loop come back around instead of blocking forever). That timeout
then triggers a 500ms sleep (
response_timeout, unrelated to the 1sthat actually elapsed) followed by a flush. Any request the client sends
during that ~500ms window — which recurs every idle cycle — lands in the
OS receive buffer sometime before or during the flush and is discarded.
With uncoordinated client timing, the probability of a collision per
idle cycle is roughly
sleep_duration / cycle_length(≈500ms / ~1.5shere), which is high enough to show up quickly in normal operation, not
just as a rare race.
Note: this manifests when
indication_timeoutis explicitly set toa finite value. With the library default (
indication_timeoutunset,select()blocks indefinitely waiting for a request), the indicationwait itself never times out, so this path isn't triggered. Setting
indication_timeoutexplicitly is precisely what a non-blocking,select()-driven server needs to do to stay responsive to otherfds/timers in its event loop instead of blocking forever on one read,
which is why this shows up for exactly the kind of design libmodbus is
otherwise well-suited to.
Testing
This fix was arrived at empirically, not just by code inspection — see
"Symptom" above for how the drops were first observed.
non-blocking
select()loop,MODBUS_ERROR_RECOVERY_LINKenabled, concurrent client traffic. Before this change: client
requests are intermittently dropped by the server-side
modbus_flush()racing with in-flight request bytes. After thischange: no drops observed under the same traffic pattern, and
modbus_receive()still returns promptly (-1/errno == ETIMEDOUT) on genuine idle timeouts, with no sleep orflush performed on the indication path.
Notes
Happy to adjust scope/wording or add a test per maintainer preference —
this is a small, deliberately minimal change to keep review easy.