Merge lp:~jamesh/storm/bug-854787 into lp:storm

Proposed by James Henstridge
Status: Merged
Merged at revision: 411
Proposed branch: lp:~jamesh/storm/bug-854787
Merge into: lp:storm
Diff against target: 277 lines (+116/-22)
6 files modified
storm/database.py (+7/-3)
storm/databases/mysql.py (+3/-2)
storm/databases/postgres.py (+2/-2)
storm/django/backend/base.py (+9/-2)
tests/database.py (+34/-4)
tests/django/backend.py (+61/-9)
To merge this branch: bzr merge lp:~jamesh/storm/bug-854787
Reviewer Review Type Date Requested Status
Stuart Bishop (community) Approve
Martin Albisetti (community) Approve
Review via email: mp+76559@code.launchpad.net

Description of the change

My previous patch to fix the detection of database disconnection errors in the Storm <-> Django bridge did not work correctly.

While it passed the DB access from the cursor through _check_disconnect(), the cursor it was acting on is one that the Django backend had wrapped to return Django's DatabaseError instead of the adapter's native exceptions. This meant that the is_disconnection_error() check would not even end up looking at the error.

This branch updates the disconnection checkers to handle the extra exception. It is not quite as neat as I'd like, so if anyone has suggestions that might improve it, I'm open to suggestions.

To post a comment you must log in.
lp:~jamesh/storm/bug-854787 updated
414. By James Henstridge

Import the Django exception from django.db instead of django.db.utils.

Revision history for this message
Stuart Bishop (stub) wrote :

You asked for suggestions too, so...

The disconnection errors don't change. Should the connection object have a method to register extra disconnection errors instead of them being passed in as arguments?

Instead of providing a list of exceptions and relying on the existing behavior of is_disconnection_error, should a callable be passed instead? This would provide more control to sniff the database exception when it isn't generic enough. Or is the goal here to insure the current is_disconnection_error logic is used because the PG backend already sniffs the errors strings?

How does this code handle and integrity violation (or other DatabaseError) btw? Given these seem to be thunked down from IntegrityError to DatabaseError it would be worth a test even if the test is demonstrating broken behavior and citing a bug in the Django bug tracker.

lp:~jamesh/storm/bug-854787 updated
415. By James Henstridge

Add a test for the transaction registration code path after a database
disconnection (currently fails).

416. By James Henstridge

Register with the transaction manager when providing the connection to
Django.

417. By James Henstridge

Remove doubled register-transaction call.

Revision history for this message
James Henstridge (jamesh) wrote :

The object returned by DatabaseWrapper._cursor() ends up being:

 1. StormCursorWrapper wrapping
 2. CursorWrapper from django.db.backends.postgresql_psycopg2.base wrapping
 3. a psycopg2 cursor

We're checking the exceptions at layer (1), and the exceptions are being modified at (2). The is_disconnection_error() code is different for each database backend, so I wanted to avoid duplicating the logic.

As for IntegrityError, there is no special handling in this branch because it isn't related to disconnection handling. The Django CursorWrapper classes do map those exceptions to their own IntegrityError though (I guess they think that error is important enough not to squash down to DatabaseError). I agree that it'd be better if Django didn't modify the error objects and used a strategy closer to what Storm does. I'll file a bug report about this, but I do want something that works with current Django releases.

I have also updated the branch to fix a related bug in the transaction registration code (and adds some extra tests that should have been in there from the start), which I believe fixes the last known problems related to disconnection handling.

Revision history for this message
James Henstridge (jamesh) wrote :

I've filed a bug report suggesting how Django could improve matters on their end:

https://code.djangoproject.com/ticket/16948

I'd still like to get something in place to handle current Django.

Revision history for this message
Martin Albisetti (beuno) :
review: Approve
Revision history for this message
Stuart Bishop (stub) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'storm/database.py'
--- storm/database.py 2011-09-12 10:56:26 +0000
+++ storm/database.py 2011-09-28 06:23:18 +0000
@@ -353,7 +353,7 @@
353 else:353 else:
354 self._state = STATE_CONNECTED354 self._state = STATE_CONNECTED
355355
356 def is_disconnection_error(self, exc):356 def is_disconnection_error(self, exc, extra_disconnection_errors=()):
357 """Check whether an exception represents a database disconnection.357 """Check whether an exception represents a database disconnection.
358358
359 This should be overridden by backends to detect whichever359 This should be overridden by backends to detect whichever
@@ -363,10 +363,14 @@
363363
364 def _check_disconnect(self, function, *args, **kwargs):364 def _check_disconnect(self, function, *args, **kwargs):
365 """Run the given function, checking for database disconnections."""365 """Run the given function, checking for database disconnections."""
366 # Allow the caller to specify additional exception types that
367 # should be treated as possible disconnection errors.
368 extra_disconnection_errors = kwargs.pop(
369 'extra_disconnection_errors', ())
366 try:370 try:
367 return function(*args, **kwargs)371 return function(*args, **kwargs)
368 except Error, exc:372 except Exception, exc:
369 if self.is_disconnection_error(exc):373 if self.is_disconnection_error(exc, extra_disconnection_errors):
370 self._state = STATE_DISCONNECTED374 self._state = STATE_DISCONNECTED
371 self._raw_connection = None375 self._raw_connection = None
372 raise DisconnectionError(str(exc))376 raise DisconnectionError(str(exc))
373377
=== modified file 'storm/databases/mysql.py'
--- storm/databases/mysql.py 2009-09-21 17:03:08 +0000
+++ storm/databases/mysql.py 2011-09-28 06:23:18 +0000
@@ -114,9 +114,10 @@
114 else:114 else:
115 yield param115 yield param
116116
117 def is_disconnection_error(self, exc):117 def is_disconnection_error(self, exc, extra_disconnection_errors=()):
118 # http://dev.mysql.com/doc/refman/5.0/en/gone-away.html118 # http://dev.mysql.com/doc/refman/5.0/en/gone-away.html
119 return (isinstance(exc, OperationalError) and119 return (isinstance(exc, (OperationalError,
120 extra_disconnection_errors)) and
120 exc.args[0] in (2006, 2013)) # (SERVER_GONE_ERROR, SERVER_LOST)121 exc.args[0] in (2006, 2013)) # (SERVER_GONE_ERROR, SERVER_LOST)
121122
122123
123124
=== modified file 'storm/databases/postgres.py'
--- storm/databases/postgres.py 2011-03-21 18:59:55 +0000
+++ storm/databases/postgres.py 2011-09-28 06:23:18 +0000
@@ -276,9 +276,9 @@
276 else:276 else:
277 yield param277 yield param
278278
279 def is_disconnection_error(self, exc):279 def is_disconnection_error(self, exc, extra_disconnection_errors=()):
280 if not isinstance(exc, (InterfaceError, OperationalError,280 if not isinstance(exc, (InterfaceError, OperationalError,
281 ProgrammingError)):281 ProgrammingError, extra_disconnection_errors)):
282 return False282 return False
283283
284 # XXX: 2007-09-17 jamesh284 # XXX: 2007-09-17 jamesh
285285
=== modified file 'storm/django/backend/base.py'
--- storm/django/backend/base.py 2011-07-14 11:36:04 +0000
+++ storm/django/backend/base.py 2011-09-28 06:23:18 +0000
@@ -18,6 +18,9 @@
18 def _get_connection(self):18 def _get_connection(self):
19 if self._store is None:19 if self._store is None:
20 self._store = get_store(settings.DATABASE_NAME)20 self._store = get_store(settings.DATABASE_NAME)
21 # Make sure that the store is registered with the transaction
22 # manager: we don't know what the connection will be used for.
23 self._store._event.emit("register-transaction")
21 self._store._connection._ensure_connected()24 self._store._connection._ensure_connected()
22 return self._store._connection._raw_connection25 return self._store._connection._raw_connection
2326
@@ -33,7 +36,6 @@
3336
34 def _cursor(self, *args):37 def _cursor(self, *args):
35 cursor = super(StormDatabaseWrapperMixin, self)._cursor(*args)38 cursor = super(StormDatabaseWrapperMixin, self)._cursor(*args)
36 self._store._event.emit("register-transaction")
37 return StormCursorWrapper(self._store, cursor)39 return StormCursorWrapper(self._store, cursor)
3840
39 def _commit(self):41 def _commit(self):
@@ -54,9 +56,14 @@
54 """A cursor wrapper that checks for disconnection errors."""56 """A cursor wrapper that checks for disconnection errors."""
5557
56 def __init__(self, store, cursor):58 def __init__(self, store, cursor):
57 self._check_disconnect = store._connection._check_disconnect59 self._connection = store._connection
58 self._cursor = cursor60 self._cursor = cursor
5961
62 def _check_disconnect(self, *args, **kwargs):
63 from django.db import DatabaseError as DjangoDatabaseError
64 kwargs['extra_disconnection_errors'] = DjangoDatabaseError
65 return self._connection._check_disconnect(*args, **kwargs)
66
60 def execute(self, statement, *args):67 def execute(self, statement, *args):
61 """Execute an SQL statement."""68 """Execute an SQL statement."""
62 return self._check_disconnect(self._cursor.execute, statement, *args)69 return self._check_disconnect(self._cursor.execute, statement, *args)
6370
=== modified file 'tests/database.py'
--- tests/database.py 2009-05-08 17:27:18 +0000
+++ tests/database.py 2011-09-28 06:23:18 +0000
@@ -236,7 +236,8 @@
236236
237 install_tracer(tracer_mock)237 install_tracer(tracer_mock)
238 self.connection.is_disconnection_error = (238 self.connection.is_disconnection_error = (
239 lambda exc: 'connection closed' in str(exc))239 lambda exc, extra_disconnection_errors=():
240 'connection closed' in str(exc))
240241
241 self.assertRaises(DisconnectionError,242 self.assertRaises(DisconnectionError,
242 self.connection.execute, "something")243 self.connection.execute, "something")
@@ -251,7 +252,8 @@
251252
252 install_tracer(tracer_mock)253 install_tracer(tracer_mock)
253 self.connection.is_disconnection_error = (254 self.connection.is_disconnection_error = (
254 lambda exc: 'connection closed' in str(exc))255 lambda exc, extra_disconnection_errors=():
256 'connection closed' in str(exc))
255257
256 self.assertRaises(DisconnectionError,258 self.assertRaises(DisconnectionError,
257 self.connection.execute, "something")259 self.connection.execute, "something")
@@ -270,7 +272,8 @@
270272
271 install_tracer(tracer_mock)273 install_tracer(tracer_mock)
272 self.connection.is_disconnection_error = (274 self.connection.is_disconnection_error = (
273 lambda exc: 'connection closed' in str(exc))275 lambda exc, extra_disconnection_errors=():
276 'connection closed' in str(exc))
274277
275 self.assertRaises(DisconnectionError,278 self.assertRaises(DisconnectionError,
276 self.connection.execute, "something")279 self.connection.execute, "something")
@@ -356,7 +359,8 @@
356 class FakeException(DatabaseError):359 class FakeException(DatabaseError):
357 """A fake database exception that indicates a disconnection."""360 """A fake database exception that indicates a disconnection."""
358 self.connection.is_disconnection_error = (361 self.connection.is_disconnection_error = (
359 lambda exc: isinstance(exc, FakeException))362 lambda exc, extra_disconnection_errors=():
363 isinstance(exc, FakeException))
360364
361 self.assertEqual(self.connection._state,365 self.assertEqual(self.connection._state,
362 storm.database.STATE_CONNECTED)366 storm.database.STATE_CONNECTED)
@@ -369,6 +373,32 @@
369 storm.database.STATE_DISCONNECTED)373 storm.database.STATE_DISCONNECTED)
370 self.assertEqual(self.connection._raw_connection, None)374 self.assertEqual(self.connection._raw_connection, None)
371375
376 def test_wb_check_disconnection_extra_errors(self):
377 """Ensure that _check_disconnect() can check for additional
378 exceptions."""
379 class FakeException(DatabaseError):
380 """A fake database exception that indicates a disconnection."""
381 self.connection.is_disconnection_error = (
382 lambda exc, extra_disconnection_errors=():
383 isinstance(exc, extra_disconnection_errors))
384
385 self.assertEqual(self.connection._state,
386 storm.database.STATE_CONNECTED)
387 # Error is converted to DisconnectionError:
388 def raise_exception():
389 raise FakeException
390 # Exception passes through as normal.
391 self.assertRaises(FakeException,
392 self.connection._check_disconnect, raise_exception)
393 self.assertEqual(self.connection._state,
394 storm.database.STATE_CONNECTED)
395 # Exception treated as a disconnection when keyword argument passed.
396 self.assertRaises(DisconnectionError,
397 self.connection._check_disconnect, raise_exception,
398 extra_disconnection_errors=FakeException)
399 self.assertEqual(self.connection._state,
400 storm.database.STATE_DISCONNECTED)
401
372 def test_wb_rollback_clears_disconnected_connection(self):402 def test_wb_rollback_clears_disconnected_connection(self):
373 """Check that rollback clears the DISCONNECTED state."""403 """Check that rollback clears the DISCONNECTED state."""
374 self.connection._state = storm.database.STATE_DISCONNECTED404 self.connection._state = storm.database.STATE_DISCONNECTED
375405
=== modified file 'tests/django/backend.py'
--- tests/django/backend.py 2011-07-25 18:59:25 +0000
+++ tests/django/backend.py 2011-09-28 06:23:18 +0000
@@ -32,6 +32,7 @@
32 from storm.django import stores32 from storm.django import stores
33 from storm.zope.zstorm import global_zstorm, StoreDataManager33 from storm.zope.zstorm import global_zstorm, StoreDataManager
3434
35import storm.database
35from storm.exceptions import DisconnectionError36from storm.exceptions import DisconnectionError
3637
37from tests.helper import TestHelper38from tests.helper import TestHelper
@@ -146,6 +147,19 @@
146 result = cursor.fetchall()147 result = cursor.fetchall()
147 self.assertEqual(len(result), 0)148 self.assertEqual(len(result), 0)
148149
150 def test_register_transaction(self):
151 wrapper = make_wrapper()
152 store = global_zstorm.get("django")
153 # Watch for register-transaction calls.
154 calls = []
155 def register_transaction(owner):
156 calls.append(owner)
157 store._event.hook("register-transaction", register_transaction)
158
159 cursor = wrapper.cursor()
160 cursor.execute("SELECT 1")
161 self.assertNotEqual(calls, [])
162
149163
150class DjangoBackendDisconnectionTests(DatabaseDisconnectionMixin):164class DjangoBackendDisconnectionTests(DatabaseDisconnectionMixin):
151165
@@ -174,20 +188,58 @@
174 transaction.manager.free(transaction.get())188 transaction.manager.free(transaction.get())
175 super(DjangoBackendDisconnectionTests, self).tearDown()189 super(DjangoBackendDisconnectionTests, self).tearDown()
176190
177 def test_disconnect(self):191 def test_wb_disconnect(self):
178 from django.db import DatabaseError as DjangoDatabaseError
179 wrapper = make_wrapper()192 wrapper = make_wrapper()
193 store = global_zstorm.get("django")
180 cursor = wrapper.cursor()194 cursor = wrapper.cursor()
181 cursor.execute("SELECT 'about to reset connection'")195 cursor.execute("SELECT 'about to reset connection'")
182 wrapper._rollback()196 wrapper._rollback()
197 cursor = wrapper.cursor()
183 self.proxy.restart()198 self.proxy.restart()
184 cursor = wrapper.cursor()199 self.assertRaises(DisconnectionError, cursor.execute, "SELECT 1")
185 self.assertRaises((DjangoDatabaseError, DisconnectionError),200 self.assertEqual(
186 cursor.execute, "SELECT 1")201 store._connection._state, storm.database.STATE_DISCONNECTED)
187 wrapper._rollback()202 wrapper._rollback()
188203
189 cursor = wrapper.cursor()204 self.assertEqual(
190 cursor.execute("SELECT 1")205 store._connection._state, storm.database.STATE_RECONNECT)
206 cursor = wrapper.cursor()
207 cursor.execute("SELECT 1")
208
209 def test_wb_transaction_registration(self):
210 wrapper = make_wrapper()
211 store = global_zstorm.get("django")
212 # Watch for register-transaction calls.
213 calls = []
214 def register_transaction(owner):
215 calls.append(owner)
216 store._event.hook("register-transaction", register_transaction)
217
218 # Simulate a disconnection, and put the connection into a
219 # state where it would attempt to reconnect.
220 store._connection._raw_connection = None
221 store._connection._state = storm.database.STATE_RECONNECT
222 self.proxy.stop()
223
224 self.assertRaises(DisconnectionError, wrapper.cursor)
225 # The connection is in the disconnected state, and has been
226 # registered with any listening transaction manager.
227 self.assertNotEqual(calls, [])
228 self.assertEqual(
229 store._connection._state, storm.database.STATE_DISCONNECTED)
230
231 wrapper._rollback()
232 del calls[:]
233
234 # Now reconnect:
235 self.proxy.start()
236 cursor = wrapper.cursor()
237 cursor.execute("SELECT 1")
238 # The connection is up, and has been registered with any
239 # listening transaction manager.
240 self.assertNotEqual(calls, [])
241 self.assertEqual(
242 store._connection._state, storm.database.STATE_CONNECTED)
191243
192244
193class PostgresDjangoBackendTests(DjangoBackendTests, TestHelper):245class PostgresDjangoBackendTests(DjangoBackendTests, TestHelper):

Subscribers

People subscribed via source and target branches

to status/vote changes: