Opened 3 weeks ago

Last modified 2 weeks ago

#37448 assigned Cleanup/optimization

Log sync/async adaptations in MiddlewareMixin.__acall__()

Reported by: Mykhailo Havelia Owned by: Mykhailo Havelia
Component: HTTP handling Version: dev
Severity: Normal Keywords: async, middleware
Cc: Mykhailo Havelia, Jacob Walls Triage Stage: Accepted
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

Follow-up to #37177, for the part that was deferred there.

#37177 fixed per-hook context switching by setting async_capable = False on Django's built-in middleware. The cause is still in MiddlewareMixin: it declares async_capable = True by default, so any subclass with sync hooks goes through __acall__(), which wraps each hook in its own sync_to_async().

This defeats load_middleware(), which is designed to adapt once per contiguous block of sync middleware.

So every user and third-party middleware written the documented way (subclass MiddlewareMixin, implement sync process_request / process_response) still has the problem #37177 measured. A default project that adds one or two such middleware between the built-in ones also splits the sync block, adding more switches around them.

A side effect: these switches are invisible. __acall__() calls sync_to_async() itself, so adapt_method_mode() never logs "Asynchronous handler adapted for middleware ...", which is the tool the docs point users to for finding this.

Proposal

https://code.djangoproject.com/ticket/37177#comment:7

Deprecate the default MiddlewareMixin.__acall__() implementation: raise a deprecation warning when a subclass ends up using it with sync hooks (i.e. sets async_capable = True explicitly but doesn't override __acall__()). After the deprecation period, remove it and make async_capable = False the default.

Change History (6)

comment:1 by Sarah Boyce, 3 weeks ago

Cc: Jacob Walls added
Summary: Performance issue in Async Middleware handling → Log sync/async adaptations in MiddlewareMixin.__acall__()
Triage Stage: Unreviewed → Accepted
Version: 6.1 → dev

Thank you for the follow-up!

I'm not convinced that deprecating MiddlewareMixin.__acall__() is necessary, as the current behavior is a documented performance trade-off. Any change to this behavior would need a wider discussion. You can start a forum discussion to propose this idea and see if the community agrees with your proposal: ​https://forum.djangoproject.com/c/internals/async/8

However, I agree that improving the visibility of these context switches would be useful. Let's focus this ticket on adding logging for the sync_to_async() calls in MiddlewareMixin.__acall__(). Accepting on that basis.

If your deprecation proposal has community by in, you can later create another ticket linking the forum discussion.

comment:2 by Mykhailo Havelia, 3 weeks ago

I'm not convinced that deprecating MiddlewareMixin.acall() is necessary, as the current behavior is a documented performance trade-off. Any change to this behavior would need a wider discussion.

I think "deprecation" was the wrong way to frame this, so let me clarify. The step I care about doesn't need a deprecation at all:

class MiddlewareMixin:
    sync_capable = True

    @classproperty
    def async_capable(cls):
        return cls.__acall__ is not MiddlewareMixin.__acall__

If a subclass doesn't override __acall__(), it's marked as sync-only, and BaseHandler.load_middleware() groups it with the neighboring sync middleware. That's the same fix #37177 applied to the built-in middleware, extended to third-party subclasses. Subclasses that do override __acall__(), or set async_capable explicitly, keep their current behavior.

It would also cover logging. The switches would go through adapt_method_mode(), so they'd show up in the existing "Asynchronous handler adapted" debug logs without any new logging.

Deprecating the default __acall__() would be a separate step, and I agree that one should go to the forum first. If it's eventually removed, the note in the docs about the performance trade-off could go too, since overriding __acall__() on top of sync hooks rarely makes sense.

refs

comment:3 by NAYAN SHARMA, 3 weeks ago

Hi,

I'd like to contribute to this ticket. I read the discussion and the related PR #21523.

My understanding is that the proposed approach is to let BaseHandler.load_middleware() handle sync/async adaptation for MiddlewareMixin subclasses that don't override __acall__(), so the existing adaptation logging also covers them.

Before I start implementing this, could you confirm whether this approach is preferred for the ticket, or whether you'd like the change to be limited to adding logging inside MiddlewareMixin.__acall__()?

comment:4 by Mykhailo Havelia, 3 weeks ago

Owner: set to Mykhailo Havelia
Status: new → assigned
Triage Stage: Accepted → Unreviewed

comment:5 by Carlton Gibson, 2 weeks ago

Triage Stage: Unreviewed → Accepted

I'm assuming the move back to Unreviewed was accidental.

in reply to:  5 comment:6 by Mykhailo Havelia, 2 weeks ago

Replying to Carlton Gibson:

I'm assuming the move back to Unreviewed was accidental.

It was deliberate. The ticket is about performance, and Sarah only approved fixing the logging part 🙂 I've reframed the solution a bit and stepped back from the deprecation, so now I’m waiting for a response.

Note: See TracTickets for help on using tickets.
Back to Top