-
Notifications
You must be signed in to change notification settings - Fork 283
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
Handle warnings in tests #751
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for spending the necessary time on this @blink1073 - it's non-trivial and much appreciated! Just had a couple of comments.
Should we open an issue to move the remaining tests to pytest to rid ourselves of TestCase
? Perhaps someone with some bandwidth could take that on as a good intro?
jupyter_client/client.py
Outdated
if self._created_context: | ||
self.context.destroy() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should self._create_context
be unset here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
if self.has_kernel: | ||
await ensure_async(self.interrupt_kernel()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice - make this call to interrupt() conditional on start status. Good catch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍🏼
jupyter_client/tests/conftest.py
Outdated
if resource is not None: | ||
soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE) | ||
|
||
DEFAULT_SOFT = 4096 | ||
if hard >= DEFAULT_SOFT: | ||
soft = DEFAULT_SOFT | ||
|
||
old_soft, old_hard = resource.getrlimit(resource.RLIMIT_NOFILE) | ||
hard = old_hard | ||
if old_soft < soft: | ||
if hard < soft: | ||
hard = soft | ||
resource.setrlimit(resource.RLIMIT_NOFILE, (soft, hard)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find this a little difficult to understand. I think the double calls to getrlimit
are part of the confusion. Perhaps a comment as to the goal would be helpful. It appears to ensure a minimal soft limit of DEFAULT_SOFT
if the current hard limit is at least that much.
It seems like the hard < soft
(L30) condition will never be true because old_soft < soft
(L29) can only be true if soft
was increased to DEFAULT_SOFT
, which implies hard >= soft
, which leads me to wonder some kind of update to hard
is missing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cleaned up
It looks like this breaks the server tests, so I'll have to do both PRs at once. |
Okay, this part is done, stopping for today. |
jupyter_client/manager.py
Outdated
fut = Future() | ||
except RuntimeError: | ||
# No event loop running, use concurrent future | ||
fut = CFuture() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is inside an async def
, can this except branch ever occur? There should always be a running loop from inside a coroutine.
The previous strategy of setting _ready
outside a coroutine might not have a running loop, but now that you've moved it into the coroutine (good, I think), there should always be a running loop.
Returning two types of Future depending on context would be tricky because they aren't interchangeable (CFuture is not awaitable without calling asyncio.wrap_future(cfuture)
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An important difference of this strategy, though: the attribute will not be set until an arbitrarily-later point in time due to asyncness. The previous strategy ensured ._ready
was defined immediately before any waiting, whereas putting it in the coroutine means the attribute will not be defined immediately.
Two new things to consider (may well both be covered):
- previous waits for
self._ready
that may now be called when_ready
is not yet - can
_ready
be set and then re-set? If so, waits for_ready
may get a 'stale' state before the new_ready
future is attached. This can be avoided with a non-asyncdelattr
prior to the async bit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Originally _ready
was only defined in the constructor, which is why we had the fallback option. It seems like these futures are becoming problematic in general. Maybe what we really want is a state and a signal when the state changes, like we have on the JavaScript side.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternatively, if we want to define ready
and shutdown_ready
as "the most recent" starting and shutting down futures, we could also add an is_ready
and is_shutdown_ready
for when there are no pending of either. We would use this trick that tornado used to work around the new deprecation warnings:
loop = asyncio.get_event_loop_policy().get_event_loop() # this always returns the same loop
future = asyncio.Future(loop=loop)
We could then make .ready
and .shutdown
ready futures only available when using the AsyncKernelManager
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haha oh my, nevermind, I'm digesting https://bugs.python.org/issue39529
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think part of the problem is that jupyter_client
is a library, not an application, in the same way that tornado
is. It looks like we also can't rely on asyncio_run
in the future, since it relies on get_event_loop
and set_event_loop
. I think here's a sketch of what we need to do:
- The base classes will be asynchronous and call the the private async versions of functions directly as needed. We will only use futures and tasks from within async methods.
- The synchronous classes will work by wrapping the asynchronous public methods using a decorator that runs a new private asyncio loop as
return asyncio.new_event_loop().run_until_complete(async_method)
. - We remove the
.ready
future and add new methods calledasync def wait_for_pending_start()
andasync def wait_for_pending_shutdown()
to handle pending kernels.
With this new structure, the synchronous managers could also support pending kernels, breaking it into two synchronous steps.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can handle the ready future in this PR and defer the other parts to a separate refactor PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
btw, concurrent.futures are generally more compatible and flexible than asyncio futures - they are threadsafe, reusable across loops, etc. If you always use a cf.Future, you can await it in a coroutine with asyncio.wrap_future(cf_future)
. That may be the simplest solution here. It's the inconsistency that I saw as a problem (self._ready
may be awaitable). Something like:
# earlier, _not_ in a coroutine
self._ready = cf.Future()
async def wait_for_ready(self):
if self._ready is None:
raise RuntimeError("haven't started yet!")
# wrap cf.Future self._ready to make it awaitable
await asyncio.wrap_future(self._ready)
ought to be more reliable.
To always use new event loops (or asyncio.run
) for each sync method call, one important consideration is that all your methods completely resolve by the end of the coroutine (i.e. not hold references to the current loop for later calls, which means requiring pyzmq 22.2, among other things). A possibly simpler alternative is to maintain your own event loop reference. asyncio.get_event_loop()
is deprecated because it did too many things, but that doesn't mean nobody should hold onto persistent loop references. It just means that they don't want to track a global not-running loop. If each instance had a self._loop = asyncio.new_event_loop()
and always ran the sync wrappers in that loop (self._loop.run_until_complete(coro())
), it should work fine. That avoids the multiple-loop problem for sync uses because it's still a single persistent loop. Each instance may happen to use a different loop, but that should not be a problem in the sync case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sweet, thanks for the insights Min. That sounds like a good approach.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More thoughts on ready future handling:
- Remove the public ready attribute(s), but add public
is_start_pending
andis_shutdown_pending
flags. - Internally, store current ready and pending flags for start and shutdown.
- In
start()
, check for_start_pending
and error if true. Set_start_pending
and set a new_start_ready
future. Clear_start_pending
when ready. - In
shutdown()
, check for_shutdown_pending
and bail if true. Set_shutdown_pending
and set a new_shutdown_ready
future. Clear_shutdown_pending
when ready. - In
wait_for_start_ready()
andwait_for_shutdown_ready()
store current ready, and wait for it. If new current ready is different from the stored one, recurse.
Rather than introduce more flags and more confusion, I propose we actually use a simple state machine as @echarles suggested. Currently server adds "execution_state" to the manager object based on kernel status messages. I think eventually we should move that logic here, but it doesn't need to be in this PR. The states used here could mirror the ones used in
We are
Then we have a The We also have an |
Maybe the first step would be to draw and share that state machine that would be highly beneficial to jupyter client, but also to e.g. the jupyterlab services package evolution. Having a SVG format for that drawing would allow everyone to use their favorite editor (e.g. I use inkscape). |
I have not looked at this PR in details, but it seems to me that supporting a sync API is becoming problematic. At the time we switched jupyter_client to async, I had the feeling that keeping a sync API was kind of mandatory, although it involved using nest-asyncio which is not without problems. But today, could jupyter_client be only async? That would simplify the library a lot. |
Good call @echarles. I think we should adopt the We could add a CI job that create the diagram and compares it to the one in the documentation, and fails and asks you to rebuild if it has changed. |
@davidbrochart, I think there are still valid use cases for having a synchronous manager. What we could say is that only the async class is meant to be overridable, and offer a function that wraps the async class to become synchronous. We would have a section like:
And the public functions would be async in the base class, but made synchronous in the sync class. |
Code-to-doc, that sounds exciting. Looks like
That sounds exiciting. BTW I have been looking recently at xstate on the frontend area. Looks like state machine architecture is trendy atm. |
+1 Although async + sync is harder to maintain in |
I think keeping sync is important. It's also possible to avoid nest_asyncio if you run 'asyncio-for-sync' in a dedicated IO thread. |
I opened #755 to track the |
Done so far:
LocalProvisioner.wait
to avoid unclosed fidsstop_channels
to avoid unclosed zmq contextkernel.shutdown_ready
KernelManager.shutdown
So far the only warnings we can't easily work around are due to pytest-dev/pytest-asyncio#77. We'd have to rewrite all of the tests not to use
TestCase
.