Repository navigation
Run the sync tcp client on the async client (draft) - #3088
sankalpsthakur wants to merge 4 commits into
Conversation
ModbusTcpClient now runs an AsyncModbusTcpClient on a private event loop in a daemon thread. Each request is submitted with asyncio.run_coroutine_threadsafe(), so the sync client uses the async transaction code and also works when the calling thread runs a loop. The loop starts on connect and stops on close. The TLS, UDP and serial clients keep their sync code for now.
|
Please check that an exception raised in the async part goes back to the app....I remember having that problem in the server. |
janiversen
left a comment
There was a problem hiding this comment.
Looks good ! I have added a small number of questions/comments.
| self.use_udp = False | ||
| self._loop_thread: _LoopThread | None = None | ||
| self._async_client: ModbusBaseClient | None = None | ||
| self._custom_responses: list[type[ModbusPDU]] = [] |
There was a problem hiding this comment.
Why do we need to store the custom responses, once registred they are kept in the pdu module.
There was a problem hiding this comment.
They are kept per decoder: DecodePDU.register() copies pdu_table into the instance, so the async client created on connect did not know them. I removed the list, the async client now uses the decoder of the sync client, so registered responses are known to both.
|
|
||
| :meta private: | ||
| """ | ||
| return AsyncModbusTcpClient(**self._async_args, reconnect_delay=0) # type: ignore[arg-type] |
There was a problem hiding this comment.
it would be nicer with functools.partial
There was a problem hiding this comment.
Done, the tcp client sets self._async_client_factory = functools.partial(AsyncModbusTcpClient, ...).
| from pymodbus.simulator import DataType, SimData, SimDevice | ||
|
|
||
|
|
||
| def free_port() -> int: |
There was a problem hiding this comment.
please see the other tests, we use a standard fixture use_port
There was a problem hiding this comment.
Done, the test class uses use_port with a new entry in BASE_PORTS.
| return sock.getsockname()[1] | ||
|
|
||
|
|
||
| class ServerThread: # pylint: disable=too-few-public-methods |
There was a problem hiding this comment.
There is a standard fixture for this.
There was a problem hiding this comment.
Done, the tests use mock_server. The server runs on the test loop, so the sync calls go through asyncio.to_thread().
The tcp client creates its async counterpart with functools.partial. The async client shares the decoder of the sync client, so the list of registered custom responses is no longer needed. The tests use the standard use_port and mock_server fixtures, run with _run_async True and False, and check that an exception raised in the async client reaches the caller.
|
Checked: |
|
Kept the 5 tests that mock the socket on the old sync code path, so they test what they did before. |
janiversen
left a comment
There was a problem hiding this comment.
LGTM, with one expection test_sync_client only run the old tests, please confirm that the new test file contains the same tests and possible more.
It is important to show that any old app will still work, basically ensuring that test_sync_client works in both implementations. Of course it is perfectly ok, if you state the tests are all in the new file.
|
I suggest to make a PR (later) for each type of client to make bisecting easier. |
|
not all of them, so i added a commit. the tcp tests in to show that old apps still work, 490d06d runs the tcp combinations of agree on one PR per client type, i will do udp, tls and serial separately once this one is in |
Draft, to discuss the approach from #3076 on real code. Only the tcp client is converted.
ModbusTcpClientnow creates anAsyncModbusTcpClienton a private event loop that runs in a daemon thread.connect()starts the loop and connects,execute()submits the request withasyncio.run_coroutine_threadsafe()and waits for the result,close()closes the async client and stops the loop. The async client getsreconnect_delay=0, because the sync client connects when a request needs it.The mechanism is in
ModbusBaseSyncClient(_LoopThread,_connect_async,_close_async), so the other clients only need_new_async_client()and_run_async = True. The TLS client keeps its sync code for now, UDP and serial are unchanged.Open points:
test_client_sync.pyandtest_client.pymocksocket.create_connection/socket.sendfor the tcp client and fail now. I'd rewrite them against a server, like the newtest_client_sync_loop.py, once the approach is agreed.Validation
pytest test/client/test_client_sync_loop.py: 6 passed (real server: read, connect on request, with block, failed connect leaves no thread, call from a thread that runs a loop, set_max_no_responses).pytest test/client: 242 passed, 8 failed (the 5 above, and 3 tls tests that cannot find the certificates in my worktree, also on dev).ruff check,ruff format --check,codespellandpylint(10.00/10) are clean;mypyreports the same findings as ondev.Related to #3076.
AI disclosure: written with an AI coding agent and tested as described above.