#37033: Psycopg2 has different semantics than psycopg3 and should not be 
deprecated
--------------------------+-----------------------------------------
     Reporter:  sastraxi  |                     Type:  Uncategorized
       Status:  new       |                Component:  Uncategorized
      Version:  6.0       |                 Severity:  Normal
     Keywords:            |             Triage Stage:  Unreviewed
    Has patch:  0         |      Needs documentation:  0
  Needs tests:  0         |  Patch needs improvement:  0
Easy pickings:  0         |                    UI/UX:  0
--------------------------+-----------------------------------------
 The context: we have been trialling psycopg3 in our (async-free!) Django
 5.2 codebase. I noted that
 https://docs.djangoproject.com/en/6.0/releases/4.2/#psycopg-3-support says
 "Support for psycopg2 is likely to be deprecated and removed at some point
 in the future." -- this ticket has my observations that make me believe
 this is not a good idea.

 We have many Celery (5.6.2) tasks that run on idempotent queue workers
 with Django fully loaded. When we re-deploy, we kill the worker containers
 and re-queue the tasks that were in-flight / ready to process on that
 worker. It does this by raising exceptions from signal handlers:
 https://github.com/celery/billiard/blob/main/billiard/common.py#L106
 ultimately raises a `SystemExit` exception from inside the signal handler
 when it receives the SIGQUIT signal.

 As outlined in [Safe Asynchronous Exceptions for
 Python](https://www.cs.williams.edu/~freund/papers/python.pdf), raising
 exceptions in this way can arbitrarily interrupt execute at any bytecode
 boundary.

 === Example case

 Here's a simplified task handler so you can visualize the problem:

 {{{#!python
 @celery_app.task(MY_IDEMPOTENT_QUEUE)
 def my_task():
     with transaction.atomic():
         [...]
         my_model.save()
         transaction.on_commit(lambda: my_other_task.delay())
 }}}

 Sometimes, when a signal is delivered while commit() is running, the
 following happens:

 1. PostgreSQL has applied the COMMIT (we can later verify DB state).
 2. psycopg3 is interrupted during its Python‑level wait loop; an exception
 bubbles out of commit() (or _set_autocommit) rather than a normal return.
 3. Django sees an exception in `atomic.__exit__` and therefore does not
 run `on_commit` callbacks, even though the transaction actually committed.

 === Root cause

 * psycopg2's COMMIT path uses `PQexec("COMMIT")` inside a
 Py_BEGIN_ALLOW_THREADS region, so the interpreter will not run the Python
 signal handler until that call returns; this effectively makes COMMIT
 atomic w.r.t. Python async exceptions.
 * psycopg3's design explicitly avoids blocking libpq calls, using a Python
 generator + wait functions to drive the COMMIT protocol step-by-step. That
 means Python bytecode is executing throughout, so signal handlers can
 raise exceptions mid‑protocol.

 I do not believe this will be possible to solve with psycopg3's approach,
 so developers using Django's on_commit inside of celery worker handlers
 need to be aware of this behaviour.
-- 
Ticket URL: <https://code.djangoproject.com/ticket/37033>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion visit 
https://groups.google.com/d/msgid/django-updates/0107019d8729a824-d4716301-ae32-4b0f-b4c0-3ad97f877f27-000000%40eu-central-1.amazonses.com.

Reply via email to