Repository navigation
Discard the late response of a cancelled request - #3076
Conversation
|
I just tried your test case, as expected it shows the problem, so with that I will try
I do suspect however that the problem might be older, because I can produce the problem without cancelling:
This is of course a configuration problem (timeout too small), but the effect seems identical to cancelling. I will keep you informed of my findings, because your method to solve might be the best one. |
|
Used your test case on v3.15.0 modified line 321: to adapt to the non passing of cancel exception. Your test FAILS, meaning this is as suspected a very old problem. Your fix raises a big problem, because it should be allowed to send a request to another device on the same line, without delay: Ex 1:
Ex 2:
If we get those 2 examples working, the only outstanding problem is if timeout is configured too low:
This seems theoretically insolvable, but it is a configuration problem. |
|
I have updated your test case in #3077, to cover the scenarios I think are important. Please have a look at it, and feel free to amend commits if something seems wrong. As you can see, it causes a bit of problems with your PR; however I think you are on the right road. And of course you are added as co-author, since the original comes from your PR. |
|
thanks for writing the scenarios. i ran your test from #3077 here. async def _cancel_send(self, transact, transport, delay_cond, pdu):
"""Create task and wait."""
+ writes = transport.write.call_count
resp = asyncio.create_task(transact.execute(False, pdu))
delay = 0
- while transport.write.call_count < 2:
+ while transport.write.call_count == writes:
await asyncio.sleep(0.1)
delay += 1
@@
case "resend": # resend within timeout, must delay until after timeout
- resp = asyncio.create_task(transact.execute(False, req_org2))
await asyncio.sleep(0.1)timeout and new_id pass on dev, on ce90cff and on bbf30db, which i just pushed: after a cancel only the next request to the same device waits, and requests to other devices and requests with a transaction id go out at once. (new_id also passes with the old global wait, because with timeout 0.5 the wait ends within 5 ticks.
and how do you want to land it: rebase this PR on #3077, or take your test into this one? both change |
|
I am happy you liked the scenarios, it was easier than writing pseudo code :-) I have added your 2 first suggestions, thanks they are very much correct, and I have merged the PR to dev. Your 3 suggestion (resend) is not added, since it is a resend WITHIN the timeout, actually this is the most likely scenario....app cancels the request, and immediately reissued the same request. If you saw something I did not see in this scenario, please add your change again, but please explain why mine is wrong. resend: I am happy with the delay being timeout_connect, not considering cancel time, whatever is easier for you, new_fc: I think I just changed that on dev. Sorry it was a mix of 2 places making the same check. the message should be dropped. I think I made a wrong assert, feel free to correct it. Please rebase your PR on dev, and remove "pragma: no cover", "mark.skip" in transaction.py in your pr. Thanks a lot for your work, this is NOT an easy task and you are doing a great job !!!! I really hope you would like to help with other interesting problems later. |
|
Sorry the conflict is because I added a test to check that wrong responses are discarded. |
3d6fcf9 to
03343a6
Compare
|
thanks, glad the scenarios helped, and thanks for the kind words. i rebased this PR on dev and removed the skip and the new_fc: dev drops the response in resend: your scenario is right, the app cancels and sends the same request again at once. but the test had two requests: the
the delay keeps the cancel time, since that was already in place. i also removed the guard and its pragma in i'd be glad to help with other problems later too. |
|
The resend was my bad, thanks for catching it. I will review your code a bit later. |
janiversen
left a comment
There was a problem hiding this comment.
LGTM, nice idea with the dict key as tuple.
| raise self._io_exception_from_request(txt, request) | ||
|
|
||
| async def execute( | ||
| async def execute( # noqa: C901 |
There was a problem hiding this comment.
Would be nice with a followup PR that got rid of the noqa, by moving a part out in a new method
|
thanks for merging, and for writing the scenarios. happy to take the next one: is there a problem you'd like me to look at? |
|
For a starter please submit a PR, making the noqa unnecessary We try to solve the issues/problems when they arrives, so there are no open problems, but there are a number of pending enhancements:
These are all bigger problems, but can of course be split in chunks and thus something easier to make and to review. |
understood |
When an async
execute()is cancelled while it waits, the device can still answer after the next request is sent. With RTU/ASCII, which have no transaction id, and the same function code, the next request to that device then returns that stale data, as in #3070.execute()keeps a fresh response future for the late response, and the nextexecute()to the same device with the same function code waits for it before sending, at mosttimeout_connectafter the cancel. Requests to other devices, with another function code, or with a transaction id (socket framers, wherehandleFrameskips an old transaction id) are not delayed.callback_dataskips a response whose function code differs from the request's, before it sets the response future. Test wrong response gets discarded. #3078 drops it inexecute(), after it has taken the future, so a correct response right behind it was ignored and the request was sent again.CancelledErrorstill propagates as in Let CancelledError propagate from async execute #3010, and there is no API change.Validation
pragma: no coverremoved): timeout, resend, resend_response, new_id and new_fc. Ondev(45c8933), resend, resend_response and new_fc fail; with this change all pass.test_transaction_execute_cancel_no_wait(new): after a cancel, a request to another device, a request with another function code (FramerRTU) or a request with a transaction id (FramerSocket) is sent at once and gets its own response. The function code case fails ondev.pytest test/transaction: 54 passed on Python 3.10.18 and 3.14.7 (macOS), with 100% line and branch coverage ofpymodbus/transactionandtest/transaction.ruff check,ruff format --check,codespellandzuban checkon the touched files are clean, andpylintreports nothing new.execute()was already at the C901 limit of 10, so it gets# noqa: C901, likeconvert_from_registers.Fixes #3070
AI disclosure: written with an AI coding agent and tested as described above.