Skip to content

Flask with WSGI sync gives asyncio event loop errors. #244

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
CoultonF opened this issue May 5, 2025 · 5 comments
Open

Flask with WSGI sync gives asyncio event loop errors. #244

CoultonF opened this issue May 5, 2025 · 5 comments
Labels
bug Something isn't working

Comments

@CoultonF
Copy link

CoultonF commented May 5, 2025

If I use this in a WSGI flask application when you try to access a nested relationship pattern you get a threading error.

query {
  posts {
    id
    comments {
      text
      id
    }
  }
}
Stack trace:
There is no current event loop in thread 'Thread-8 (process_request_thread)'.
Traceback (most recent call last):
  File "/project/.venv/lib/python3.12/site-packages/strawberry/schema/schema.py", line 632, in execute_sync
    ensure_future(result).cancel()
    ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/asyncio/tasks.py", line 693, in ensure_future
    loop = events.get_event_loop()
           ^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/asyncio/events.py", line 702, in get_event_loop
    raise RuntimeError('There is no current event loop in thread %r.'
RuntimeError: There is no current event loop in thread 'Thread-8 (process_request_thread)'.

If I avoid using this package, and write out the fields and types it works.

@CoultonF CoultonF added the bug Something isn't working label May 5, 2025
@Ckk3
Copy link
Contributor

Ckk3 commented May 6, 2025

Hi, @CoultonF , can you please share the models declaration? It helps a lot to write tests and search for solutions 😉

@CoultonF
Copy link
Author

CoultonF commented May 6, 2025

I have hacky solution to this, by overriding the StrawberrySQLAlchemyMapper() class. Specifically, I had to rewrite the relationship_resolver_for() and association_proxy_resolver_for() to use sync methods. Flask doesn't have great support for asynchronous. In case someone else comes across this I'll leave my solution here.

class SyncStrawberrySQLAlchemyMapper(StrawberrySQLAlchemyMapper):
    """A custom mapper that uses synchronous resolvers instead of async ones"""

    def relationship_resolver_for(self, relationship: RelationshipProperty):
        """Override to return a synchronous resolver instead of an async one"""

        def resolve(self, info):
            # Get the relationship data directly from the model
            instance_state = cast(InstanceState, inspect(self))
            if relationship.key not in instance_state.unloaded:
                return getattr(self, relationship.key)

            # Get the foreign key values from the current object
            local_remote_pairs = relationship.local_remote_pairs
            if not local_remote_pairs:
                return [] if relationship.uselist else None

            # Extract the foreign key values
            fk_values = []
            for local_col, remote_col in local_remote_pairs:
                local_attr = local_col.key
                local_value = getattr(self, local_attr)
                if local_value is None:
                    return [] if relationship.uselist else None
                fk_values.append((remote_col.key, local_value))

            # Build the query
            target_class = relationship.mapper.class_
            query = select(target_class)

            # Add conditions for each foreign key
            for remote_attr, value in fk_values:
                query = query.where(getattr(target_class, remote_attr) == value)

            # Execute the query
            result = db.session.scalars(query).all()
            return result if relationship.uselist else (result[0] if result else None)

        # Mark this as a generated resolver
        setattr(resolve, "_IS_GENERATED_RESOLVER_KEY", True)

        return resolve

    def _get_association_proxy_annotation(
        self, mapper: Mapper, key: str, descriptor: Any, use_list: bool = False
    ) -> Union[Type[Any], ForwardRef]:
        """
        Given an association proxy, return the type annotation for it.
        This supports association proxies that are:
        1. relationship -> relationship
        2. relationship -> column
        3. column -> value
        """
        # Check if descriptor has the expected attributes
        if not hasattr(descriptor, "target_collection") or not hasattr(descriptor, "value_attr"):
            # For simple column-to-value proxies, return string type
            return str

        # Get information about the intermediate relationship
        is_multiple = mapper.relationships[descriptor.target_collection].uselist
        in_between_mapper = mapper.relationships[descriptor.target_collection].entity

        # Case 1: relationship -> relationship
        if descriptor.value_attr in in_between_mapper.relationships:
            relationship = in_between_mapper.relationships[descriptor.value_attr]
            is_multiple = is_multiple or relationship.uselist
            
            # Get the target model and convert to a strawberry type
            relationship_model = relationship.entity.entity
            type_name = self.model_to_type_or_interface_name(relationship_model)
            
            if self.model_is_interface(relationship_model):
                self._related_interface_models.add(relationship_model)
            else:
                self._related_type_models.add(relationship_model)
                
            # Handle multiple results (list or connection)
            if is_multiple:
                if use_list:
                    return List[ForwardRef(type_name)]
                else:
                    return self._connection_type_for(type_name)
            else:
                # Handle optional relationships
                if self._get_relationship_is_optional(relationship):
                    return Optional[ForwardRef(type_name)]
                else:
                    return ForwardRef(type_name)
                    
        # Case 2: relationship -> column
        elif descriptor.value_attr in in_between_mapper.columns:
            column = in_between_mapper.columns[descriptor.value_attr]
            column_type = self._convert_column_to_strawberry_type(column)
            
            # Handle multiple results
            if is_multiple:
                return List[column_type]
            else:
                return column_type
                
        # Case 3: For any other type of association proxy
        else:
            # Default to string for unknown types
            if is_multiple:
                return List[str]
            else:
                return str

    def association_proxy_resolver_for(
        self, mapper: Mapper, descriptor: Any, strawberry_type: Type
    ) -> Callable:
        """
        Return a synchronous field resolver for the given association proxy.
        """
        # Handle simple proxies without target_collection or value_attr
        if not hasattr(descriptor, "target_collection") or not hasattr(descriptor, "value_attr"):
            def simple_resolve(self, info: Info):
                try:
                    return getattr(self, descriptor.key)
                except Exception:
                    return None
                    
            setattr(simple_resolve, "_IS_GENERATED_RESOLVER_KEY", True)
            return simple_resolve

        # Regular association proxy handling
        in_between_relationship = mapper.relationships[descriptor.target_collection]
        in_between_resolver = self.relationship_resolver_for(in_between_relationship)
        in_between_mapper = mapper.relationships[descriptor.target_collection].entity

        # Case 1: relationship -> relationship
        if descriptor.value_attr in in_between_mapper.relationships:
            end_relationship = in_between_mapper.relationships[descriptor.value_attr]
            end_relationship_resolver = self.relationship_resolver_for(end_relationship)

            end_type_name = self.model_to_type_or_interface_name(
                end_relationship.entity.entity
            )
            connection_type = self._connection_type_for(end_type_name)
            edge_type = self._edge_type_for(end_type_name)
            is_multiple = self._is_connection_type(strawberry_type)

            def resolve(self, info: Info):
                # Get the intermediate objects
                in_between_objects = in_between_resolver(self, info)

                if in_between_objects is None:
                    if is_multiple:
                        return connection_type(edges=[])
                    else:
                        return None

                if isinstance(in_between_objects, collections.abc.Iterable):
                    # Process each intermediate object
                    outputs = []
                    for obj in in_between_objects:
                        result = end_relationship_resolver(obj, info)
                        if result is not None:
                            outputs.append(result)

                    # Flatten the outputs if needed
                    if outputs and isinstance(outputs[0], list):
                        outputs = list(chain.from_iterable(outputs))
                    else:
                        outputs = [output for output in outputs if output is not None]
                else:
                    # Single intermediate object
                    outputs = end_relationship_resolver(in_between_objects, info)

                if not isinstance(outputs, collections.abc.Iterable):
                    return outputs

                # Create connection with edges
                return connection_type(
                    edges=[
                        edge_type(node=obj, cursor=i) for i, obj in enumerate(outputs)
                    ]
                )
        # Case 2: relationship -> column
        else:
            def resolve(self, info: Info):
                in_between_objects = in_between_resolver(self, info)
                
                if in_between_objects is None:
                    return None
                    
                if isinstance(in_between_objects, collections.abc.Iterable):
                    return [
                        getattr(obj, descriptor.value_attr)
                        for obj in in_between_objects
                    ]
                else:
                    return getattr(in_between_objects, descriptor.value_attr)

        setattr(resolve, "_IS_GENERATED_RESOLVER_KEY", True)
        return resolve

@CoultonF CoultonF closed this as completed May 6, 2025
@Ckk3
Copy link
Contributor

Ckk3 commented May 7, 2025

Thanks, @CoultonF , so you think that its a Flask problem, right?
I will reopen this issue so we dont lost this info and see later if makes sense add something about this on README later.

@Ckk3 Ckk3 reopened this May 7, 2025
@CoultonF
Copy link
Author

CoultonF commented May 7, 2025

@Ckk3 Yes, only a flask issue.

@Ckk3
Copy link
Contributor

Ckk3 commented May 24, 2025

I'll keep this issue opened because I think we need to make a easier option to devs use sync connection.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants