Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new timeout for observe_value #650

Merged
merged 11 commits into from
Dec 2, 2024
Merged

Add new timeout for observe_value #650

merged 11 commits into from
Dec 2, 2024

Conversation

coretl
Copy link
Collaborator

@coretl coretl commented Nov 14, 2024

@DominicOram @olliesilvester this is the epics test I mentioned in DiamondLightSource/dodal#897 (comment)

It looks like I need to yield after the q.get rather than before to let the timeout break without an extra yield

@coretl coretl requested a review from DominicOram November 14, 2024 15:11
@coretl
Copy link
Collaborator Author

coretl commented Nov 14, 2024

Spoke too soon, doesn't work on CI...

@coretl
Copy link
Collaborator Author

coretl commented Nov 15, 2024

Ok, looks like there are actually numerous python bugs in asyncio.wait_for, which is fixed by its reimplementation on top of asyncio.timeout in 3.12: python/cpython#96764

I think this fixes your particular use case, but you can't pass a timeout to observe_value and also wrap it in wait_for with a timeout on python 3.11 and below.

I would also suggest that you move the opencv calls to another thread and consume all the values from the Queue between the two, dropping old updates.

@olliesilvester
Copy link
Contributor

Great, thank you!

@coretl coretl marked this pull request as draft November 15, 2024 17:01
@coretl
Copy link
Collaborator Author

coretl commented Nov 15, 2024

Ok, still some issues will this, will investigate next week

observe_value runs in a lot of tests, so the testing checks for max_yields
needs to be much more than the one for observe_value otherwise they
fight each other
@coretl coretl marked this pull request as ready for review November 18, 2024 11:16
@coretl
Copy link
Collaborator Author

coretl commented Nov 18, 2024

@DominicOram @olliesilvester @rtuck99 I think this is now working, but would be good to talk through the code with one of you to check I understand the problem correctly

@olliesilvester
Copy link
Contributor

@DominicOram @olliesilvester @rtuck99 I think this is now working, but would be good to talk through the code with one of you to check I understand the problem correctly

I'm about today, I'm just reading through the changes now

@olliesilvester
Copy link
Contributor

Discussed with @coretl, I think this change fixes our issue but I will investigate further to see if there are any safer solutions

Copy link
Contributor

@DominicOram DominicOram left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks reasonable to me, especially if @olliesilvester has talked over it too. My only comment would be that if we think it's mostly a problem with asyncio.wait_for in <3.11 then we should add a comment/issue to drop it when we drop 3.11. But if we think there are other, more general, problems this might catch then maybe we shouldn't drop it.

@olliesilvester
Copy link
Contributor

I tried using asyncio.timeout instead, and we seem to get the same issue. I guess this explains why python 3.12 still had this issue.

I'm leaning towards just not using asyncio.wait_for for this scenario. We could just do

async def observe_value(...):
  if time.time() - start_time > overall_timeout:
                  raise TimeoutError
...

instead, and warn against using asyncio.wait_for (or timeout) with observe_value

@coretl
Copy link
Collaborator Author

coretl commented Nov 20, 2024

I'm leaning towards just not using asyncio.wait_for for this scenario

It's messy, but it's probably the most pragmatic solution here, then we relegate wait_for_pending_wakeups to ophyd_async.testing and only use it in tests

@DominicOram thoughts?

@olliesilvester when this is decided would you mind amending this PR or making a new one with the implementation please?

@DominicOram
Copy link
Contributor

I'm not a big fan of this, having to roll you're own timeout code when there's provision for it in asyncio looks on the face of it like a code-smell if you're not familiar with the bug. How would we feel about adding the functionality into observe_value itself e.g. optionally pass in a overall_timeout value? This would then make the path of least resistance be to just use that

@olliesilvester
Copy link
Contributor

Yeah, the timeout will be in observe_value as an optional parameter

@coretl coretl marked this pull request as draft November 22, 2024 10:00
@olliesilvester
Copy link
Contributor

olliesilvester commented Nov 28, 2024

I've given this a go but there are some extra things to think about

  1. The regular timeout defaults to None, which means that if we do our overall_timeout, as above, then the code can get stuck forever doing get_value. asyncio.wait_for protects against this, regardless of the other problem it caused

Solution here is either to either give timeout a default value or make a clever decorator which automatically supplies our overall_timeout parameter with the same value which was used in our timeout in asyncio.wait_for(timeout=...)

  1. If we don't go with the latter solution above, then we could add a warning (with a decorator?) if observe_value has been used with asyncio.timeout

I think the clever decorator is best here, assuming it isn't too hard to make

Thoughts @coretl?

@coretl
Copy link
Collaborator Author

coretl commented Nov 28, 2024

I suggest something like:

async def observe_value(signal, timeout=None, overall_timeout=None):
    overall_deadline = time.monotonic() + overall_timeout if overall_timeout else None
    ...
    while True:
        deadline = time.monotonic() + timeout if timeout else overall_deadline
        if deadline and overall_deadline:
            deadline = min(deadline, overall_deadline)
        value = await asyncio.wait_for(q.get(), deadline - time.monotonic() if deadline else 0) 
    ...

@olliesilvester
Copy link
Contributor

Although people will probably still do asyncio.wait_for(observe_value...), this issue (if anyone even sees it again) should be easier to spot with the extra docstring

@olliesilvester olliesilvester marked this pull request as ready for review November 29, 2024 10:43
@olliesilvester olliesilvester changed the title Add epics test for busy signals Add new timeout for observe_value Nov 29, 2024
src/ophyd_async/core/_signal.py Outdated Show resolved Hide resolved
src/ophyd_async/core/_signal.py Outdated Show resolved Hide resolved
src/ophyd_async/core/_signal.py Outdated Show resolved Hide resolved
@coretl coretl merged commit b846eab into main Dec 2, 2024
19 of 20 checks passed
@coretl coretl deleted the observe-epics branch December 2, 2024 17:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants