django-debug-toolbar icon indicating copy to clipboard operation
django-debug-toolbar copied to clipboard

Document that the toolbar does not support async functionality

Open tim-schilling opened this issue 2 years ago • 16 comments

We should include in our readme that the application doesn't not support async functionality and that effort is under development. If possible, it'd be great to detect that async functionality is being used and warn the user that the toolbar won't be effective. @michaelurban suggested this in https://github.com/jazzband/django-debug-toolbar/issues/1430#issuecomment-1697910837

tim-schilling avatar Oct 23 '23 00:10 tim-schilling

Hello!

I would like to implement showing warning message if DebugToolbarMiddleware is called in async environment.

I suppose the easiest way to check if async is enabled is something like this:

from django.core.handlers.asgi import ASGIRequest

...

class DebugToolbarMiddleware:

   ...   

   def __call__(self, request):
          # Decide whether the toolbar is active for this request.
          show_toolbar = get_show_toolbar()

          if not show_toolbar(request) or DebugToolbar.is_toolbar_request(request):
              return self.get_response(request)

          if type(request) == ASGIRequest:
             self._warn_djdt_asgi_uneffective()

          ...

What do you think about this approach? I guess there are some important thinks about compability, but general idea is the same.

What is the optimal way to warn developer? At this point I think about log a warning message, but may be it's something else.

denisSurkov avatar Jan 16 '24 14:01 denisSurkov

@denisSurkov what do you think of using iscoroutinefunction(get_response) similar to https://docs.djangoproject.com/en/4.2/topics/http/middleware/#asynchronous-support?

tim-schilling avatar Jan 16 '24 15:01 tim-schilling

@tim-schilling , yes, this is an option

I can do something like this:

from inspect import iscoroutinefunction

...

class DebugToolbarMiddleware:

       def __init__(self, get_response):
        self.get_response = get_response   
       
        if iscoroutinefunction(get_response):
             self._warn_djdt_asgi_uneffective()

This seems cleaner I agree

denisSurkov avatar Jan 16 '24 15:01 denisSurkov

What's the difference between asgiref's version and inspects?

tim-schilling avatar Jan 16 '24 15:01 tim-schilling

I'm not completely sure that works, since Django automatically wraps get_response to be either a coroutine or not depending on whether the current middleware (and maybe other middlewares in the stack) support async or not.

The note here is especially relevant:

https://docs.djangoproject.com/en/5.0/topics/http/middleware/#async-middleware

If you declare a hybrid middleware that supports both synchronous and asynchronous calls, the kind of call you get may not match the underlying view. Django will optimize the middleware call stack to have as few sync/async transitions as possible.

So, as long as the DebugToolbarMiddleware doesn't officially support async we won't ever get a get_response. At least that's my understanding of where we are now.

matthiask avatar Jan 16 '24 15:01 matthiask

@matthiask , you are right.

Currently DegubToolbarMiddleware is sync_only. There are two attributes django checking (and does fallback) for every middleware:

sync_capable = True
async_capable = False

and DebugToolbarMiddleware is sync_capable only.

So async get_response would not be an option.


For this feature I see two options now:

  • Adding flags (and also refactoring for new way middlewares work with process_view and etc, but it may break compability) and checking with iscoroutinefunction;

  • Checking type of request - we could do it one time, because once ASGIRequest came to middleware next requests would have the same type I guess.

denisSurkov avatar Jan 16 '24 15:01 denisSurkov

I think checking the type of the request may be fine?

I'm thinking if there are other signals such as the presence of an event loop (https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.get_running_loop), but it seems to me that "just" doing a type check may be the most straightforward option for now. I didn't spend much time thinking this through just now so that's not really a well researched opinion.

matthiask avatar Jan 17 '24 08:01 matthiask

That's good for me. Sorry for the run around @denisSurkov!

tim-schilling avatar Jan 17 '24 19:01 tim-schilling

That's okey

I'll try to make an MVP in a couple of days.

@tim-schilling , I still have question about how to warn a developer? Just printing a message or logging it somehow? If DJDT has way to log it, can you point the place?

Also I think making tests for it would be challenging

denisSurkov avatar Jan 18 '24 07:01 denisSurkov

That's a good question. I think there are a number of issues that would benefit from a warnings/alerts panel (#1435, #1682). This could also potentially go in there too.

I think for now let's keep it easy and log it out with logger.info().

To test it you should be able to patch logger, then use logger.info.assert_called_once_with("the warning goes here")

tim-schilling avatar Jan 18 '24 18:01 tim-schilling

@matthiask what do you think about logging vs some sort of panel to render these messages?

tim-schilling avatar Jan 18 '24 18:01 tim-schilling

I'm not sure. We have removed the logging panel some time ago and I'm not sure we want to reintroduce it since we had so many hard to find bugs around logging.

Re. warnings: Maybe it would be nice to show it inside the panel, but I also think something like a multiline and really loud and obnoxious warning (something with borders and ASCII art 😅) would be a good fit for this issue. It's not similar to a warning or anything of the sort: For the time being the toolbar is of very limited use when running an async server.

I just checked and Django also has a similar thing, when doing stuff which is probable to cause problems down the road: https://github.com/django/django/blob/10c7c7320baf1c655fcb91202169d77725c9c4bd/setup.py#L34-L55

matthiask avatar Jan 18 '24 19:01 matthiask

Hmm. I wasn't thinking something to show all calls to logger.log(). But instead there would be a series of checks/validations that the toolbar could perform and inform the user. Such as "Hey, this particular form has a file input, but the form doesn't have enctype="multipart/form-data"" set.

tim-schilling avatar Jan 18 '24 21:01 tim-schilling

Ah yes, something like that would certainly be nice.

matthiask avatar Jan 18 '24 21:01 matthiask

@tim-schilling ,

To test it you should be able to patch logger, then use logger.info.assert_called_once_with("the warning goes here")

Testing logging is not a problem. I'm worried about how to pass ASGIRequest. I can construct a request directly, but this seems like a hack. So I was thinking about running ASGIHandler with async_to_sync in tests.

denisSurkov avatar Jan 19 '24 06:01 denisSurkov

@denisSurkov I suspect we can get away with using the AsyncRequestFactory

tim-schilling avatar Jan 19 '24 13:01 tim-schilling