graphql-ws icon indicating copy to clipboard operation
graphql-ws copied to clipboard

Using Django's GenericForeignKey with Async Graphene Subscription

Open soukiassianb opened this issue 4 years ago • 0 comments

  • GraphQL AioWS version:
  • Python version: 3
  • Operating System: Mac OS

Description

I've implemented graphql_ws to subscribe to updates from a Notification model that uses multiple GenericForeignKeys.

My setup works well, except when I try to query information from those foreign objects. I then get the following error:

graphql.error.located_error.GraphQLLocatedError: You cannot call this from an async context - use a thread or sync_to_async.

From what I seem to understand, that's because some database query operations are being done outside the async context of get_notifications (that is, in the resolve_actor function). But I'm really not clear on how I can pull the operations of resolve_actor
into the async context.

What I Did

I've unsuccessfully tried using prefetch_related (plus I'm not sure it'll work with multiple content types per Django's docs).

Here's the code

models.py


class Notification(TimeStampedModel):
	# ...
	actor_content_type = models.ForeignKey(ContentType, related_name='notify_actor', on_delete=models.CASCADE)
	actor_object_id = models.CharField(max_length=255)
	actor = GenericForeignKey('actor_content_type', 'actor_object_id')
	# ...

schema.py

class ActorTypeUnion(graphene.Union):
	"""
	All possible types for Actors
	(The object that performed the activity.)
	"""
	class Meta:
		types = (UserType,) # here's there's only one type, but other fields have multiple


class NotificationType(DjangoObjectType):
	actor = graphene.Field(ActorTypeUnion)
	
	def resolve_actor(self, args):
		if self.actor	is not None:
			model_name = self.actor._meta.model_name
			app_label = self.actor._meta.app_label
			model = ContentType.objects.get(app_label=app_label, model=model_name)
			return model.get_object_for_this_type(pk=self.actor_object_id)
		return None
	# ...
	class Meta:
		model = Notification

class Subscription(graphene.ObjectType):
	unread_notifications = graphene.List(NotificationType)

	async def resolve_unread_notifications(self, info, **kwargs):
		user = info.context['user']
		if user.is_anonymous:
			raise Exception('Not logged in!')
		
		@database_sync_to_async
		def get_notifications(user):
			notifications = Notification.objects.filter(
				recipient=user,
				read=False,
				organization=user.active_organization,
			)
			return [notifications]

		while True:
			await asyncio.sleep(1)
			yield await get_notifications(user)

The query (things works well except when I query fields on actor)

subscription {
   unreadNotifications {
      id,
      read,
      actor {
        ... on UserType {
          __typename,
          id

        }
      }
    }
  }

Full traceback

Traceback (most recent call last):
  File "/Users/benjaminsoukiassian/.pyenv/versions/logbook/lib/python3.8/site-packages/graphql/execution/executor.py", line 452, in resolve_or_error
    return executor.execute(resolve_fn, source, info, **args)
  File "/Users/benjaminsoukiassian/.pyenv/versions/logbook/lib/python3.8/site-packages/graphql/execution/executors/asyncio.py", line 74, in execute
    result = fn(*args, **kwargs)
  File "/Users/benjaminsoukiassian/Projects/logbook-back/logbook/notifications/schema.py", line 52, in resolve_actor
    model = ContentType.objects.get(app_label=app_label, model=model_name)
  File "/Users/benjaminsoukiassian/.pyenv/versions/logbook/lib/python3.8/site-packages/django/db/models/manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/Users/benjaminsoukiassian/.pyenv/versions/logbook/lib/python3.8/site-packages/django/db/models/query.py", line 431, in get
    num = len(clone)
  File "/Users/benjaminsoukiassian/.pyenv/versions/logbook/lib/python3.8/site-packages/django/db/models/query.py", line 262, in __len__
    self._fetch_all()
  File "/Users/benjaminsoukiassian/.pyenv/versions/logbook/lib/python3.8/site-packages/django/db/models/query.py", line 1324, in _fetch_all
    self._result_cache = list(self._iterable_class(self))
  File "/Users/benjaminsoukiassian/.pyenv/versions/logbook/lib/python3.8/site-packages/django/db/models/query.py", line 51, in __iter__
    results = compiler.execute_sql(chunked_fetch=self.chunked_fetch, chunk_size=self.chunk_size)
  File "/Users/benjaminsoukiassian/.pyenv/versions/logbook/lib/python3.8/site-packages/django/db/models/sql/compiler.py", line 1173, in execute_sql
    cursor = self.connection.cursor()
  File "/Users/benjaminsoukiassian/.pyenv/versions/logbook/lib/python3.8/site-packages/django/utils/asyncio.py", line 24, in inner
    raise SynchronousOnlyOperation(message)
graphql.error.located_error.GraphQLLocatedError: You cannot call this from an async context - use a thread or sync_to_async.

Thank you very much for your help

soukiassianb avatar Aug 07 '21 12:08 soukiassianb