#21202 closed Cleanup/optimization (fixed)
Atomic masks fatal database errors and instead propagates "connection already closed"
Reported by: | Marti Raudsepp | Owned by: | Aymeric Augustin |
---|---|---|---|
Component: | Database layer (models, ORM) | Version: | 1.6-beta-1 |
Severity: | Normal | Keywords: | |
Cc: | depaolim@… | Triage Stage: | Accepted |
Has patch: | yes | Needs documentation: | no |
Needs tests: | no | Patch needs improvement: | no |
Easy pickings: | no | UI/UX: | no |
Description
I noticed this problem with the new Atomic code in Django 1.6: when the PostgreSQL server raises a FATAL error (meaning that the server closes the connection after the error), then the Atomic block will swallow the original error and propagates a meaningless "InterfaceError: connection already closed" instead. I don't know whether this also affects other database backends.
I find that it's quite annoying to debug problems when the original cause of an error is hidden by subsequent error recovery code. It's not a new problem with Atomic, I believe commit_on_success (and Christophe Pettus's xact) had the same problem.
The easiest way to reproduce this is to terminate your own connection. (In PostgreSQL 9.1 and earlier you need to be superuser to do this):
from django.db import connection from django.db.transaction import atomic with atomic(): cur = connection.cursor() cur.execute("select pg_terminate_backend(pg_backend_pid())") cur.fetchall()
With current Django 1.6 this raises an error like:
Traceback (most recent call last): File "term1.py", line 7, in <module> cur.fetchall() File "/home/marti/src/django/django/db/transaction.py", line 351, in __exit__ connection.set_autocommit(True) File "/home/marti/src/django/django/db/backends/__init__.py", line 335, in set_autocommit self._set_autocommit(autocommit) File "/home/marti/src/django/django/db/backends/postgresql_psycopg2/base.py", line 184, in _set_autocommit self.connection.autocommit = autocommit psycopg2.InterfaceError: connection already closed
It tells you that the connection is closed, but not why. And there's a second problem too: a psycopg2 exception is propagated into user code, without using Django's exception wrappers.
To solve this problem, I submitted pull request 1696: https://github.com/django/django/issues/1696
- Changed BaseDatabaseWrapper.set_autocommit to wrap backend-specific errors.
- Wrapped
Atomic.__exit__
in another layer of try/except and re-raising the original exception in case of an InterfaceError.
Now the original exception is properly raised:
django.db.utils.OperationalError: terminating connection due to administrator command
I can also write unit tests if someone can help me figure out how to test this (the example I provided above requires PostgreSQL, with either superuser privs or version 9.2+).
Ran all 'transactions' and 'db_backends' tests on these changes with both SQLite (OK (skipped=42)) and psycopg2 (OK (skipped=1))
Change History (13)
comment:1 by , 11 years ago
Owner: | changed from | to
---|---|
Patch needs improvement: | set |
Status: | new → assigned |
Triage Stage: | Unreviewed → Accepted |
Type: | Uncategorized → Cleanup/optimization |
comment:2 by , 11 years ago
comment:3 by , 11 years ago
The report only covers SQLite and PostgreSQL. We need to check and possibly make changes for MySQL and Oracle.
The diff in atomic
is very scary, but maybe that's just a side effect of indenting everything.
comment:4 by , 11 years ago
Yeah, the change in atomic is not really scary at all, here's a whitespace-independent diff for 2nd commit:
-
django/db/transaction.py
diff --git a/django/db/transaction.py b/django/db/transaction.py index 86a357f..2a4f16d 100644
a b from functools import wraps 18 18 19 19 from django.db import ( 20 20 connections, DEFAULT_DB_ALIAS, 21 DatabaseError, ProgrammingError )21 DatabaseError, ProgrammingError, InterfaceError) 22 22 from django.utils.decorators import available_attrs 23 23 24 24 … … class Atomic(object): 312 312 connection.in_atomic_block = False 313 313 314 314 try: 315 try: 315 316 if exc_type is None and not connection.needs_rollback: 316 317 if connection.in_atomic_block: 317 318 # Release savepoint if there is one … … class Atomic(object): 354 355 elif not connection.savepoint_ids and not connection.commit_on_exit: 355 356 connection.in_atomic_block = False 356 357 358 except InterfaceError: 359 if exc_type is not None: 360 # We tried to clean up from an exception, but ran into an 361 # InterfaceError, meaning the connection is unusable from now 362 # on. Propagate up the original error instead of masking it 363 # with InterfaceError ("connection already closed") 364 return 365 357 366 def __call__(self, func): 358 367 @wraps(func, assigned=available_attrs(func)) 359 368 def inner(*args, **kwargs):
comment:5 by , 11 years ago
Whatever happened to this? Seeing this happen a lot on production with 1.6. Even worse, the connection then doesn't get closed, and so all future requests error as well. So should we also be closing the bad connection... somewhere?
Longer explanation: As it is with this patch applied (to 1.6, I can't seem to get dev to run), django.db.close_old_connections()
fails on conn.rollback()
(which is called by conn.abort()
) with the same InterfaceError
. This means that conn.close_if_unusable_or_obsolete()
never gets called, and the broken connection is reused for later requests, causing all future requests on that connection to error.
You can replicate this behavior pretty easily as described above, by enabling connection pooling and terminating your own connection in a request, and then trying to make a subsequent request. I suspect this is still the behavior in dev as well, nothing appears to have changed.
My "solution" is to squelch InterfaceError
as well as DatabaseError
in close_old_connections
, but I'm new to the Django internals so I'm not sure if this is best, or if it would be better to just close the connection straight away here in __exit__
before we return, or what. Happy to help sort this but I'm not sure of the correct approach.
comment:6 by , 11 years ago
Patch needs improvement: | unset |
---|
https://github.com/django/django/pull/2468
I believe that patch works for the case when the database closes the connection and also adresses comment 5.
It also prevents further queries until the exit of the outermost atomic block, thus preserving atomicity.
comment:7 by , 11 years ago
Cc: | added |
---|
comment:9 by , 11 years ago
Resolution: | → fixed |
---|---|
Status: | assigned → closed |
Hi, I see you set the "Patch needs improvement" flag, but I can't see a review anywhere. Am I missing something?