-
-
Notifications
You must be signed in to change notification settings - Fork 108
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
Changes to how signals are caught and handled in awatch
#136
Conversation
Hm, now I need to learn how the tests work and see which is wrong: the code or the tests... Any advise would be greatly appreciated. |
Codecov Report
@@ Coverage Diff @@
## main #136 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 6 6
Lines 372 368 -4
Branches 79 76 -3
=========================================
- Hits 372 368 -4
Continue to review full report at Codecov.
|
There are some obvious oversights in my PR (like the now unused |
This doesn't work because the rust code won't hear the signal. If you have |
Unless I'm wrong somehow... But I'm pretty sure I'm not. Remember inside rust we have threading, threads can't generally detect signals. |
It's quite possible I misunderstand how things work. But in the current code, where do you tell the Rust code to stop? The signal handler just responds to a SIGINT, and sets the stop event, which is handled by anyio. In this PR, I don't listen to SIGINT: Python/asyncio is already doing that, and the asyncio loop cancels all pending tasks, causing CancelledError to be raised, upon which I set the stop event. It's not all that different from the current code. |
Let me look again when I'm at my laptop. |
I still don't think this works - it doesn't set the event when a signal is received, so the rust code can't know to return. Have you tried it? If compiling the binary is difficult, just copy it from a regular install. Also I went to a talk yesterday at PyCon about how you should never catch |
I tried it, and it does fix my problem. This PR just moves setting the stop event to a different place in the code, one that works better with asyncio. |
|
My bad, I shouldn't reply on my phone without reading it properly. |
This is my test code: import asyncio
import watchfiles
async def watcher(path):
async for changes in watchfiles.awatch(path, debug=True):
print(changes)
async def watch_multiple(paths):
await asyncio.gather(*(watcher(p) for p in paths))
print("done?")
def main():
asyncio.run(watch_multiple(["dir_A", "dir_B"]))
if __name__ == "__main__":
main() Without this PR, I have to do Ctl-C twice before the process exists. |
Btw. this PR also ensures that cancelling an asyncio task containing an awatch call works correctly. I wrote a test for it, but it is asyncio-specific. I bet the same could work with anyio, but I've never used it so I don't know how. async def test_awatch_cancel_task(mocker, mock_rust_notify: 'MockRustType'):
import asyncio
async def watcher():
async for _ in awatch('.'):
pass
async def delayed_cancel():
await asyncio.sleep(0.1)
task.cancel()
task = asyncio.create_task(watcher())
cancel_task = asyncio.create_task(delayed_cancel())
with pytest.raises(asyncio.CancelledError):
await task
await cancel_task |
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.
Looks to me like this is almost right, but raise_interrupt
isn't correctly handled anymore.
Either way, I'll have to wait until I'm back home and have access to linux to test it properly.
watchfiles/main.py
Outdated
elif raw_changes == 'signal': | ||
# in theory the watch thread should never get a signal | ||
if raise_interrupt: | ||
raise KeyboardInterrupt |
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 don't think this will work, since we raise the KeyboardInterrupt
on line 202 above.
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.
This code should never be hittable, as py.check_signals()
calls PyErr_CheckSignals
which always returns 0
if not on the main thread.
So I think raise_interrupt==False
might make sense to suppress a KeyboardInterrupt
(and maybe a CancelledError
but that would be somewhat surprising) after setting the stop_event_
above. This just makes it a short-hand for with contextlib.suppress
though, as it is in watch
.
I also suspect that the equivalent call in watch
is faulty for a different reason: if PyErr_CheckSignals
returns -1
, then the signal has been handled (and an exception raised and (I assume captured) and returned) and so raising KeyboardInterrupt
would confuse a user who had installed a different SIGINT
handler, or if the signal that raised the exception was not SIGINT
at all. I guess that return Ok("signal".to_object(py))
should probably return the value it got from py.check_signals()
instead, and then a KeyboardInterrupt
or whatever exception the installed signal handler specified would appear naturally as an exception from the watcher.watch(debounce, step, rust_timeout, stop_event)
call.
I was having the double-control-C-needed issue described above, and also having issues where The change in #136 looks reasonable to me, but I haven't tested it. It seems to mirror the changes I made locally (trimmed to the relevant parts)
All my other long-running tasks are bounded with Co-incidentally, I'd be interested in knowing more about the PyCon talk that said "never catch |
awatch
@justvanrossum @TBBle could you try the changes I've proposed in 1e32ded and let me know how it works for you. It's basically what @justvanrossum did with |
I've tried this a fair bit on ubuntu and it seems to work well, but it's virtually impossible to think of and try every edge case. |
Seems to work fine for me, thanks! |
Thank you so much for properly finishing this! Much appreciated. |
No problem. |
Sorry for not getting back to you earlier. I've upgraded our local If I don't install my own SIGINT handler to set the stop_event, I still see what appears to be a double Anyway, thank you both for pushing these changes through. ^_^ |
Great, thanks for the feedback. |
For anyone coming to this in future, this is a more correct and elegant way of fixing #128 than my attempt (#132).
However it does bring some changes, in particuarly we can no longer optionally suppress
KeyboardInterrupt
insideawatch
, so you might need to catch it where you callasyncio.run()
or similar.I'm am not very confident about this PR, but I think this would be the correct fix for #128, at least for asyncio:
You don't need to handle SIGINT explicitly, you just have to catch CancelledError, and set the stop even from there.
For me, this fixes the Ctr-C problem when using multiple watchers without the user code having to bother with an explicit stop event.