Support async hooks. Fixes #4207.#5106
Conversation
806cce1 to
06a8953
Compare
mhils
left a comment
There was a problem hiding this comment.
Thanks for the very nice PR! 🍰
Eliminate concurrent entirely, and possibly even the entire reply mechanism. This would be a significant, breaking API change.
Yes, eliminating reply is something I definitely want to see happening. @concurrent is here to stay though, that's widely used by existing addons. It just needs to be changed to spawn a thread, and then await the completion of an asyncio.Event which is set by the thread on completion. This way users can continue to use requests or other blocking stuff in their hooks.
If we could tackle the reply-sectomy in this PR that would be great, if you give it a stab and notice it's too much work I won't insist. :)
Eliminate the sync versions of trigger and invoke_addon to allow async hooks in all contexts. This is trickier than it seems because of the number of places in which hooks are triggered in a synchronous context; a lot of code would probably need to be updated to be async. While this probably should happen eventually, I'm hoping this much smaller patch can serve 80% of use cases immediately.
Yes, 👍 to this. See my inline comments below.
Document which contexts will permit async hooks. Lifecycle hooks like start, done, etc. don't look like they should be async, for example, since they're often called synchronously.
My naive approach would be that we could just permit all hooks to be async. If you add asyncio.sleep(5) to the start hook that's your own problem really, on the other hand it can be useful to have I-know-what-I'm-doing awaits in there.
I'd be happy to, but I did just realize one problem: as it currently stands, if a hook can't be made async (because it gets called synchronously somewhere), it can be made |
e258301 to
de48612
Compare
Good catch. We don't use |
|
Thank you very much! I've been playing with it this morning and so far haven't run into any issues. It does consistently increase the throughput of my addon quite a bit (between 10% and 300%). I've been using mostly https://http2.akamai.com/demo to test. |
de48612 to
b02f2d8
Compare
|
@mhils: I appended a patch series to this PR which eliminates the
|
mhils
left a comment
There was a problem hiding this comment.
This looks exceptional - thank you so much! 🍰🎉 I'm in the somewhat dry final write-up stage of my PhD and this PR just made my day. 😃
A few minor comments below. Once those are addressed I'd be happy to get this into main right away so that it can already get some testing before 8.0. There likely are some unintended side-effects we haven't considered yet, but that's okay. Overall this PR is a massive net benefit.
Ah, also for your entertainment:
chrome_2022-02-03_16-20-46.mp4
A mismatch between the actual changes and the rendered diff tickles my spidey sense, but I'll refrain from falling into this rabbit hole now. 😅
582f8c1 to
a3dc25b
Compare
mhils
left a comment
There was a problem hiding this comment.
Excellent, thanks. I'll re-review the changes tomorrow with a fresh mind so that this can go into main right away. 😃
a3dc25b to
044b307
Compare
|
Just rebased to main to fix a merge conflict. |
|
Thanks! I'm currently on a final pass locally and will force-push again once I'm done. :) |
async_trigger -> trigger_event invoke_addon -> invoke_addon_sync (API breakage) async_invoke_addon -> invoke_addon
Previously, it was throwing the following error:
File mitmproxy/addons/dumper.py, line 133, in _echo_request_line
elif flow.client_conn.peername:
AttributeError: 'bool' object has no attribute 'peername'
This should improve behaviour, since calls to @Concurrent will now be serialized relative to other hooks on the same flow (but will still run in parallel with hooks on different flows). Unlike a plain async hook, @Concurrent allows blocking sync APIs to run concurrently (e.g. requests). This is patch 1/4 of the reply-ectomy.
In principle, a flow is killable as long as the connection handler is still checking the error status of the flow. This is patch 2/4 of the reply-ectomy.
This is patch 3/4 of the reply-ectomy.
The major, breaking change is that it is no longer possible to "take" a reply in order to block the effect of a later addon hook. This is patch 4/4 of the reply-ectomy.
We previously relied on the state of `Flow.reply` to check if a flow can be killed, but this doesn't work anymore with `Flow.reply` being removed. Instead, we now reintroduce the `Flow.live` attribute, which signals if we are on a live connection. Killing still is not ideal (see comment in `Flow.killable`), but this paves the way.
|
@nneonneo: Done! I think we are in a good state here. The only major issue I discovered was that |
|
LGTM! Let's ship it! :) |
Description
Add support for async hooks to AddonManager, enabling the proxy server to call hooks asynchronously. Uses
inspect.isawaitableto ensure that existing sync hooks continue to work without any change. Fixes #4207.The forking of
invoke_addonandtriggeris a little unfortunate, but both of these get called from sync contexts throughout mitmproxy (in particular,triggers are often called fromcommand.commandhandlers, which are all sync). To ensure that we don't accidentally invoke an async handler in such cases, this patch adds a check to throw an exception if an async handler gets invoked synchronously.This is a first stab at async hook support. There are more things that could be done here, and I'd love to hear if either of them makes sense for this PR, or left for future work:
EliminateBasically done:concurrententirely, and possibly even the entirereplymechanism. This would be a significant, breaking API change. However, in almost every case,asynchooks are better: async hooks are properly sequenced (even with respect to other sync hooks), races are essentially eliminated, and the behaviour is much more predictable. (It might be better to just deprecateconcurrentnow for future removal).concurrentis kept, but made async.replyis gone.triggerandinvoke_addonto allow async hooks in all contexts. This is trickier than it seems because of the number of places in which hooks are triggered in a synchronous context; a lot of code would probably need to be updated to be async. While this probably should happen eventually, I'm hoping this much smaller patch can serve 80% of use cases immediately.There are also a few easier things that could be done:
start,done, etc. don't look like they should be async, for example, since they're often called synchronously.Checklist