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
1=== modified file 'storm/database.py'
2--- storm/database.py 2011-09-12 10:56:26 +0000
3+++ storm/database.py 2011-09-28 06:23:18 +0000
4@@ -353,7 +353,7 @@
5 else:
6 self._state = STATE_CONNECTED
7
8- def is_disconnection_error(self, exc):
9+ def is_disconnection_error(self, exc, extra_disconnection_errors=()):
10 """Check whether an exception represents a database disconnection.
11
12 This should be overridden by backends to detect whichever
13@@ -363,10 +363,14 @@
14
15 def _check_disconnect(self, function, *args, **kwargs):
16 """Run the given function, checking for database disconnections."""
17+ # Allow the caller to specify additional exception types that
18+ # should be treated as possible disconnection errors.
19+ extra_disconnection_errors = kwargs.pop(
20+ 'extra_disconnection_errors', ())
21 try:
22 return function(*args, **kwargs)
23- except Error, exc:
24- if self.is_disconnection_error(exc):
25+ except Exception, exc:
26+ if self.is_disconnection_error(exc, extra_disconnection_errors):
27 self._state = STATE_DISCONNECTED
28 self._raw_connection = None
29 raise DisconnectionError(str(exc))
30
31=== modified file 'storm/databases/mysql.py'
32--- storm/databases/mysql.py 2009-09-21 17:03:08 +0000
33+++ storm/databases/mysql.py 2011-09-28 06:23:18 +0000
34@@ -114,9 +114,10 @@
35 else:
36 yield param
37
38- def is_disconnection_error(self, exc):
39+ def is_disconnection_error(self, exc, extra_disconnection_errors=()):
40 # http://dev.mysql.com/doc/refman/5.0/en/gone-away.html
41- return (isinstance(exc, OperationalError) and
42+ return (isinstance(exc, (OperationalError,
43+ extra_disconnection_errors)) and
44 exc.args[0] in (2006, 2013)) # (SERVER_GONE_ERROR, SERVER_LOST)
45
46
47
48=== modified file 'storm/databases/postgres.py'
49--- storm/databases/postgres.py 2011-03-21 18:59:55 +0000
50+++ storm/databases/postgres.py 2011-09-28 06:23:18 +0000
51@@ -276,9 +276,9 @@
52 else:
53 yield param
54
55- def is_disconnection_error(self, exc):
56+ def is_disconnection_error(self, exc, extra_disconnection_errors=()):
57 if not isinstance(exc, (InterfaceError, OperationalError,
58- ProgrammingError)):
59+ ProgrammingError, extra_disconnection_errors)):
60 return False
61
62 # XXX: 2007-09-17 jamesh
63
64=== modified file 'storm/django/backend/base.py'
65--- storm/django/backend/base.py 2011-07-14 11:36:04 +0000
66+++ storm/django/backend/base.py 2011-09-28 06:23:18 +0000
67@@ -18,6 +18,9 @@
68 def _get_connection(self):
69 if self._store is None:
70 self._store = get_store(settings.DATABASE_NAME)
71+ # Make sure that the store is registered with the transaction
72+ # manager: we don't know what the connection will be used for.
73+ self._store._event.emit("register-transaction")
74 self._store._connection._ensure_connected()
75 return self._store._connection._raw_connection
76
77@@ -33,7 +36,6 @@
78
79 def _cursor(self, *args):
80 cursor = super(StormDatabaseWrapperMixin, self)._cursor(*args)
81- self._store._event.emit("register-transaction")
82 return StormCursorWrapper(self._store, cursor)
83
84 def _commit(self):
85@@ -54,9 +56,14 @@
86 """A cursor wrapper that checks for disconnection errors."""
87
88 def __init__(self, store, cursor):
89- self._check_disconnect = store._connection._check_disconnect
90+ self._connection = store._connection
91 self._cursor = cursor
92
93+ def _check_disconnect(self, *args, **kwargs):
94+ from django.db import DatabaseError as DjangoDatabaseError
95+ kwargs['extra_disconnection_errors'] = DjangoDatabaseError
96+ return self._connection._check_disconnect(*args, **kwargs)
97+
98 def execute(self, statement, *args):
99 """Execute an SQL statement."""
100 return self._check_disconnect(self._cursor.execute, statement, *args)
101
102=== modified file 'tests/database.py'
103--- tests/database.py 2009-05-08 17:27:18 +0000
104+++ tests/database.py 2011-09-28 06:23:18 +0000
105@@ -236,7 +236,8 @@
106
107 install_tracer(tracer_mock)
108 self.connection.is_disconnection_error = (
109- lambda exc: 'connection closed' in str(exc))
110+ lambda exc, extra_disconnection_errors=():
111+ 'connection closed' in str(exc))
112
113 self.assertRaises(DisconnectionError,
114 self.connection.execute, "something")
115@@ -251,7 +252,8 @@
116
117 install_tracer(tracer_mock)
118 self.connection.is_disconnection_error = (
119- lambda exc: 'connection closed' in str(exc))
120+ lambda exc, extra_disconnection_errors=():
121+ 'connection closed' in str(exc))
122
123 self.assertRaises(DisconnectionError,
124 self.connection.execute, "something")
125@@ -270,7 +272,8 @@
126
127 install_tracer(tracer_mock)
128 self.connection.is_disconnection_error = (
129- lambda exc: 'connection closed' in str(exc))
130+ lambda exc, extra_disconnection_errors=():
131+ 'connection closed' in str(exc))
132
133 self.assertRaises(DisconnectionError,
134 self.connection.execute, "something")
135@@ -356,7 +359,8 @@
136 class FakeException(DatabaseError):
137 """A fake database exception that indicates a disconnection."""
138 self.connection.is_disconnection_error = (
139- lambda exc: isinstance(exc, FakeException))
140+ lambda exc, extra_disconnection_errors=():
141+ isinstance(exc, FakeException))
142
143 self.assertEqual(self.connection._state,
144 storm.database.STATE_CONNECTED)
145@@ -369,6 +373,32 @@
146 storm.database.STATE_DISCONNECTED)
147 self.assertEqual(self.connection._raw_connection, None)
148
149+ def test_wb_check_disconnection_extra_errors(self):
150+ """Ensure that _check_disconnect() can check for additional
151+ exceptions."""
152+ class FakeException(DatabaseError):
153+ """A fake database exception that indicates a disconnection."""
154+ self.connection.is_disconnection_error = (
155+ lambda exc, extra_disconnection_errors=():
156+ isinstance(exc, extra_disconnection_errors))
157+
158+ self.assertEqual(self.connection._state,
159+ storm.database.STATE_CONNECTED)
160+ # Error is converted to DisconnectionError:
161+ def raise_exception():
162+ raise FakeException
163+ # Exception passes through as normal.
164+ self.assertRaises(FakeException,
165+ self.connection._check_disconnect, raise_exception)
166+ self.assertEqual(self.connection._state,
167+ storm.database.STATE_CONNECTED)
168+ # Exception treated as a disconnection when keyword argument passed.
169+ self.assertRaises(DisconnectionError,
170+ self.connection._check_disconnect, raise_exception,
171+ extra_disconnection_errors=FakeException)
172+ self.assertEqual(self.connection._state,
173+ storm.database.STATE_DISCONNECTED)
174+
175 def test_wb_rollback_clears_disconnected_connection(self):
176 """Check that rollback clears the DISCONNECTED state."""
177 self.connection._state = storm.database.STATE_DISCONNECTED
178
179=== modified file 'tests/django/backend.py'
180--- tests/django/backend.py 2011-07-25 18:59:25 +0000
181+++ tests/django/backend.py 2011-09-28 06:23:18 +0000
182@@ -32,6 +32,7 @@
183 from storm.django import stores
184 from storm.zope.zstorm import global_zstorm, StoreDataManager
185
186+import storm.database
187 from storm.exceptions import DisconnectionError
188
189 from tests.helper import TestHelper
190@@ -146,6 +147,19 @@
191 result = cursor.fetchall()
192 self.assertEqual(len(result), 0)
193
194+ def test_register_transaction(self):
195+ wrapper = make_wrapper()
196+ store = global_zstorm.get("django")
197+ # Watch for register-transaction calls.
198+ calls = []
199+ def register_transaction(owner):
200+ calls.append(owner)
201+ store._event.hook("register-transaction", register_transaction)
202+
203+ cursor = wrapper.cursor()
204+ cursor.execute("SELECT 1")
205+ self.assertNotEqual(calls, [])
206+
207
208 class DjangoBackendDisconnectionTests(DatabaseDisconnectionMixin):
209
210@@ -174,20 +188,58 @@
211 transaction.manager.free(transaction.get())
212 super(DjangoBackendDisconnectionTests, self).tearDown()
213
214- def test_disconnect(self):
215- from django.db import DatabaseError as DjangoDatabaseError
216+ def test_wb_disconnect(self):
217 wrapper = make_wrapper()
218+ store = global_zstorm.get("django")
219 cursor = wrapper.cursor()
220 cursor.execute("SELECT 'about to reset connection'")
221 wrapper._rollback()
222+ cursor = wrapper.cursor()
223 self.proxy.restart()
224- cursor = wrapper.cursor()
225- self.assertRaises((DjangoDatabaseError, DisconnectionError),
226- cursor.execute, "SELECT 1")
227- wrapper._rollback()
228-
229- cursor = wrapper.cursor()
230- cursor.execute("SELECT 1")
231+ self.assertRaises(DisconnectionError, cursor.execute, "SELECT 1")
232+ self.assertEqual(
233+ store._connection._state, storm.database.STATE_DISCONNECTED)
234+ wrapper._rollback()
235+
236+ self.assertEqual(
237+ store._connection._state, storm.database.STATE_RECONNECT)
238+ cursor = wrapper.cursor()
239+ cursor.execute("SELECT 1")
240+
241+ def test_wb_transaction_registration(self):
242+ wrapper = make_wrapper()
243+ store = global_zstorm.get("django")
244+ # Watch for register-transaction calls.
245+ calls = []
246+ def register_transaction(owner):
247+ calls.append(owner)
248+ store._event.hook("register-transaction", register_transaction)
249+
250+ # Simulate a disconnection, and put the connection into a
251+ # state where it would attempt to reconnect.
252+ store._connection._raw_connection = None
253+ store._connection._state = storm.database.STATE_RECONNECT
254+ self.proxy.stop()
255+
256+ self.assertRaises(DisconnectionError, wrapper.cursor)
257+ # The connection is in the disconnected state, and has been
258+ # registered with any listening transaction manager.
259+ self.assertNotEqual(calls, [])
260+ self.assertEqual(
261+ store._connection._state, storm.database.STATE_DISCONNECTED)
262+
263+ wrapper._rollback()
264+ del calls[:]
265+
266+ # Now reconnect:
267+ self.proxy.start()
268+ cursor = wrapper.cursor()
269+ cursor.execute("SELECT 1")
270+ # The connection is up, and has been registered with any
271+ # listening transaction manager.
272+ self.assertNotEqual(calls, [])
273+ self.assertEqual(
274+ store._connection._state, storm.database.STATE_CONNECTED)
275
276
277 class PostgresDjangoBackendTests(DjangoBackendTests, TestHelper):

Subscribers

People subscribed via source and target branches

to status/vote changes: