Merge lp:~jamesh/storm/bug-854787 into lp:storm
- bug-854787
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Stuart Bishop (community) | Approve | ||
Martin Albisetti (community) | Approve | ||
Review via email: mp+76559@code.launchpad.net |
Commit message
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_
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.
- 414. By James Henstridge
-
Import the Django exception from django.db instead of django.db.utils.
Stuart Bishop (stub) wrote : | # |
- 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.
James Henstridge (jamesh) wrote : | # |
The object returned by DatabaseWrapper
1. StormCursorWrapper wrapping
2. CursorWrapper from django.
3. a psycopg2 cursor
We're checking the exceptions at layer (1), and the exceptions are being modified at (2). The is_disconnectio
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.
James Henstridge (jamesh) wrote : | # |
I've filed a bug report suggesting how Django could improve matters on their end:
https:/
I'd still like to get something in place to handle current Django.
Martin Albisetti (beuno) : | # |
Stuart Bishop (stub) : | # |
Preview Diff
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 | 353 | else: | 353 | else: |
6 | 354 | self._state = STATE_CONNECTED | 354 | self._state = STATE_CONNECTED |
7 | 355 | 355 | ||
9 | 356 | def is_disconnection_error(self, exc): | 356 | def is_disconnection_error(self, exc, extra_disconnection_errors=()): |
10 | 357 | """Check whether an exception represents a database disconnection. | 357 | """Check whether an exception represents a database disconnection. |
11 | 358 | 358 | ||
12 | 359 | This should be overridden by backends to detect whichever | 359 | This should be overridden by backends to detect whichever |
13 | @@ -363,10 +363,14 @@ | |||
14 | 363 | 363 | ||
15 | 364 | def _check_disconnect(self, function, *args, **kwargs): | 364 | def _check_disconnect(self, function, *args, **kwargs): |
16 | 365 | """Run the given function, checking for database disconnections.""" | 365 | """Run the given function, checking for database disconnections.""" |
17 | 366 | # Allow the caller to specify additional exception types that | ||
18 | 367 | # should be treated as possible disconnection errors. | ||
19 | 368 | extra_disconnection_errors = kwargs.pop( | ||
20 | 369 | 'extra_disconnection_errors', ()) | ||
21 | 366 | try: | 370 | try: |
22 | 367 | return function(*args, **kwargs) | 371 | return function(*args, **kwargs) |
25 | 368 | except Error, exc: | 372 | except Exception, exc: |
26 | 369 | if self.is_disconnection_error(exc): | 373 | if self.is_disconnection_error(exc, extra_disconnection_errors): |
27 | 370 | self._state = STATE_DISCONNECTED | 374 | self._state = STATE_DISCONNECTED |
28 | 371 | self._raw_connection = None | 375 | self._raw_connection = None |
29 | 372 | raise DisconnectionError(str(exc)) | 376 | raise DisconnectionError(str(exc)) |
30 | 373 | 377 | ||
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 | 114 | else: | 114 | else: |
36 | 115 | yield param | 115 | yield param |
37 | 116 | 116 | ||
39 | 117 | def is_disconnection_error(self, exc): | 117 | def is_disconnection_error(self, exc, extra_disconnection_errors=()): |
40 | 118 | # http://dev.mysql.com/doc/refman/5.0/en/gone-away.html | 118 | # http://dev.mysql.com/doc/refman/5.0/en/gone-away.html |
42 | 119 | return (isinstance(exc, OperationalError) and | 119 | return (isinstance(exc, (OperationalError, |
43 | 120 | extra_disconnection_errors)) and | ||
44 | 120 | exc.args[0] in (2006, 2013)) # (SERVER_GONE_ERROR, SERVER_LOST) | 121 | exc.args[0] in (2006, 2013)) # (SERVER_GONE_ERROR, SERVER_LOST) |
45 | 121 | 122 | ||
46 | 122 | 123 | ||
47 | 123 | 124 | ||
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 | 276 | else: | 276 | else: |
53 | 277 | yield param | 277 | yield param |
54 | 278 | 278 | ||
56 | 279 | def is_disconnection_error(self, exc): | 279 | def is_disconnection_error(self, exc, extra_disconnection_errors=()): |
57 | 280 | if not isinstance(exc, (InterfaceError, OperationalError, | 280 | if not isinstance(exc, (InterfaceError, OperationalError, |
59 | 281 | ProgrammingError)): | 281 | ProgrammingError, extra_disconnection_errors)): |
60 | 282 | return False | 282 | return False |
61 | 283 | 283 | ||
62 | 284 | # XXX: 2007-09-17 jamesh | 284 | # XXX: 2007-09-17 jamesh |
63 | 285 | 285 | ||
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 | 18 | def _get_connection(self): | 18 | def _get_connection(self): |
69 | 19 | if self._store is None: | 19 | if self._store is None: |
70 | 20 | self._store = get_store(settings.DATABASE_NAME) | 20 | self._store = get_store(settings.DATABASE_NAME) |
71 | 21 | # Make sure that the store is registered with the transaction | ||
72 | 22 | # manager: we don't know what the connection will be used for. | ||
73 | 23 | self._store._event.emit("register-transaction") | ||
74 | 21 | self._store._connection._ensure_connected() | 24 | self._store._connection._ensure_connected() |
75 | 22 | return self._store._connection._raw_connection | 25 | return self._store._connection._raw_connection |
76 | 23 | 26 | ||
77 | @@ -33,7 +36,6 @@ | |||
78 | 33 | 36 | ||
79 | 34 | def _cursor(self, *args): | 37 | def _cursor(self, *args): |
80 | 35 | cursor = super(StormDatabaseWrapperMixin, self)._cursor(*args) | 38 | cursor = super(StormDatabaseWrapperMixin, self)._cursor(*args) |
81 | 36 | self._store._event.emit("register-transaction") | ||
82 | 37 | return StormCursorWrapper(self._store, cursor) | 39 | return StormCursorWrapper(self._store, cursor) |
83 | 38 | 40 | ||
84 | 39 | def _commit(self): | 41 | def _commit(self): |
85 | @@ -54,9 +56,14 @@ | |||
86 | 54 | """A cursor wrapper that checks for disconnection errors.""" | 56 | """A cursor wrapper that checks for disconnection errors.""" |
87 | 55 | 57 | ||
88 | 56 | def __init__(self, store, cursor): | 58 | def __init__(self, store, cursor): |
90 | 57 | self._check_disconnect = store._connection._check_disconnect | 59 | self._connection = store._connection |
91 | 58 | self._cursor = cursor | 60 | self._cursor = cursor |
92 | 59 | 61 | ||
93 | 62 | def _check_disconnect(self, *args, **kwargs): | ||
94 | 63 | from django.db import DatabaseError as DjangoDatabaseError | ||
95 | 64 | kwargs['extra_disconnection_errors'] = DjangoDatabaseError | ||
96 | 65 | return self._connection._check_disconnect(*args, **kwargs) | ||
97 | 66 | |||
98 | 60 | def execute(self, statement, *args): | 67 | def execute(self, statement, *args): |
99 | 61 | """Execute an SQL statement.""" | 68 | """Execute an SQL statement.""" |
100 | 62 | return self._check_disconnect(self._cursor.execute, statement, *args) | 69 | return self._check_disconnect(self._cursor.execute, statement, *args) |
101 | 63 | 70 | ||
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 | 236 | 236 | ||
107 | 237 | install_tracer(tracer_mock) | 237 | install_tracer(tracer_mock) |
108 | 238 | self.connection.is_disconnection_error = ( | 238 | self.connection.is_disconnection_error = ( |
110 | 239 | lambda exc: 'connection closed' in str(exc)) | 239 | lambda exc, extra_disconnection_errors=(): |
111 | 240 | 'connection closed' in str(exc)) | ||
112 | 240 | 241 | ||
113 | 241 | self.assertRaises(DisconnectionError, | 242 | self.assertRaises(DisconnectionError, |
114 | 242 | self.connection.execute, "something") | 243 | self.connection.execute, "something") |
115 | @@ -251,7 +252,8 @@ | |||
116 | 251 | 252 | ||
117 | 252 | install_tracer(tracer_mock) | 253 | install_tracer(tracer_mock) |
118 | 253 | self.connection.is_disconnection_error = ( | 254 | self.connection.is_disconnection_error = ( |
120 | 254 | lambda exc: 'connection closed' in str(exc)) | 255 | lambda exc, extra_disconnection_errors=(): |
121 | 256 | 'connection closed' in str(exc)) | ||
122 | 255 | 257 | ||
123 | 256 | self.assertRaises(DisconnectionError, | 258 | self.assertRaises(DisconnectionError, |
124 | 257 | self.connection.execute, "something") | 259 | self.connection.execute, "something") |
125 | @@ -270,7 +272,8 @@ | |||
126 | 270 | 272 | ||
127 | 271 | install_tracer(tracer_mock) | 273 | install_tracer(tracer_mock) |
128 | 272 | self.connection.is_disconnection_error = ( | 274 | self.connection.is_disconnection_error = ( |
130 | 273 | lambda exc: 'connection closed' in str(exc)) | 275 | lambda exc, extra_disconnection_errors=(): |
131 | 276 | 'connection closed' in str(exc)) | ||
132 | 274 | 277 | ||
133 | 275 | self.assertRaises(DisconnectionError, | 278 | self.assertRaises(DisconnectionError, |
134 | 276 | self.connection.execute, "something") | 279 | self.connection.execute, "something") |
135 | @@ -356,7 +359,8 @@ | |||
136 | 356 | class FakeException(DatabaseError): | 359 | class FakeException(DatabaseError): |
137 | 357 | """A fake database exception that indicates a disconnection.""" | 360 | """A fake database exception that indicates a disconnection.""" |
138 | 358 | self.connection.is_disconnection_error = ( | 361 | self.connection.is_disconnection_error = ( |
140 | 359 | lambda exc: isinstance(exc, FakeException)) | 362 | lambda exc, extra_disconnection_errors=(): |
141 | 363 | isinstance(exc, FakeException)) | ||
142 | 360 | 364 | ||
143 | 361 | self.assertEqual(self.connection._state, | 365 | self.assertEqual(self.connection._state, |
144 | 362 | storm.database.STATE_CONNECTED) | 366 | storm.database.STATE_CONNECTED) |
145 | @@ -369,6 +373,32 @@ | |||
146 | 369 | storm.database.STATE_DISCONNECTED) | 373 | storm.database.STATE_DISCONNECTED) |
147 | 370 | self.assertEqual(self.connection._raw_connection, None) | 374 | self.assertEqual(self.connection._raw_connection, None) |
148 | 371 | 375 | ||
149 | 376 | def test_wb_check_disconnection_extra_errors(self): | ||
150 | 377 | """Ensure that _check_disconnect() can check for additional | ||
151 | 378 | exceptions.""" | ||
152 | 379 | class FakeException(DatabaseError): | ||
153 | 380 | """A fake database exception that indicates a disconnection.""" | ||
154 | 381 | self.connection.is_disconnection_error = ( | ||
155 | 382 | lambda exc, extra_disconnection_errors=(): | ||
156 | 383 | isinstance(exc, extra_disconnection_errors)) | ||
157 | 384 | |||
158 | 385 | self.assertEqual(self.connection._state, | ||
159 | 386 | storm.database.STATE_CONNECTED) | ||
160 | 387 | # Error is converted to DisconnectionError: | ||
161 | 388 | def raise_exception(): | ||
162 | 389 | raise FakeException | ||
163 | 390 | # Exception passes through as normal. | ||
164 | 391 | self.assertRaises(FakeException, | ||
165 | 392 | self.connection._check_disconnect, raise_exception) | ||
166 | 393 | self.assertEqual(self.connection._state, | ||
167 | 394 | storm.database.STATE_CONNECTED) | ||
168 | 395 | # Exception treated as a disconnection when keyword argument passed. | ||
169 | 396 | self.assertRaises(DisconnectionError, | ||
170 | 397 | self.connection._check_disconnect, raise_exception, | ||
171 | 398 | extra_disconnection_errors=FakeException) | ||
172 | 399 | self.assertEqual(self.connection._state, | ||
173 | 400 | storm.database.STATE_DISCONNECTED) | ||
174 | 401 | |||
175 | 372 | def test_wb_rollback_clears_disconnected_connection(self): | 402 | def test_wb_rollback_clears_disconnected_connection(self): |
176 | 373 | """Check that rollback clears the DISCONNECTED state.""" | 403 | """Check that rollback clears the DISCONNECTED state.""" |
177 | 374 | self.connection._state = storm.database.STATE_DISCONNECTED | 404 | self.connection._state = storm.database.STATE_DISCONNECTED |
178 | 375 | 405 | ||
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 | 32 | from storm.django import stores | 32 | from storm.django import stores |
184 | 33 | from storm.zope.zstorm import global_zstorm, StoreDataManager | 33 | from storm.zope.zstorm import global_zstorm, StoreDataManager |
185 | 34 | 34 | ||
186 | 35 | import storm.database | ||
187 | 35 | from storm.exceptions import DisconnectionError | 36 | from storm.exceptions import DisconnectionError |
188 | 36 | 37 | ||
189 | 37 | from tests.helper import TestHelper | 38 | from tests.helper import TestHelper |
190 | @@ -146,6 +147,19 @@ | |||
191 | 146 | result = cursor.fetchall() | 147 | result = cursor.fetchall() |
192 | 147 | self.assertEqual(len(result), 0) | 148 | self.assertEqual(len(result), 0) |
193 | 148 | 149 | ||
194 | 150 | def test_register_transaction(self): | ||
195 | 151 | wrapper = make_wrapper() | ||
196 | 152 | store = global_zstorm.get("django") | ||
197 | 153 | # Watch for register-transaction calls. | ||
198 | 154 | calls = [] | ||
199 | 155 | def register_transaction(owner): | ||
200 | 156 | calls.append(owner) | ||
201 | 157 | store._event.hook("register-transaction", register_transaction) | ||
202 | 158 | |||
203 | 159 | cursor = wrapper.cursor() | ||
204 | 160 | cursor.execute("SELECT 1") | ||
205 | 161 | self.assertNotEqual(calls, []) | ||
206 | 162 | |||
207 | 149 | 163 | ||
208 | 150 | class DjangoBackendDisconnectionTests(DatabaseDisconnectionMixin): | 164 | class DjangoBackendDisconnectionTests(DatabaseDisconnectionMixin): |
209 | 151 | 165 | ||
210 | @@ -174,20 +188,58 @@ | |||
211 | 174 | transaction.manager.free(transaction.get()) | 188 | transaction.manager.free(transaction.get()) |
212 | 175 | super(DjangoBackendDisconnectionTests, self).tearDown() | 189 | super(DjangoBackendDisconnectionTests, self).tearDown() |
213 | 176 | 190 | ||
216 | 177 | def test_disconnect(self): | 191 | def test_wb_disconnect(self): |
215 | 178 | from django.db import DatabaseError as DjangoDatabaseError | ||
217 | 179 | wrapper = make_wrapper() | 192 | wrapper = make_wrapper() |
218 | 193 | store = global_zstorm.get("django") | ||
219 | 180 | cursor = wrapper.cursor() | 194 | cursor = wrapper.cursor() |
220 | 181 | cursor.execute("SELECT 'about to reset connection'") | 195 | cursor.execute("SELECT 'about to reset connection'") |
221 | 182 | wrapper._rollback() | 196 | wrapper._rollback() |
222 | 197 | cursor = wrapper.cursor() | ||
223 | 183 | self.proxy.restart() | 198 | self.proxy.restart() |
231 | 184 | cursor = wrapper.cursor() | 199 | self.assertRaises(DisconnectionError, cursor.execute, "SELECT 1") |
232 | 185 | self.assertRaises((DjangoDatabaseError, DisconnectionError), | 200 | self.assertEqual( |
233 | 186 | cursor.execute, "SELECT 1") | 201 | store._connection._state, storm.database.STATE_DISCONNECTED) |
234 | 187 | wrapper._rollback() | 202 | wrapper._rollback() |
235 | 188 | 203 | ||
236 | 189 | cursor = wrapper.cursor() | 204 | self.assertEqual( |
237 | 190 | cursor.execute("SELECT 1") | 205 | store._connection._state, storm.database.STATE_RECONNECT) |
238 | 206 | cursor = wrapper.cursor() | ||
239 | 207 | cursor.execute("SELECT 1") | ||
240 | 208 | |||
241 | 209 | def test_wb_transaction_registration(self): | ||
242 | 210 | wrapper = make_wrapper() | ||
243 | 211 | store = global_zstorm.get("django") | ||
244 | 212 | # Watch for register-transaction calls. | ||
245 | 213 | calls = [] | ||
246 | 214 | def register_transaction(owner): | ||
247 | 215 | calls.append(owner) | ||
248 | 216 | store._event.hook("register-transaction", register_transaction) | ||
249 | 217 | |||
250 | 218 | # Simulate a disconnection, and put the connection into a | ||
251 | 219 | # state where it would attempt to reconnect. | ||
252 | 220 | store._connection._raw_connection = None | ||
253 | 221 | store._connection._state = storm.database.STATE_RECONNECT | ||
254 | 222 | self.proxy.stop() | ||
255 | 223 | |||
256 | 224 | self.assertRaises(DisconnectionError, wrapper.cursor) | ||
257 | 225 | # The connection is in the disconnected state, and has been | ||
258 | 226 | # registered with any listening transaction manager. | ||
259 | 227 | self.assertNotEqual(calls, []) | ||
260 | 228 | self.assertEqual( | ||
261 | 229 | store._connection._state, storm.database.STATE_DISCONNECTED) | ||
262 | 230 | |||
263 | 231 | wrapper._rollback() | ||
264 | 232 | del calls[:] | ||
265 | 233 | |||
266 | 234 | # Now reconnect: | ||
267 | 235 | self.proxy.start() | ||
268 | 236 | cursor = wrapper.cursor() | ||
269 | 237 | cursor.execute("SELECT 1") | ||
270 | 238 | # The connection is up, and has been registered with any | ||
271 | 239 | # listening transaction manager. | ||
272 | 240 | self.assertNotEqual(calls, []) | ||
273 | 241 | self.assertEqual( | ||
274 | 242 | store._connection._state, storm.database.STATE_CONNECTED) | ||
275 | 191 | 243 | ||
276 | 192 | 244 | ||
277 | 193 | class PostgresDjangoBackendTests(DjangoBackendTests, TestHelper): | 245 | class PostgresDjangoBackendTests(DjangoBackendTests, TestHelper): |
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_disconnectio n_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_disconnectio n_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.