Skip to content
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

feat(async): add support for async sessions #350

Merged
merged 21 commits into from
Dec 21, 2022

Conversation

jendrikjoe
Copy link
Contributor

@jendrikjoe jendrikjoe commented May 16, 2022

This is my initial suggestion on how to support async sessions in graphene-sqlalchemy as discussed in #324 🙂
It is not completely done yet, but I thought I would already open it to give you a chance for some feedback.
Sorry as well for all the style changes. My IDE ran black over it. I can revert those changes, however I couldn't find anything in the contribution guidelines regarding the style, so I thought I keep it for the time being 🤷‍♂️
As mentioned in the original issue: Async only supports joined and selectin loads and no lazy ones. This should be stated somewhere in the docs, I assume 😉

Open points:

  • Add async tests for batching (do we need this? When reading this comment: N + 1 round trip problem #35 (comment), batching seems to be used with lazy connections which anyhow don't work with async 🤔 )
  • Add limitation on relationships to docs

@erikwrede
Copy link
Member

Thank you! I'll have a look at this in the next days.

Copy link
Member

@erikwrede erikwrede left a comment

Choose a reason for hiding this comment

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

Sorry for replying a bit later. I've taken an initial look at your PR. Using AsyncSession with this implementation seems to be working great and might bring advantages to the users with supporting web frameworks.
However, the current PR would break things in synchronous execution contexts. We still have to support these since many relying projects use frameworks like flask 1.x. I have added a few comments to the files and I'm looking forward to your ideas on how to solve these challenges!

I have not yet taken a look at the tests since they are failing here. I think before fixing them, we should discuss my points. Additionally, I'd prefer the tests to remain mainly synchronous since that's the major use of this library right now. Async tests should complement the current tests, not replace them.

Looking forward to your reply!

graphene_sqlalchemy/fields.py Outdated Show resolved Hide resolved
graphene_sqlalchemy/types.py Outdated Show resolved Hide resolved
graphene_sqlalchemy/types.py Outdated Show resolved Hide resolved
@codecov-commenter
Copy link

codecov-commenter commented May 31, 2022

Codecov Report

Base: 97.09% // Head: 96.24% // Decreases project coverage by -0.84% ⚠️

Coverage data is based on head (1e857e0) compared to base (2edeae9).
Patch coverage: 90.74% of modified lines in pull request are covered.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #350      +/-   ##
==========================================
- Coverage   97.09%   96.24%   -0.85%     
==========================================
  Files           7        9       +2     
  Lines         722      879     +157     
==========================================
+ Hits          701      846     +145     
- Misses         21       33      +12     
Impacted Files Coverage Δ
graphene_sqlalchemy/batching.py 95.34% <75.00%> (ø)
graphene_sqlalchemy/types.py 93.45% <76.47%> (-1.95%) ⬇️
graphene_sqlalchemy/fields.py 98.58% <100.00%> (-0.58%) ⬇️
graphene_sqlalchemy/utils.py 93.85% <100.00%> (ø)
graphene_sqlalchemy/__init__.py 100.00% <0.00%> (ø)

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

☔ View full report at Codecov.
📢 Do you have feedback about the report comment? Let us know in this issue.

@erikwrede
Copy link
Member

Looks like everything is coming together, I'll check the new changes out soon! Thank you for all the effort 🙂

@Fred7b do you also have time for a full review? If I recall correctly, u originally created the issue so I would appreciate the input from you!
Formatting will be solved by black soon, so no need to care for that on the review.

@jendrikjoe
Copy link
Contributor Author

A polite ping 🙂

@erikwrede
Copy link
Member

@jendrikjoe sorry, I've been focused on other issues. My plan is to merge those first, release a new beta with the other issues and then immediately release another beta including async session support so both versions can be tried out, in case async session support still has some bugs we didn't find. If there's anything left to change I'll let you know in the next days :)

@jendrikjoe
Copy link
Contributor Author

@erikwrede no worries and thanks for explaining 🤗 The release plan makes a lot of sense and take all the time you need for it 👍 Just wanted to make sure that the MR is not forgotten 😉

@erikwrede
Copy link
Member

erikwrede commented Aug 7, 2022

Quick Update: Release of this is pushed back until #355 is merged.
We're reformatting connection fields and it makes sense to merge that first and then add async sessions later. Should require minor reformatting here once #355 is done. I definitely want to get this integrated afterwards. Sorry to keep you waiting, thanks for all the patience! 🙂

@jendrikjoe
Copy link
Contributor Author

No worries @erikwrede 🤗 Just ping me whenever I should resolve conflicts here 👍

@erikwrede erikwrede mentioned this pull request Aug 12, 2022
9 tasks
@erikwrede
Copy link
Member

Hey @jendrikjoe
Sorry it took so long! The consolidation is done and ready to be addressed. Should only be causing minor conflicts. LMK if you need any help 🙂

@jendrikjoe
Copy link
Contributor Author

Thanks for pinging me 👍 Will try resolve the conflicts later today 🙂

@erikwrede
Copy link
Member

Great!

I noticed that some of the original tests are still modified to use async methods. I'd prefer that entire part to be separate - leave the sync tests and Schema resolvers as is and create separate object types with async resolvers and corresponding tests. Otherwise a test failure will not tell wether the failure hints at a fundamental problem or problems with the async extension. What do you think?

@jendrikjoe
Copy link
Contributor Author

I parametrize the schema now to have a synchronous and an asynchronous for all relevant tests. This way every test reports with which schema it failed, while still avoiding to copy all the tests. Or are you referring to the changes I made on the lazy attributes of the SQLModels?

I still have to investigate why the batching tests using get_full_relay_schema are failing for me. Presumably, because I separated the batching models from the others, so the lazy attribute is not set on them.

I as well moved some assert statements in the batching tests. I had some bugs in them, which led to the result of querying the schema having an error and then of course wrong sql statement counts, too. However, first testing that no errors occur when calling the schema, before counting the SQL statements made the debugging a lot easier and the error more apparent.

@jendrikjoe
Copy link
Contributor Author

Will try to figure as well what the issues with the session fixture is 🙈

@jendrikjoe
Copy link
Contributor Author

Should be all working now 🥳

@@ -81,8 +84,51 @@ def get_query(cls, model, info, sort=None, **args):

@classmethod
def resolve_connection(cls, connection_type, model, info, args, resolved):
session = get_session(info.context)
if resolved is None:
if not is_sqlalchemy_version_less_than("1.4") and isinstance(
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we need to check this every request, it will just introduce inefficiencies in every request. Maybe a "startup check" or documentation does the job here.

Copy link
Member

Choose a reason for hiding this comment

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

Maybe changing lines 16/17 so that AsyncSession has a defined value (None), if sqlalchemy_version < 1.4 will be more elegant

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The problem here is the isinstance check. The import of it only happens if the version is nor less than 1.4. So in all other versions I need to fail the if statement before doing the isinstance check. I can imagine to ways around:

  • do the startup check as you described and store the result in a constant SQLALCHEMY_LESS_THAN_1_4, so I just need to check the boolean value of the constant
  • Invert this statement to check if it is a synchronous session

I would prefer the first option, as I am not sure how to check if a session is sync in sqlalchemy. There seems to be a protected class attribute _is_asyncio with sessions, but checking that would probably agin require checking the version of sqlalchemy first as I assume the property is new 😉

Copy link
Member

Choose a reason for hiding this comment

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

Startup check option sounds great!

Copy link
Member

Choose a reason for hiding this comment

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

We'll need this anyways as 2.0 is in beta; so we should use 1.4 style on all queries if possible - even in sync session. Scope of a different PR though. This will also require the global option.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Implemented the startup option 👍

graphene_sqlalchemy/tests/models.py Show resolved Hide resolved
graphene_sqlalchemy/tests/test_batching.py Outdated Show resolved Hide resolved
graphene_sqlalchemy/tests/test_batching.py Show resolved Hide resolved
assert select_statements[-2].startswith("SELECT reporters_1.id")
assert "WHERE reporters_1.id IN" in select_statements[-2]
if async_session:
assert len(select_statements) == 2 # TODO: Figure out why async has less calls
Copy link
Member

Choose a reason for hiding this comment

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

Have you found that out?

Copy link
Contributor Author

@jendrikjoe jendrikjoe Oct 7, 2022

Choose a reason for hiding this comment

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

Yes the underlying reason is that with an async session, the count query is replaced by taking the length of the executed query. Definitely not ideal, but as all connections are created using connection_from_array_slice, all seem to anyhow query all elements and then filter by first, last, before and after 🤔
I would replace the comment here with the actual reason and would maybe then raise a separate MR about connections and how they are create, after understanding them better and having a discussion on what the roadmap for them is anyhow 🙂

Comment on lines 89 to 96
@benchmark
def execute_query():
result = schema.execute(
async def execute_query():
result = await schema.execute_async(
query,
context_value={"session": session_factory()},
)
assert not result.errors

Copy link
Member

Choose a reason for hiding this comment

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

This doesn't seem to be compatible with asyncio:
ionelmc/pytest-benchmark#66

I get warnings in this scheme, meaning that these tests don't really do what they should currently:

  graphene_sqlalchemy/tests/test_benchmark.py:218: RuntimeWarning: coroutine 'benchmark_query.<locals>.execute_query' was never awaited
    await benchmark_query(

Copy link
Member

@erikwrede erikwrede Oct 6, 2022

Choose a reason for hiding this comment

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

This could be a workaround: https://github.com/graphql-python/graphql-core/blob/1a5aca82eb1d026a8f48dd7c7d8f9c56b8cd651b/tests/benchmarks/test_execution_async.py#L41

Otherwise, maybe some of the solutions in the issue I linked could work as well. I'll try it out once I find some time

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great catch 👍 I couldn't get the workaround running without adding nest_asyncio to the dev dependencies. I hope that is fine 🙂

Comment on lines +341 to +343
@pytest.mark.asyncio
async def test_custom_identifier(session):
await add_test_data(session)
Copy link
Member

Choose a reason for hiding this comment

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

I get the following warning here (doesn't appear without this PR):

graphene_sqlalchemy/tests/test_query.py::test_custom_identifier[False]
  /Users/erik/.pyenv/versions/graphene-sqalchemy/lib/python3.10/site-packages/_pytest/unraisableexception.py:78: PytestUnraisableExceptionWarning: Exception ignored in: <coroutine object SQLAlchemyObjectType.get_node.<locals>.get_result at 0x11496bae0>
  
  Traceback (most recent call last):
    File "/Users/erik/.pyenv/versions/graphene-sqalchemy/lib/python3.10/site-packages/graphql/language/visitor.py", line 324, in get_enter_leave_for_kind
      return self.enter_leave_map[kind]
  KeyError: 'operation_definition'
  
  During handling of the above exception, another exception occurred:
  
  Traceback (most recent call last):
    File "/Users/erik/.pyenv/versions/graphene-sqalchemy/lib/python3.10/site-packages/graphql/language/visitor.py", line 136, in get_enter_leave_for_kind
      return self.enter_leave_map[kind]
  KeyError: 'operation_definition'
  

Copy link
Contributor Author

@jendrikjoe jendrikjoe Oct 7, 2022

Choose a reason for hiding this comment

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

That is interesting, I cannot reproduce that locally 🤔 Given your output, I tried to reproduce it using tox -e py310-sql14 🙂

graphene_sqlalchemy/utils.py Outdated Show resolved Hide resolved
graphene_sqlalchemy/tests/test_batching.py Outdated Show resolved Hide resolved
Copy link
Member

@erikwrede erikwrede left a comment

Choose a reason for hiding this comment

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

After playing around with it for a while, I think no further modifications are necessary, this has reached the state of a great first version of async session support.

@erikwrede erikwrede merged commit 32d0d18 into graphql-python:master Dec 21, 2022
@jendrikjoe jendrikjoe deleted the add-async-support branch December 21, 2022 13:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants