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

Remove asgiref dependency from non Django code #1226

Open
wants to merge 4 commits into
base: v3
Choose a base branch
from

Conversation

medihack
Copy link
Member

Closes #1222

Successful PR Checklist:

  • Tests
    • (not applicable?)
  • Documentation
    • (not applicable?)

PR label(s):

Copy link

github-actions bot commented Oct 19, 2024

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  procrastinate
  cli.py
  connector.py
  psycopg_connector.py 137
  shell.py
  worker.py
  procrastinate/contrib/aiopg
  aiopg_connector.py 201
Project Total  

This report was generated by python-coverage-comment-action

@@ -96,15 +95,19 @@ def async_to_sync(awaitable: Callable[..., Awaitable[T]], *args, **kwargs) -> T:
Given a callable returning an awaitable, call the callable, await it
synchronously. Returns the result after it's done.
"""
return sync.async_to_sync(awaitable)(*args, **kwargs)

async def wrapper() -> T:
Copy link
Member Author

Choose a reason for hiding this comment

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

Not sure if there is a nicer way ... just calming down the pyright gods.

Copy link
Contributor

@onlyann onlyann Oct 20, 2024

Choose a reason for hiding this comment

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

Might work if this util (as well as the sync_to_async) is removed. By using asyncio.run directly, that should infer types correctly.

Copy link
Contributor

Choose a reason for hiding this comment

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

From the asyncio docs:

This function should be used as a main entry point for asyncio programs, and should ideally only be called once. It is recommended to use loop_factory to configure the event loop instead of policies

I don't think it is a good idea to hide asyncio.run under this util for the reason above.

Looking at where it gets used:

The Shell code might need to be refactored a bit to use asyncio.run at the top level and not within loop. I believe this is what @ewjoachim mentions in another comment.

If I have not missed something, there is no class inheriting the base async connector that doesn't redefine the sync operations.
In that case, it might be those function implementations could be removed entirely.
In the event user code was to inherit from that class for their own custom connector, it is worth adding a note in the breaking changes section for v3.

Copy link
Member Author

@medihack medihack Oct 21, 2024

Choose a reason for hiding this comment

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

Might work if this util (as well as the sync_to_async) is removed. By using asyncio.run directly, that should infer types correctly.

Yes, I think so too. Those utility functions wrap only one code line each.

The Shell code might need to be refactored a bit to use asyncio.run at the top level and not within loop. I believe this is what @ewjoachim mentions in another comment.

Yes, makes sense. I will refactor it. Another objection of @ewjoachim is (if I understood it correctly) that when you call asyncio.run inside a sync task, it will create a new event loop that is different from the main event loop (that of the worker). This isn't the case with asgiref, which ensures the same event loop is reused. But I think we should just add a warning to the documentation. It is how asyncio works.

If I have not missed something, there is no class inheriting the base async connector that doesn't redefine the sync operations. In that case, it might be those function implementations could be removed entirely. In the event user code was to inherit from that class for their own custom connector, it is worth adding a note in the breaking changes section for v3.

Ok, but let's tackle this in another PR.

you will likely get a `RuntimeError`.
operations. This will only work if you call it inside a function that runs
in its own thread (such as inside a sync job). Otherwise, you will likely
get a `RuntimeError`.
Copy link
Member Author

Choose a reason for hiding this comment

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

I am not sure if this statement is still valid.

Copy link
Member

@ewjoachim ewjoachim left a comment

Choose a reason for hiding this comment

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

I'll need to run additional tests to understand if it's something that will break user code or not.

procrastinate/utils.py Outdated Show resolved Hide resolved
@@ -233,7 +233,7 @@ async def ensure_async() -> Callable[..., Awaitable]:
if inspect.iscoroutinefunction(task.func):
await_func = task
else:
await_func = functools.partial(utils.sync_to_async, task)
await_func = functools.partial(asyncio.to_thread, task)
Copy link
Contributor

Choose a reason for hiding this comment

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

It will be interesting to discover if doing that causes issues with Django applications (https://docs.djangoproject.com/en/5.1/topics/async/#async-safety).

Also, according to https://docs.djangoproject.com/en/5.1/topics/async/#sync-to-async , the use of asgiref.sync_to_async with thread_sensitive=True (the default) needs to be done in a async_to_sync wrapper to work correctly.

Copy link
Member Author

Choose a reason for hiding this comment

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

It will be interesting to discover if doing that causes issues with Django applications (https://docs.djangoproject.com/en/5.1/topics/async/#async-safety).

I don't think so, as the worker manages its own connections (independent of Django), and only the user code would use Django connections.

Copy link
Contributor

@onlyann onlyann Oct 23, 2024

Choose a reason for hiding this comment

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

I was referring to the user code (the task implementation) that is being wrapped here.

What happens then when the user code uses Django connections?
How can they make it work when the thread the code runs in is not the Django main thread?

Copy link
Member Author

@medihack medihack Oct 24, 2024

Choose a reason for hiding this comment

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

From my understanding, it shouldn't matter if the Django connection wasn't initially created in the main thread but directly in the user thread. There is also no request/response cycle like in a Django view that messes around with those connections. And as you already recognized, our previous sync_to_async (with back then thread_sensitive=True) also didn't use the main thread as the Django command never started the worker with async_to_sync. All sync tasks (back then when thread_sensitive was True) used a single thread, but it wasn't the main thread.
Since we switched to thread_sensitive=False all sync tasks use their own separate thread. This should also work as Django connections are thread local. It should work as long as the user doesn't start threads on their own and reuses the connections there (the same as in Django itself).

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah yes, as long as we don't try to support thread_sensitive=true, that should be fine.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, and I also think that using asyncio.to_thread is, in our scenario, the same as using sync_to_async with thread_sensitive=False as we always started our worker in the Django command with asyncio.run and never used async_to_sync there.

procrastinate/shell.py Outdated Show resolved Hide resolved
@medihack medihack force-pushed the replace-asgiref-in-non-django-code branch from f08edc5 to c8b7d66 Compare October 23, 2024 15:05
@medihack medihack force-pushed the replace-asgiref-in-non-django-code branch from c8b7d66 to 457a762 Compare December 17, 2024 22:12
@medihack
Copy link
Member Author

@ewjoachim, is there anything we still have to do here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
PR type: miscellaneous 👾 Contains misc changes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants