Skip to content

FIX: improve how we interact with the asyncio event loop - #1125

Merged
danielballan merged 147 commits into
bluesky:masterfrom
danielballan:run-eventloop-continuously
Jun 5, 2019
Merged

danielballan merged 147 commits into
bluesky:masterfrom
danielballan:run-eventloop-continuously

Conversation

@danielballan

@danielballan danielballan commented Dec 7, 2018 •

Copy link
Copy Markdown
Member

Do not merge. Needs work.

Closes #1062
Closes #1087

TODO:

  • rename run_matplotlib_qApp, it also handles the non Qt cases (semi-) gracefully
  • make stop, request_pause, abort, halt use the same pattern as request_suspend
  • make sure it actually works in a jupyter notebook and figures are responsive
  • discuss wait_for api changes
  • remove _delayed_partial which is a poorly implemented version of threading.Timer

@danielballan

Copy link
Copy Markdown
Member Author

This is essentially bait left to nerd-snipe @tacaswell into finishing the Qt integration sketched in #1062.

@danielballan

Copy link
Copy Markdown
Member Author

Status update:

Basic scanning and live-updating plots now works in both IPython and Jupyter (including with tornado > 5). Pause/resume is currently broken.

@danielballan

Copy link
Copy Markdown
Member Author

Now that this PR has rendered install_qt_kicker() unnecessary, it would be nice to do the same for install_nb_kicker(). One way to do this would be to check the matplotlib backend and automatically install the kicker on the "bluesky event loop" if it's nbAgg.

Comment thread bluesky/utils.py Outdated
@danielballan

Copy link
Copy Markdown
Member Author

This is I think, now basically correct. Several tests that stopped/restarted the event loop need to be updated, and then we're done.

To surface some in-person comments by @tacaswell --- his work on https://github.com/NSLS-II/mily has surfaces several other ways that we could consider architecting this. The current approach may not be the best one in the long term, though it's unclear yet which approach is best.

This is detail-oriented work that requires focus that is in short supply at the moment, and I propose completing and merging the current approach. Our incompatibility with tornado>=5 is a known stumbling point for users who are evaluating the library.

@stuartcampbell

Copy link
Copy Markdown
Member

If this PR makes it possible to work with tornado>5 and is a good 'first attempt' then I would be in favour of merging and then revisiting / refactoring when we are clearer what the best approach is.

@tacaswell
tacaswell force-pushed the run-eventloop-continuously branch from cca48a1 to eb7aa90 Compare April 3, 2019 19:59
@tacaswell

Copy link
Copy Markdown
Contributor

I can locally sometimes get the test to run through without hanging or segfaulting.

@tacaswell
tacaswell force-pushed the run-eventloop-continuously branch from 78ee3c5 to c617c47 Compare May 1, 2019 10:58
@tacaswell

Copy link
Copy Markdown
Contributor

force-pushed to fix ci typo.

@tacaswell
tacaswell force-pushed the run-eventloop-continuously branch from bac7ce5 to 77d083c Compare May 3, 2019 14:21
@tacaswell
tacaswell force-pushed the run-eventloop-continuously branch from 04f25e7 to 09550e2 Compare May 11, 2019 17:45
@tacaswell

Copy link
Copy Markdown
Contributor

rebased to account for conflicts in .travis.yml.

Will likely amend and force-push the last commit as well.

@tacaswell

Copy link
Copy Markdown
Contributor

The justification for the API change to wait_for is in these two snippets:

Old way:

import asyncio
import time
start = time.monotonic()


async def f1():
    loop = asyncio.get_event_loop()
    ev = asyncio.Event()

    print('launch the task', time.monotonic() - start)
    tsk = loop.create_task(f2(ev.wait()))
    loop.call_later(2, ev.set)
    print('sleep for a bit (1s)', time.monotonic() - start)
    await asyncio.sleep(1)
    print('cancel the task', time.monotonic() - start)
    tsk.cancel()
    print('sleep for a bit', time.monotonic() - start)
    print('really cancelled?: ', tsk.cancelled(), time.monotonic() - start)
    ex = await tsk
    print('victory!, should be ~2s', time.monotonic() - start)
    if ex:
        raise ex


async def f2(fut):
    while True:
        try:
            print('  try to wait', time.monotonic() - start)
            (t,), _ = (await asyncio.wait([fut]))
            print('  and escape later', time.monotonic() - start)
            return t.exception()
        except Exception as ex:
            print('  Should fail once', time.monotonic() - start)
            print(repr(ex))




loop = asyncio.get_event_loop()
loop.run_until_complete(f1())

New way:

import asyncio
import time
start = time.monotonic()


async def f1():
    loop = asyncio.get_event_loop()
    ev = asyncio.Event()

    print('launch the task', time.monotonic() - start)
    tsk = loop.create_task(f2(ev.wait))
    loop.call_later(2, ev.set)
    print('sleep for a bit (1s)', time.monotonic() - start)
    await asyncio.sleep(1)
    print('cancel the task', time.monotonic() - start)
    tsk.cancel()
    print('sleep for a bit', time.monotonic() - start)
    print('really cancelled?: ', tsk.cancelled(), time.monotonic() - start)
    ex = await tsk
    print('victory!, should be ~2s', time.monotonic() - start)
    if ex:
        raise ex


async def f2(fut):
    while True:
        try:
            print('  try to wait', time.monotonic() - start)
            (t,), _ = (await asyncio.wait([fut()]))
            print('  and escape later', time.monotonic() - start)
            return t.exception()
        except Exception as ex:
            print('  Should fail once', time.monotonic() - start)
            print(repr(ex))




loop = asyncio.get_event_loop()
loop.run_until_complete(f1())

The old way "works" on py3.6+ in that the timing is correct, but the inner task still sets a RuntimeError due to trying to re-await a coroutine. In 3.5 this fails immediately (which is why the tests were failing) but it is no less wrong on 3.6+ and there is no garuntee that it won't re-break in the future. The new way works everywhere. I strongly suspect that there is no non-test use of 'wait_for' in the wild (it is under documented and any use of it would likely be in a plan that is less than ideal anyway as I don't see how to make use of 'wait_for' and still have a plan that you can pass through list with no side-effects).

@tacaswell
tacaswell force-pushed the run-eventloop-continuously branch from 09550e2 to e09e1e4 Compare May 11, 2019 20:54
@tacaswell

Copy link
Copy Markdown
Contributor

However

import asyncio
import time
from asyncio.futures import Future
start = time.monotonic()


async def f1():
    loop = asyncio.get_event_loop()
    fut = Future()

    print('launch the task', time.monotonic() - start)
    tsk = loop.create_task(f2(fut))
    loop.call_later(2, fut.set_result, 3)
    print('sleep for a bit (1s)', time.monotonic() - start)
    await asyncio.sleep(1)
    print('cancel the task', time.monotonic() - start)
    tsk.cancel()
    print('sleep for a bit', time.monotonic() - start)
    print('really cancelled?: ', tsk.cancelled(), time.monotonic() - start)
    ex = await tsk
    print('victory!, should be ~2s', time.monotonic() - start)
    if ex:
        raise ex


async def f2(fut):
    while True:
        try:
            print('  try to wait', time.monotonic() - start)
            (t,), _ = (await asyncio.wait([fut]))
            print('  and escape later', time.monotonic() - start)
            return t.exception()
        except Exception as ex:
            print('  Should fail once', time.monotonic() - start)
            print(repr(ex))




loop = asyncio.get_event_loop()
loop.run_until_complete(f1())

Works correctly on all versions so the problem is passing on co-routines as futures. We should have a bit of a think about what to change this API to.

@tacaswell
tacaswell force-pushed the run-eventloop-continuously branch 2 times, most recently from 17a1feb to 93fc013 Compare May 15, 2019 22:14
Comment thread .travis.yml Outdated
- pip install -r requirements.txt
- pip install -r dev-requirements.txt
- pip install -U ophyd --pre
- pip install pyqt5

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Should this be added to dev-requirements.txt?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I agree with @danielballan's suggestion.

Comment thread bluesky/run_engine.py Outdated
Comment thread bluesky/run_engine.py
Expected signature: f(md)
Expected return: updated scan_id value

during_task : callable, optional

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

👍 very clear

Comment thread bluesky/run_engine.py Outdated
Comment thread bluesky/run_engine.py
Comment thread doc/source/api_changes.rst
@tacaswell

Copy link
Copy Markdown
Contributor

#1191 (comment) @prjemian

That comment reminded me that interrupters exist and that this PR may break them. Would you be able to manually test this locally?

We really need to move "interuptors" upstream.

@tacaswell
tacaswell force-pushed the run-eventloop-continuously branch from e87fc03 to d6c0fce Compare May 21, 2019 22:31
@tacaswell

Copy link
Copy Markdown
Contributor

force-pushed to hide a dumb mistake

@codecov-io

codecov-io commented May 21, 2019 •

Copy link
Copy Markdown

Codecov Report

Merging #1125 into master will increase coverage by 0.08%.
The diff coverage is 97.49%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1125      +/-   ##
==========================================
+ Coverage   89.24%   89.33%   +0.08%     
==========================================
  Files          52       52              
  Lines        8600     8766     +166     
==========================================
+ Hits         7675     7831     +156     
- Misses        925      935      +10
Impacted Files Coverage Δ
bluesky/tests/utils.py 97.22% <100%> (+0.07%) ⬆️
bluesky/suspenders.py 89.15% <100%> (ø) ⬆️
bluesky/tests/test_scans.py 100% <100%> (ø) ⬆️
bluesky/tests/test_new_examples.py 99.39% <100%> (ø) ⬆️
bluesky/plan_stubs.py 96.19% <100%> (ø) ⬆️
bluesky/tests/test_zmq.py 80.43% <100%> (ø) ⬆️
bluesky/tests/test_suspenders.py 100% <100%> (ø) ⬆️
bluesky/preprocessors.py 94.73% <100%> (+0.01%) ⬆️
bluesky/tests/test_examples.py 97.89% <100%> (+0.03%) ⬆️
bluesky/tests/conftest.py 87.17% <50%> (-7.27%) ⬇️
... and 6 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 72588b7...f75d128. Read the comment docs.

@tacaswell tacaswell changed the title WIP: improve how we interact with the asyncio event loop FIX: improve how we interact with the asyncio event loop May 21, 2019
@tacaswell
tacaswell force-pushed the run-eventloop-continuously branch from 3deaa7c to c30c157 Compare May 31, 2019 02:54
@tacaswell
tacaswell force-pushed the run-eventloop-continuously branch from a98ad7d to 34bbb59 Compare May 31, 2019 16:09

@klauer klauer left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This looks to be a great improvement, but also a very hard to review one...

Take these comments with a grain of salt, as I'm distractedly reviewing during our weekly meeting ;)

Comment thread bluesky/run_engine.py
Comment thread bluesky/run_engine.py Outdated
Comment thread bluesky/run_engine.py Outdated
Comment thread bluesky/run_engine.py Outdated
Comment thread bluesky/run_engine.py Outdated
Comment thread bluesky/run_engine.py
futs, = msg.args
yield from asyncio.wait(futs, loop=self.loop, **msg.kwargs)
futs = [f() for f in futs]
await asyncio.wait(futs, loop=self.loop, **msg.kwargs)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

https://docs.python.org/3/library/asyncio-task.html#asyncio.wait
The loop argument is deprecated and scheduled for removal in Python 3.10

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same comment as the other one, want to deal with this in a follow up PR as we need to sort out what the minimum version of python that correctly works without loop is.

Comment thread bluesky/utils.py
Comment thread bluesky/utils.py
will do nothing.

It is safe to call this function multiple times.
DEPRECATED: This functionality is now handled automatically by default and

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎆

Comment thread bluesky/utils.py
wakeupsn.activated.connect(handleWakeup)

else:
# On Windows, non-blocking anonymous pipe or socket is

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Has any interactive testing been done on Windows with this?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We do have at least one collection machine on the floor of NSLS-II, but we haven't tried. I don't know if we have a chance to test it from the PR branch before it gets merged.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this was adapted from tortioushg which does get heavy windows use so I am optimistic.

Comment thread doc/source/api_changes.rst Outdated
@tacaswell

Copy link
Copy Markdown
Contributor

The mac test failures are very confusing as they are UnbouldLocalError s from inside of a list comprehension that works on linux. Going to restart the 3.7 mac on the last passing build.

@mrakitin mrakitin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Here are a few comments for the first part. run_engine.py and tests will go next.

Comment thread .travis.yml Outdated
- pip install -r requirements.txt
- pip install -r dev-requirements.txt
- pip install -U ophyd --pre
- pip install pyqt5

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I agree with @danielballan's suggestion.

Comment thread setup.py Outdated
Comment thread .travis.yml Outdated

services:
- xvfb

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this section is not needed for testing, as discovered in #1203.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pushed a commit to see if it works (but am skeptical).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Comment thread .travis.yml
# plot everything, independent or dependent variables
self._table = LiveTable(list(self.all_dim_fields) + columns)
self._table('start', self._start_doc)
self._table('descriptor', doc)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

👍
That particular change resolves the issue with arranging live table and live plot in a Jupyter notebook.

Comment thread bluesky/utils.py
wakeupsn.activated.connect(handleWakeup)

else:
# On Windows, non-blocking anonymous pipe or socket is

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We do have at least one collection machine on the floor of NSLS-II, but we haven't tried. I don't know if we have a chance to test it from the PR branch before it gets merged.

Comment thread bluesky/callbacks/mpl_plotting.py
@tacaswell

Copy link
Copy Markdown
Contributor

The issue was due to pytest trying to un-roll assert all(...) which they are in the process of reverting matplotlib/matplotlib#14403 .

@tacaswell

Copy link
Copy Markdown
Contributor

I think this is ready to merge!

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.

Matplotlib import errors improve how we interact with the asyncio event loop

6 participants