Merge lp:~allenap/maas/retry-integrity-errors into lp:~maas-committers/maas/trunk

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: 5136
Proposed branch: lp:~allenap/maas/retry-integrity-errors
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 667 lines (+400/-27)
3 files modified
src/maasserver/testing/testcase.py (+162/-10)
src/maasserver/utils/orm.py (+93/-7)
src/maasserver/utils/tests/test_orm.py (+145/-10)
To merge this branch: bzr merge lp:~allenap/maas/retry-integrity-errors
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
Review via email: mp+297673@code.launchpad.net

Commit message

Retry transactions when they fail with unique violation errors.

This also makes explicit retrying via request_transaction_retry a separate thing rather than a piggyback on serialization failures.

To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Looks really good. The test case for creating the exception is awesome. Just some comments and 1 question.

review: Approve
Revision history for this message
Gavin Panella (allenap) wrote :

Thanks!

Revision history for this message
Gavin Panella (allenap) wrote :

Because of the potential for fallout at this late time in the release cycle I'm running this through CI before landing.

Revision history for this message
Gavin Panella (allenap) wrote :

All good in CI.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/maasserver/testing/testcase.py'
--- src/maasserver/testing/testcase.py 2016-05-24 12:51:54 +0000
+++ src/maasserver/testing/testcase.py 2016-06-17 14:52:22 +0000
@@ -9,9 +9,10 @@
9 'SeleniumTestCase',9 'SeleniumTestCase',
10 'SerializationFailureTestCase',10 'SerializationFailureTestCase',
11 'TestWithoutCrochetMixin',11 'TestWithoutCrochetMixin',
12 'UniqueViolationTestCase',
12 ]13 ]
1314
14from contextlib import closing15from itertools import count
15import socketserver16import socketserver
16import sys17import sys
17import threading18import threading
@@ -28,13 +29,19 @@
28 connection,29 connection,
29 transaction,30 transaction,
30)31)
31from django.db.utils import OperationalError32from django.db.utils import (
33 IntegrityError,
34 OperationalError,
35)
32from fixtures import Fixture36from fixtures import Fixture
33from maasserver.fields import register_mac_type37from maasserver.fields import register_mac_type
34from maasserver.testing.factory import factory38from maasserver.testing.factory import factory
35from maasserver.testing.orm import PostCommitHooksTestMixin39from maasserver.testing.orm import PostCommitHooksTestMixin
36from maasserver.testing.testclient import MAASSensibleClient40from maasserver.testing.testclient import MAASSensibleClient
37from maasserver.utils.orm import is_serialization_failure41from maasserver.utils.orm import (
42 is_serialization_failure,
43 is_unique_violation,
44)
38from maastesting.djangotestcase import (45from maastesting.djangotestcase import (
39 DjangoTestCase,46 DjangoTestCase,
40 DjangoTransactionTestCase,47 DjangoTransactionTestCase,
@@ -234,11 +241,11 @@
234 DjangoTransactionTestCase, PostCommitHooksTestMixin):241 DjangoTransactionTestCase, PostCommitHooksTestMixin):
235242
236 def create_stest_table(self):243 def create_stest_table(self):
237 with closing(connection.cursor()) as cursor:244 with connection.cursor() as cursor:
238 cursor.execute("CREATE TABLE IF NOT EXISTS stest (a INTEGER)")245 cursor.execute("CREATE TABLE IF NOT EXISTS stest (a INTEGER)")
239246
240 def drop_stest_table(self):247 def drop_stest_table(self):
241 with closing(connection.cursor()) as cursor:248 with connection.cursor() as cursor:
242 cursor.execute("DROP TABLE IF EXISTS stest")249 cursor.execute("DROP TABLE IF EXISTS stest")
243250
244 def setUp(self):251 def setUp(self):
@@ -247,7 +254,7 @@
247 # Put something into the stest table upon which to trigger a254 # Put something into the stest table upon which to trigger a
248 # serialization failure.255 # serialization failure.
249 with transaction.atomic():256 with transaction.atomic():
250 with closing(connection.cursor()) as cursor:257 with connection.cursor() as cursor:
251 cursor.execute("INSERT INTO stest VALUES (1)")258 cursor.execute("INSERT INTO stest VALUES (1)")
252259
253 def tearDown(self):260 def tearDown(self):
@@ -258,7 +265,7 @@
258 """Trigger an honest, from the database, serialization failure."""265 """Trigger an honest, from the database, serialization failure."""
259 # Helper to switch the transaction to SERIALIZABLE.266 # Helper to switch the transaction to SERIALIZABLE.
260 def set_serializable():267 def set_serializable():
261 with closing(connection.cursor()) as cursor:268 with connection.cursor() as cursor:
262 cursor.execute("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE")269 cursor.execute("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE")
263270
264 # Perform a conflicting update. This must run in a separate thread. It271 # Perform a conflicting update. This must run in a separate thread. It
@@ -269,7 +276,7 @@
269 def do_conflicting_update():276 def do_conflicting_update():
270 try:277 try:
271 with transaction.atomic():278 with transaction.atomic():
272 with closing(connection.cursor()) as cursor:279 with connection.cursor() as cursor:
273 cursor.execute("UPDATE stest SET a = 2")280 cursor.execute("UPDATE stest SET a = 2")
274 finally:281 finally:
275 close_old_connections()282 close_old_connections()
@@ -278,7 +285,7 @@
278 # Fetch something first. This ensures that we're inside the285 # Fetch something first. This ensures that we're inside the
279 # transaction, and that the database has a reference point for286 # transaction, and that the database has a reference point for
280 # calculating serialization failures.287 # calculating serialization failures.
281 with closing(connection.cursor()) as cursor:288 with connection.cursor() as cursor:
282 cursor.execute("SELECT * FROM stest")289 cursor.execute("SELECT * FROM stest")
283 cursor.fetchall()290 cursor.fetchall()
284291
@@ -290,7 +297,7 @@
290 # Updating the same rows as do_conflicting_update() did will297 # Updating the same rows as do_conflicting_update() did will
291 # trigger a serialization failure. We have to check the __cause__298 # trigger a serialization failure. We have to check the __cause__
292 # to confirm the failure type as reported by PostgreSQL.299 # to confirm the failure type as reported by PostgreSQL.
293 with closing(connection.cursor()) as cursor:300 with connection.cursor() as cursor:
294 cursor.execute("UPDATE stest SET a = 4")301 cursor.execute("UPDATE stest SET a = 4")
295302
296 if connection.in_atomic_block:303 if connection.in_atomic_block:
@@ -312,3 +319,148 @@
312 return sys.exc_info()319 return sys.exc_info()
313 else:320 else:
314 raise321 raise
322
323
324class UniqueViolationTestCase(
325 DjangoTransactionTestCase, PostCommitHooksTestMixin):
326
327 def create_uvtest_table(self):
328 with connection.cursor() as cursor:
329 cursor.execute("DROP TABLE IF EXISTS uvtest")
330 cursor.execute("CREATE TABLE uvtest (a INTEGER PRIMARY KEY)")
331
332 def drop_uvtest_table(self):
333 with connection.cursor() as cursor:
334 cursor.execute("DROP TABLE IF EXISTS uvtest")
335
336 def setUp(self):
337 super(UniqueViolationTestCase, self).setUp()
338 self.conflicting_values = count(1)
339 self.create_uvtest_table()
340
341 def tearDown(self):
342 super(UniqueViolationTestCase, self).tearDown()
343 self.drop_uvtest_table()
344
345 def cause_unique_violation(self):
346 """Trigger an honest, from the database, unique violation.
347
348 This may appear needlessly elaborate, but it's for a good reason.
349 Indexes in PostgreSQL are a bit weird; they don't fully support MVCC
350 so it's possible for situations like the following:
351
352 CREATE TABLE foo (id SERIAL PRIMARY KEY);
353 -- Session A:
354 BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
355 INSERT INTO foo (id) VALUES (1);
356 -- Session B:
357 BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
358 SELECT id FROM foo; -- Nothing.
359 INSERT INTO foo (id) VALUES (1); -- Hangs.
360 -- Session A:
361 COMMIT;
362 -- Session B:
363 ERROR: duplicate key value violates unique constraint "..."
364 DETAIL: Key (id)=(1) already exists.
365
366 Two things to note:
367
368 1. Session B hangs when there's a potential conflict on id's index.
369
370 2. Session B fails with a duplicate key error.
371
372 Both differ from expectations:
373
374 1. I would expect the transaction to continue optimistically and
375 only fail if session A commits.
376
377 2. I would expect a serialisation failure instead.
378
379 This method jumps through hoops to reproduce the situation above so
380 that we're testing against PostgreSQL's exact behaviour as of today,
381 not the behaviour that we observed at a single moment in time.
382 PostgreSQL may change its behaviour in later versions and this test
383 ought to tell us about it.
384
385 """
386 # Helper to switch the transaction to REPEATABLE READ.
387 def set_repeatable_read():
388 with connection.cursor() as cursor:
389 cursor.execute(
390 "SET TRANSACTION ISOLATION LEVEL "
391 "REPEATABLE READ")
392
393 # Both threads / database sessions will attempt to insert this.
394 conflicting_value = next(self.conflicting_values)
395
396 # Perform a conflicting insert. This must run in a separate thread. It
397 # also must begin after the beginning of the transaction in which we
398 # will trigger a unique violation AND commit before that other
399 # transaction commits. This doesn't need to run with any special
400 # isolation; it just needs to be in a transaction.
401 def do_conflicting_insert():
402 try:
403 with transaction.atomic():
404 with connection.cursor() as cursor:
405 cursor.execute(
406 "INSERT INTO uvtest VALUES (%s)",
407 [conflicting_value])
408 finally:
409 close_old_connections()
410
411 def trigger_unique_violation():
412 # Fetch something first. This ensures that we're inside the
413 # transaction, and so the database has a reference point for
414 # repeatable reads.
415 with connection.cursor() as cursor:
416 cursor.execute(
417 "SELECT 1 FROM uvtest WHERE a = %s",
418 [conflicting_value])
419 self.assertIsNone(cursor.fetchone(), (
420 "We've seen through PostgreSQL impenetrable transaction "
421 "isolation — or so we once thought — to witness a "
422 "conflicting value from another database session. "
423 "Needless to say, this requires investigation."))
424
425 # Run do_conflicting_insert() in a separate thread and wait for it
426 # to commit and return.
427 thread = threading.Thread(target=do_conflicting_insert)
428 thread.start()
429 thread.join()
430
431 # Still no sign of that conflicting value from here.
432 with connection.cursor() as cursor:
433 cursor.execute(
434 "SELECT 1 FROM uvtest WHERE a = %s",
435 [conflicting_value])
436 self.assertIsNone(cursor.fetchone(), (
437 "PostgreSQL, once thought of highly in transactional "
438 "circles, has dropped its kimono and disgraced itself "
439 "with its wanton exhibition of conflicting values from "
440 "another's session."))
441
442 # Inserting the same row will trigger a unique violation.
443 with connection.cursor() as cursor:
444 cursor.execute(
445 "INSERT INTO uvtest VALUES (%s)",
446 [conflicting_value])
447
448 if connection.in_atomic_block:
449 # We're already in a transaction.
450 set_repeatable_read()
451 trigger_unique_violation()
452 else:
453 # Start a transaction in this thread.
454 with transaction.atomic():
455 set_repeatable_read()
456 trigger_unique_violation()
457
458 def capture_unique_violation(self):
459 """Trigger a unique violation, return its ``exc_info`` tuple."""
460 try:
461 self.cause_unique_violation()
462 except IntegrityError as e:
463 if is_unique_violation(e):
464 return sys.exc_info()
465 else:
466 raise
315467
=== modified file 'src/maasserver/utils/orm.py'
--- src/maasserver/utils/orm.py 2016-03-28 13:54:47 +0000
+++ src/maasserver/utils/orm.py 2016-06-17 14:52:22 +0000
@@ -16,8 +16,10 @@
16 'is_deadlock_failure',16 'is_deadlock_failure',
17 'is_retryable_failure',17 'is_retryable_failure',
18 'is_serialization_failure',18 'is_serialization_failure',
19 'is_unique_violation',
19 'make_deadlock_failure',20 'make_deadlock_failure',
20 'make_serialization_failure',21 'make_serialization_failure',
22 'make_unique_violation',
21 'post_commit',23 'post_commit',
22 'post_commit_do',24 'post_commit_do',
23 'psql_array',25 'psql_array',
@@ -54,7 +56,11 @@
54)56)
55from django.db.models import Q57from django.db.models import Q
56from django.db.transaction import TransactionManagementError58from django.db.transaction import TransactionManagementError
57from django.db.utils import OperationalError59from django.db.utils import (
60 DatabaseError,
61 IntegrityError,
62 OperationalError,
63)
58from django.http import Http40464from django.http import Http404
59from maasserver.exceptions import (65from maasserver.exceptions import (
60 MAASAPIBadRequest,66 MAASAPIBadRequest,
@@ -72,6 +78,7 @@
72from psycopg2.errorcodes import (78from psycopg2.errorcodes import (
73 DEADLOCK_DETECTED,79 DEADLOCK_DETECTED,
74 SERIALIZATION_FAILURE,80 SERIALIZATION_FAILURE,
81 UNIQUE_VIOLATION,
75)82)
76from twisted.internet.defer import Deferred83from twisted.internet.defer import Deferred
7784
@@ -273,6 +280,68 @@
273 return get_psycopg2_deadlock_exception(exception) is not None280 return get_psycopg2_deadlock_exception(exception) is not None
274281
275282
283def get_psycopg2_unique_violation_exception(exception):
284 """Return the root-cause if `exception` is a unique violation.
285
286 PostgreSQL sets a specific error code, "23505", when a transaction breaks
287 because of a unique violation.
288
289 :return: The underlying `psycopg2.Error` if it's a unique violation, or
290 `None` if there isn't one.
291 """
292 exception = get_psycopg2_exception(exception)
293 if exception is None:
294 return None
295 elif exception.pgcode == UNIQUE_VIOLATION:
296 return exception
297 else:
298 return None
299
300
301def is_unique_violation(exception):
302 """Does `exception` represent a unique violation?
303
304 PostgreSQL sets a specific error code, "23505", when a transaction breaks
305 because of a unique violation.
306 """
307 return get_psycopg2_unique_violation_exception(exception) is not None
308
309
310class UniqueViolation(psycopg2.IntegrityError):
311 """Explicit serialization failure.
312
313 A real unique violation, arising out of psycopg2 (and thus signalled from
314 the database) would *NOT* be an instance of this class. However, it is not
315 obvious how to create a `psycopg2.IntegrityError` with ``pgcode`` set to
316 `UNIQUE_VIOLATION` without subclassing. I suspect only the C interface can
317 do that.
318 """
319 pgcode = UNIQUE_VIOLATION
320
321
322def make_unique_violation():
323 """Make a serialization exception.
324
325 Artificially construct an exception that resembles what Django's ORM would
326 raise when PostgreSQL fails a transaction because of a unique violation.
327
328 :returns: an instance of :py:class:`IntegrityError` that will pass the
329 `is_unique_violation` predicate.
330 """
331 exception = IntegrityError()
332 exception.__cause__ = UniqueViolation()
333 assert is_unique_violation(exception)
334 return exception
335
336
337class RetryTransaction(BaseException):
338 """An explicit request that the transaction be retried."""
339
340
341class TooManyRetries(Exception):
342 """A transaction retry has been requested too many times."""
343
344
276def request_transaction_retry():345def request_transaction_retry():
277 """Raise a serialization exception.346 """Raise a serialization exception.
278347
@@ -280,15 +349,24 @@
280 this, and then retrying the transaction, though it may choose to re-raise349 this, and then retrying the transaction, though it may choose to re-raise
281 the error if too many retries have already been attempted.350 the error if too many retries have already been attempted.
282351
283 :raises OperationalError:352 :raise RetryTransaction:
284 """353 """
285 raise make_serialization_failure()354 raise RetryTransaction()
286355
287356
288def is_retryable_failure(exception):357def is_retryable_failure(exception):
289 """Does `exception` represent a serialization or deadlock failure?"""358 """Does `exception` represent a retryable failure?
359
360 This does NOT include requested retries, i.e. `RetryTransaction`.
361
362 :param exception: An instance of :class:`DatabaseError` or one of its
363 subclasses.
364 """
290 return (365 return (
291 is_serialization_failure(exception) or is_deadlock_failure(exception))366 is_serialization_failure(exception) or
367 is_deadlock_failure(exception) or
368 is_unique_violation(exception)
369 )
292370
293371
294def gen_retry_intervals(base=0.01, rate=2.5, maximum=10.0):372def gen_retry_intervals(base=0.01, rate=2.5, maximum=10.0):
@@ -341,14 +419,22 @@
341 for _ in range(9):419 for _ in range(9):
342 try:420 try:
343 return func(*args, **kwargs)421 return func(*args, **kwargs)
344 except OperationalError as error:422 except RetryTransaction:
423 reset() # Which may do nothing.
424 sleep(next(intervals))
425 except DatabaseError as error:
345 if is_retryable_failure(error):426 if is_retryable_failure(error):
346 reset() # Which may do nothing.427 reset() # Which may do nothing.
347 sleep(next(intervals))428 sleep(next(intervals))
348 else:429 else:
349 raise430 raise
350 else:431 else:
351 return func(*args, **kwargs)432 try:
433 return func(*args, **kwargs)
434 except RetryTransaction:
435 raise TooManyRetries(
436 "This transaction has already been attempted "
437 "multiple times; giving up.")
352 return retrier438 return retrier
353439
354440
355441
=== modified file 'src/maasserver/utils/tests/test_orm.py'
--- src/maasserver/utils/tests/test_orm.py 2016-05-12 19:07:37 +0000
+++ src/maasserver/utils/tests/test_orm.py 2016-06-17 14:52:22 +0000
@@ -26,12 +26,16 @@
26)26)
27from django.db.backends.base.base import BaseDatabaseWrapper27from django.db.backends.base.base import BaseDatabaseWrapper
28from django.db.transaction import TransactionManagementError28from django.db.transaction import TransactionManagementError
29from django.db.utils import OperationalError29from django.db.utils import (
30 IntegrityError,
31 OperationalError,
32)
30from maasserver.models import Node33from maasserver.models import Node
31from maasserver.testing.testcase import (34from maasserver.testing.testcase import (
32 MAASServerTestCase,35 MAASServerTestCase,
33 MAASTransactionServerTestCase,36 MAASTransactionServerTestCase,
34 SerializationFailureTestCase,37 SerializationFailureTestCase,
38 UniqueViolationTestCase,
35)39)
36from maasserver.utils import orm40from maasserver.utils import orm
37from maasserver.utils.orm import (41from maasserver.utils.orm import (
@@ -46,11 +50,12 @@
46 get_psycopg2_deadlock_exception,50 get_psycopg2_deadlock_exception,
47 get_psycopg2_exception,51 get_psycopg2_exception,
48 get_psycopg2_serialization_exception,52 get_psycopg2_serialization_exception,
53 get_psycopg2_unique_violation_exception,
49 in_transaction,54 in_transaction,
50 is_deadlock_failure,55 is_deadlock_failure,
51 is_retryable_failure,56 is_retryable_failure,
52 is_serialization_failure,57 is_serialization_failure,
53 make_serialization_failure,58 is_unique_violation,
54 post_commit,59 post_commit,
55 post_commit_do,60 post_commit_do,
56 post_commit_hooks,61 post_commit_hooks,
@@ -81,6 +86,7 @@
81from psycopg2.errorcodes import (86from psycopg2.errorcodes import (
82 DEADLOCK_DETECTED,87 DEADLOCK_DETECTED,
83 SERIALIZATION_FAILURE,88 SERIALIZATION_FAILURE,
89 UNIQUE_VIOLATION,
84)90)
85from testtools import ExpectedException91from testtools import ExpectedException
86from testtools.matchers import (92from testtools.matchers import (
@@ -221,6 +227,16 @@
221 SERIALIZATION_FAILURE, error.__cause__.pgcode)227 SERIALIZATION_FAILURE, error.__cause__.pgcode)
222228
223229
230class TestUniqueViolation(UniqueViolationTestCase):
231 """Detecting UNIQUE_VIOLATION failures."""
232
233 def test_unique_violation_detectable_via_error_cause(self):
234 error = self.assertRaises(
235 IntegrityError, self.cause_unique_violation)
236 self.assertEqual(
237 UNIQUE_VIOLATION, error.__cause__.pgcode)
238
239
224class TestGetPsycopg2Exception(MAASTestCase):240class TestGetPsycopg2Exception(MAASTestCase):
225 """Tests for `get_psycopg2_exception`."""241 """Tests for `get_psycopg2_exception`."""
226242
@@ -281,6 +297,25 @@
281 get_psycopg2_deadlock_exception(exception))297 get_psycopg2_deadlock_exception(exception))
282298
283299
300class TestGetPsycopg2UniqueViolationException(MAASTestCase):
301 """Tests for `get_psycopg2_unique_violation_exception`."""
302
303 def test__returns_None_for_plain_psycopg2_error(self):
304 exception = psycopg2.Error()
305 self.assertIsNone(get_psycopg2_unique_violation_exception(exception))
306
307 def test__returns_None_for_other_error(self):
308 exception = factory.make_exception()
309 self.assertIsNone(get_psycopg2_unique_violation_exception(exception))
310
311 def test__returns_psycopg2_error_root_cause(self):
312 exception = Exception()
313 exception.__cause__ = orm.UniqueViolation()
314 self.assertIs(
315 exception.__cause__,
316 get_psycopg2_unique_violation_exception(exception))
317
318
284class TestIsSerializationFailure(SerializationFailureTestCase):319class TestIsSerializationFailure(SerializationFailureTestCase):
285 """Tests relating to MAAS's use of SERIALIZABLE isolation."""320 """Tests relating to MAAS's use of SERIALIZABLE isolation."""
286321
@@ -340,6 +375,36 @@
340 self.assertFalse(is_deadlock_failure(error))375 self.assertFalse(is_deadlock_failure(error))
341376
342377
378class TestIsUniqueViolation(UniqueViolationTestCase):
379 """Tests relating to MAAS's identification of unique violations."""
380
381 def test_detects_integrity_error_with_matching_cause(self):
382 error = self.assertRaises(
383 IntegrityError, self.cause_unique_violation)
384 self.assertTrue(is_unique_violation(error))
385
386 def test_rejects_integrity_error_without_matching_cause(self):
387 error = IntegrityError()
388 cause = self.patch(error, "__cause__", Exception())
389 cause.pgcode = factory.make_name("pgcode")
390 self.assertFalse(is_unique_violation(error))
391
392 def test_rejects_integrity_error_with_unrelated_cause(self):
393 error = IntegrityError()
394 error.__cause__ = Exception()
395 self.assertFalse(is_unique_violation(error))
396
397 def test_rejects_integrity_error_without_cause(self):
398 error = IntegrityError()
399 self.assertFalse(is_unique_violation(error))
400
401 def test_rejects_non_integrity_error_with_matching_cause(self):
402 error = factory.make_exception()
403 cause = self.patch(error, "__cause__", Exception())
404 cause.pgcode = UNIQUE_VIOLATION
405 self.assertFalse(is_unique_violation(error))
406
407
343class TestIsRetryableFailure(MAASTestCase):408class TestIsRetryableFailure(MAASTestCase):
344 """Tests relating to MAAS's use of catching retryable failures."""409 """Tests relating to MAAS's use of catching retryable failures."""
345410
@@ -351,33 +416,58 @@
351 error = orm.make_deadlock_failure()416 error = orm.make_deadlock_failure()
352 self.assertTrue(is_retryable_failure(error))417 self.assertTrue(is_retryable_failure(error))
353418
419 def test_detects_unique_violation(self):
420 error = orm.make_unique_violation()
421 self.assertTrue(is_retryable_failure(error))
422
354 def test_rejects_operational_error_without_matching_cause(self):423 def test_rejects_operational_error_without_matching_cause(self):
355 error = OperationalError()424 error = OperationalError()
356 cause = self.patch(error, "__cause__", Exception())425 cause = self.patch(error, "__cause__", Exception())
357 cause.pgcode = factory.make_name("pgcode")426 cause.pgcode = factory.make_name("pgcode")
358 self.assertFalse(is_retryable_failure(error))427 self.assertFalse(is_retryable_failure(error))
359428
429 def test_rejects_integrity_error_without_matching_cause(self):
430 error = IntegrityError()
431 cause = self.patch(error, "__cause__", Exception())
432 cause.pgcode = factory.make_name("pgcode")
433 self.assertFalse(is_retryable_failure(error))
434
360 def test_rejects_operational_error_with_unrelated_cause(self):435 def test_rejects_operational_error_with_unrelated_cause(self):
361 error = OperationalError()436 error = OperationalError()
362 error.__cause__ = Exception()437 error.__cause__ = Exception()
363 self.assertFalse(is_retryable_failure(error))438 self.assertFalse(is_retryable_failure(error))
364439
440 def test_rejects_integrity_error_with_unrelated_cause(self):
441 error = IntegrityError()
442 error.__cause__ = Exception()
443 self.assertFalse(is_retryable_failure(error))
444
365 def test_rejects_operational_error_without_cause(self):445 def test_rejects_operational_error_without_cause(self):
366 error = OperationalError()446 error = OperationalError()
367 self.assertFalse(is_retryable_failure(error))447 self.assertFalse(is_retryable_failure(error))
368448
369 def test_rejects_non_operational_error_with_cause_serialization(self):449 def test_rejects_integrity_error_without_cause(self):
450 error = IntegrityError()
451 self.assertFalse(is_retryable_failure(error))
452
453 def test_rejects_non_database_error_with_cause_serialization(self):
370 error = factory.make_exception()454 error = factory.make_exception()
371 cause = self.patch(error, "__cause__", Exception())455 cause = self.patch(error, "__cause__", Exception())
372 cause.pgcode = SERIALIZATION_FAILURE456 cause.pgcode = SERIALIZATION_FAILURE
373 self.assertFalse(is_retryable_failure(error))457 self.assertFalse(is_retryable_failure(error))
374458
375 def test_rejects_non_operational_error_with_cause_deadlock(self):459 def test_rejects_non_database_error_with_cause_deadlock(self):
376 error = factory.make_exception()460 error = factory.make_exception()
377 cause = self.patch(error, "__cause__", Exception())461 cause = self.patch(error, "__cause__", Exception())
378 cause.pgcode = DEADLOCK_DETECTED462 cause.pgcode = DEADLOCK_DETECTED
379 self.assertFalse(is_retryable_failure(error))463 self.assertFalse(is_retryable_failure(error))
380464
465 def test_rejects_non_database_error_with_cause_unique_violation(self):
466 error = factory.make_exception()
467 cause = self.patch(error, "__cause__", Exception())
468 cause.pgcode = UNIQUE_VIOLATION
469 self.assertFalse(is_retryable_failure(error))
470
381471
382class TestRetryOnRetryableFailure(SerializationFailureTestCase):472class TestRetryOnRetryableFailure(SerializationFailureTestCase):
383473
@@ -418,6 +508,36 @@
418 self.assertEqual(sentinel.result, function_wrapped())508 self.assertEqual(sentinel.result, function_wrapped())
419 self.assertThat(function, MockCallsMatch(call(), call()))509 self.assertThat(function, MockCallsMatch(call(), call()))
420510
511 def test_retries_on_unique_violation(self):
512 function = self.make_mock_function()
513 function.side_effect = orm.make_unique_violation()
514 function_wrapped = retry_on_retryable_failure(function)
515 self.assertRaises(IntegrityError, function_wrapped)
516 expected_calls = [call()] * 10
517 self.assertThat(function, MockCallsMatch(*expected_calls))
518
519 def test_retries_on_unique_violation_until_successful(self):
520 function = self.make_mock_function()
521 function.side_effect = [orm.make_unique_violation(), sentinel.result]
522 function_wrapped = retry_on_retryable_failure(function)
523 self.assertEqual(sentinel.result, function_wrapped())
524 self.assertThat(function, MockCallsMatch(call(), call()))
525
526 def test_retries_on_retry_transaction(self):
527 function = self.make_mock_function()
528 function.side_effect = orm.RetryTransaction()
529 function_wrapped = retry_on_retryable_failure(function)
530 self.assertRaises(orm.TooManyRetries, function_wrapped)
531 expected_calls = [call()] * 10
532 self.assertThat(function, MockCallsMatch(*expected_calls))
533
534 def test_retries_on_retry_transaction_until_successful(self):
535 function = self.make_mock_function()
536 function.side_effect = [orm.RetryTransaction(), sentinel.result]
537 function_wrapped = retry_on_retryable_failure(function)
538 self.assertEqual(sentinel.result, function_wrapped())
539 self.assertThat(function, MockCallsMatch(call(), call()))
540
421 def test_passes_args_to_wrapped_function(self):541 def test_passes_args_to_wrapped_function(self):
422 function = lambda a, b: (a, b)542 function = lambda a, b: (a, b)
423 function_wrapped = retry_on_retryable_failure(function)543 function_wrapped = retry_on_retryable_failure(function)
@@ -450,19 +570,34 @@
450 """Tests for `make_serialization_failure`."""570 """Tests for `make_serialization_failure`."""
451571
452 def test__makes_a_serialization_failure(self):572 def test__makes_a_serialization_failure(self):
453 exception = make_serialization_failure()573 exception = orm.make_serialization_failure()
454 self.assertThat(exception, MatchesPredicate(574 self.assertThat(exception, MatchesPredicate(
455 is_serialization_failure, "%r is not a serialization failure."))575 is_serialization_failure, "%r is not a serialization failure."))
456576
457577
578class TestMakeDeadlockFailure(MAASTestCase):
579 """Tests for `make_deadlock_failure`."""
580
581 def test__makes_a_deadlock_failure(self):
582 exception = orm.make_deadlock_failure()
583 self.assertThat(exception, MatchesPredicate(
584 is_deadlock_failure, "%r is not a deadlock failure."))
585
586
587class TestMakeUniqueViolation(MAASTestCase):
588 """Tests for `make_unique_violation`."""
589
590 def test__makes_a_unique_violation(self):
591 exception = orm.make_unique_violation()
592 self.assertThat(exception, MatchesPredicate(
593 is_unique_violation, "%r is not a unique violation."))
594
595
458class TestRequestTransactionRetry(MAASTestCase):596class TestRequestTransactionRetry(MAASTestCase):
459 """Tests for `request_transaction_retry`."""597 """Tests for `request_transaction_retry`."""
460598
461 def test__raises_a_serialization_failure(self):599 def test__raises_a_retry_transaction_exception(self):
462 exception = self.assertRaises(600 self.assertRaises(orm.RetryTransaction, request_transaction_retry)
463 OperationalError, request_transaction_retry)
464 self.assertThat(exception, MatchesPredicate(
465 is_serialization_failure, "%r is not a serialization failure."))
466601
467602
468class TestGenRetryIntervals(MAASTestCase):603class TestGenRetryIntervals(MAASTestCase):