Repository navigation
Conversation
…rtTransport When communicating via a local proxy (such as modbus-proxy) or with a stalled device MCU, TCP connections succeed immediately while subsequent PDU transactions time out. Previously, TimeoutError did not trigger reconnection flags, and Tenacity's reconnection attempt counter reset on each separate send_and_receive invocation, causing rapid ~1 Hz socket hammering. Key changes: - Expand TRANSPORT_FAILURES to include TimeoutError, ModbusConnectionError, and ConnectionError - Add persistent cross-call recovery backoff state (_recovery_failure_count, _next_reconnect_earliest) - Provide immediate retry for initial failures (fast-recovery), with exponential backoff (1s, 2s, 4s, ... up to 60s) for consecutive recovery failures - Reset recovery state upon decoded PDU reception or decoded Modbus exception (ModbusResponseError) - Reset recovery state on explicit public close() - Preserve existing _communication_lock, pacing, and on_reconnected invariants - Add comprehensive unit tests covering persistent backoff, failure taxonomy, and state resets
|
Hi, thank you for this PR. I'm currently on holiday with only a smartphone on me, so I'll review this in full when I return home in two weeks time. One problem I have with your current approach is that it introduces an extra backoff mechanism on top of the tenacity one, which is not configurable by the user. I designed the API in such a way that all retry-logic can be expressed in tenacity-primitives, and I want to keep it that way. (For example: if an user of this library wants a constant backoff instead of an exponential one) |
…acity primitives Align with Tenacity API conventions by removing the custom exponential backoff formula and delegating delay calculation directly to auto_reconnect.wait: - Track recovery state using Tenacity's RetryCallState across calls - Set outcome exception before computing wait delay to support exception-based wait strategies - Safely invoke before_sleep callback (sync and async) with upcoming_sleep - Advance attempt_number via prepare_for_next_attempt() after wait calculation - Support custom wait strategies (such as wait_fixed) configured by the user - Add unit tests for wait_fixed, attempt_number sequencing, and sync/async before_sleep
|
Hi @wlcrs, I went ahead and refactored the PR to align with your Tenacity-based design:
Enjoy the rest of your holiday, and take your time reviewing whenever you're back! |
|
Ok, now that I'm back from holiday I reviewed your PR. Maybe it's the jetlag, but it is very hard for me to get a mental image of the whole state machine that is proposed in this PR. And in terms of complexity that is where I draw the line: I must be able to explain it to myself so that I can also explain it to others. I went back to the drawing board to try and solve your issue in a simpler manner. Can you please review and test https://github.com/wlcrs/tmodbus/compare/improve-smart-reconnect#diff-2dc315e3125f6327e87c2af74d57676af3b436c24b615b6e21126303b0e306eb instead? In this alternate approach, we improve the existing retry-loop instead of creating a secondary loop on top of it. As we already have the |
|
Hi @wlcrs, Thank you for looking into this and proposing the alternate approach in I completely understand your point regarding complexity and maintainability. Your solution is wonderfully simple and lean (~15 LOC), and relying on the existing I just tested your
From my side, I am more than happy to close my PR in favor of your Greetings, |
|
Thank you for confirming. I've merged the changes in #152 and they will be included in the next release. |
Problem Description
When using
AsyncSmartTransportin setups with a local proxy (e.g.modbus-proxy) or when a device's Modbus MCU is temporarily stalled or unresponsive, the TCP connection (_open()) succeeds immediately (<1 ms), which Tenacity considers a successful reconnect. However, subsequent PDU requests (send_and_receive()) time out.Previously:
TimeoutErrorat the transaction boundary did not mark_must_reconnect = True.send_and_receive()executed under a separate call context, Tenacity'sauto_reconnectattempt counter reset back to 1 on every invocation.huawei_solar), this resulted in continuous socket open/close cycles at ~1 Hz (>5,000 reconnects in 90 minutes), effectively hammering the proxy and hardware.Key Invariants Preserved
_communication_lockand inter-frame pacing (wait_between_requests) remain strictly intact.on_reconnectedremains at the canonical invocation point right after connection re-establishment and before any PDU transmission.async_tcp.pyand other base transports remain untouched.Proposed Solution
Introduced
_recovery_failure_countand_next_reconnect_earliestacross calls onAsyncSmartTransport._RECONNECT_BACKOFF_MAX = 60.0\,\text{s}._communication_lock.Expanded
TRANSPORT_FAILURESto(TimeoutError, ModbusConnectionError, ConnectionError).Recovery state is reset upon receiving either:
ModbusResponseError), confirming that the device MCU is alive and responding at the protocol level.Public
close()resets the recovery state, while internal transport drops during reconnection do not.Tests & Verification
tests/transport/test_async_smart.py:test_retry_with_new_connection_if_needed_includes_timeout_and_connection_errortest_timeout_at_transaction_boundary_marks_must_reconnect_and_closes_transporttest_persistent_backoff_progression_across_callstest_application_success_resets_recovery_statetest_modbus_response_error_resets_recovery_statetest_tcp_connect_failure_preserves_existing_auto_reconnect_behaviortest_on_reconnected_called_at_canonical_pointtest_public_close_resets_recovery_statemodbus-proxy, andhuawei_solar.