|
44 | 44 | from sqlalchemy import Integer
|
45 | 45 | from sqlalchemy import MetaData
|
46 | 46 | from sqlalchemy.orm import query
|
| 47 | +from sqlalchemy.orm import session as sqla_session |
47 | 48 | from sqlalchemy import sql
|
48 | 49 | from sqlalchemy import Table
|
49 | 50 |
|
@@ -2558,21 +2559,50 @@ def test(context):
|
2558 | 2559 | test(self.ctxt)
|
2559 | 2560 |
|
2560 | 2561 | def test_instance_update_and_get_original_conflict_race(self):
|
2561 |
| - # Ensure that we retry if update_on_match fails for no discernable |
2562 |
| - # reason |
2563 |
| - instance = self.create_instance_with_args() |
2564 |
| - |
2565 |
| - orig_update_on_match = update_match.update_on_match |
| 2562 | + # Ensure that we correctly process expected_task_state when retrying |
| 2563 | + # due to an unknown conflict |
| 2564 | + |
| 2565 | + # This requires modelling the MySQL read view, which means that if we |
| 2566 | + # have read something in the current transaction and we read it again, |
| 2567 | + # we will read the same data every time even if another committed |
| 2568 | + # transaction has since altered that data. In this test we have an |
| 2569 | + # instance whose task state was originally None, but has been set to |
| 2570 | + # SHELVING by another, concurrent transaction. Therefore the first time |
| 2571 | + # we read the data we will read None, but when we restart the |
| 2572 | + # transaction we will read the correct data. |
2566 | 2573 |
|
2567 |
| - # Reproduce the conditions of a race between fetching and updating the |
2568 |
| - # instance by making update_on_match fail for no discernable reason the |
2569 |
| - # first time it is called, but work normally the second time. |
2570 |
| - with mock.patch.object(update_match, 'update_on_match', |
2571 |
| - side_effect=[update_match.NoRowsMatched, |
2572 |
| - orig_update_on_match]): |
2573 |
| - db.instance_update_and_get_original( |
2574 |
| - self.ctxt, instance['uuid'], {'metadata': {'mk1': 'mv3'}}) |
2575 |
| - self.assertEqual(update_match.update_on_match.call_count, 2) |
| 2574 | + instance = self.create_instance_with_args( |
| 2575 | + task_state=task_states.SHELVING) |
| 2576 | + |
| 2577 | + instance_out_of_date = copy.copy(instance) |
| 2578 | + instance_out_of_date['task_state'] = None |
| 2579 | + |
| 2580 | + # NOTE(mdbooth): SQLA magic which makes this dirty object look |
| 2581 | + # like a freshly loaded one. |
| 2582 | + sqla_session.make_transient(instance_out_of_date) |
| 2583 | + sqla_session.make_transient_to_detached(instance_out_of_date) |
| 2584 | + |
| 2585 | + # update_on_match will fail first time because the actual task state |
| 2586 | + # (SHELVING) doesn't match the expected task state (None). However, |
| 2587 | + # we ensure that the first time we fetch the instance object we get |
| 2588 | + # out-of-date data. This forces us to retry the operation to find out |
| 2589 | + # what really went wrong. |
| 2590 | + with mock.patch.object(sqlalchemy_api, '_instance_get_by_uuid', |
| 2591 | + side_effect=[instance_out_of_date, instance]), \ |
| 2592 | + mock.patch.object(sqlalchemy_api, '_instance_update', |
| 2593 | + side_effect=sqlalchemy_api._instance_update): |
| 2594 | + self.assertRaises(exception.UnexpectedTaskStateError, |
| 2595 | + db.instance_update_and_get_original, |
| 2596 | + self.ctxt, instance['uuid'], |
| 2597 | + {'expected_task_state': [None]}) |
| 2598 | + sqlalchemy_api._instance_update.assert_has_calls([ |
| 2599 | + mock.call(self.ctxt, instance['uuid'], |
| 2600 | + {'expected_task_state': [None]}, None, |
| 2601 | + original=instance_out_of_date), |
| 2602 | + mock.call(self.ctxt, instance['uuid'], |
| 2603 | + {'expected_task_state': [None]}, None, |
| 2604 | + original=instance), |
| 2605 | + ]) |
2576 | 2606 |
|
2577 | 2607 | def test_instance_update_and_get_original_conflict_race_fallthrough(self):
|
2578 | 2608 | # Ensure that is update_match continuously fails for no discernable
|
|
0 commit comments