Skip to content

Make reflex rendering thread safe #99

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

blubber
Copy link

@blubber blubber commented Feb 27, 2021

The monkey patch used to render the original view's context was not
thread safe. This commit fixes the issue by patching all the
get_context_data methods before accepting requests, instead of while
rendering the reflex' response.

The monkey patch used to render the original view's context was not
thread safe. This commit fixes the issue by patching all the
get_context_data methods before accepting requests, instead of while
rendering the reflex' response.
@blubber
Copy link
Author

blubber commented Feb 27, 2021

@jonathan-s this is the solution that I suggested you over email.

While working on this I noticed that the view and its context are rendered twice during the page_render call. This is quite inefficient, because it could potentially result in many duplicate database queries in the view. The page rendering logic should be streamlined further probably.

@jonathan-s
Copy link
Owner

Hi @blubber,
Thanks for taking a look at this. However, with the current code it seems like you end up in a recursive call. You can see it happen in the integration tests. I've reproduced it locally as well. I sent you another email, if you are replying to that, we can keep the discussion in this PR.

@jonathan-s
Copy link
Owner

While working on this I noticed that the view and its context are rendered twice during the page_render call.

There are some cases where this is necessary. If you're using a ListView you need to execute the get request as some instance variables are set in the get part.

However if you keep all your queries in get_context_data the queries should only be triggered once as they are cached in the reflex class.

@blubber
Copy link
Author

blubber commented Feb 28, 2021 via email

@@ -36,7 +38,7 @@ def get_context_data(self, *args, **kwargs):
view.request = self.request
try:
view.kwargs = resolved.kwargs
context = view.get_context_data()
context = view._patched_get_context_data()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you manage to fix so that we can use get_context_data here I think this PR will be usable. Because that's the issue where we get an infinite recursion here.

@blubber
Copy link
Author

blubber commented Mar 12, 2021

Unfortunately I haven't had time to look into this PR. However, I did take some time today to get to the bottom of the blocking behavior. I was a little weird to me that views or consumers where actually blocking, because the websocket consumer's dispatch method is wrapper in asgiref's sync_to_async which should execute it in a thread pool. This was bothering me, since I really couldn't explain the blocking behavior, but I now found out where my brain errored.

The sync_to_async class has an argument thread_sensitive which defaults to True, which does exactly the opposite of what I expect. If True it in facct forces execution in the main thread. Setting it to False results in the expected, non-blocking, behavior.

I believe that if you wrap a view class used with sockpuppet in sync_to_async(thread_sensitive=True) that it in fact will break on the get_context_data swapping. But I haven't had time to try this out yet.

@blubber
Copy link
Author

blubber commented Mar 12, 2021

Ok, so I finally managed to pin this down. It seems that asgiref 3.3 has changed sync_to_async's behavior to run code in a singel thread (the main thread) by default, ie the thread_sensitive argument defaults to True now. This completely changes Channel's behavior as it's now running sync views and other sync code serially by default. This includes the JsonWebSocketCustomer's dispatch method, which is decorated with a derivative of sync_to_async.

If you remove asgiref>=3.3 and install asgiref<3.3 instead, the behavior reverts to its previous default of concurrency using a thread pool (for sync parts like the consumer). Again, I'm pretty sure you can break the get_context_data replacement if you're running asgiref < 3.3.

@jonathan-s
Copy link
Owner

Thank you @blubber for this in-depth investigation! Much appreciated. So in other words this would be an issue if you were running channels 2.4, and not be an issue if you were running channels >=3.0 (as channels installs asgiref >3.3 by default there).

@blubber
Copy link
Author

blubber commented Mar 13, 2021

Well, if you force asgiref <3.3, which I did locally, than you get the old behavior, which also implies a threading issue. Also, it's unclear at this point how Channels are going to manage this change. They might decide to force thread_sensitive=False, to explicitly re-enable the old behavior.

@jonathan-s
Copy link
Owner

Relates to django/channels#1582 and django/channels#1587
So it's worth keeping these in mind if anything will / should change here.

@blubber
Copy link
Author

blubber commented Mar 17, 2021

Yeah, I was actually kind of waiting to see where they are going with this. That PR you've linked doesn't really solve the entire problem and seems to be stranded. Probably a good idea to wait and see how they handle this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants