Skip to content

Commit b57ee44

Browse files
committed
api: Avoid race-condition in volume-attach timeout handling
When nova-api calls reserve_block_device_name RPC to nova-compute and the call times out, we try to clean up by deleting the BDM entry. However, during the timeout window a second attachment request for the same volume can come in, create a valid BDM, and progress to talking to Cinder. The original timed-out request then deletes this new valid BDM, leaving the volume in an inconsistent state. We fix this by checking if the BDM has attachment_id set before deleting it. The attachment_id field is only populated in _check_attach_and_reserve_volume(), which we only call after the reserve_block_device_name RPC succeeds. If attachment_id is set, we know the BDM belongs to a subsequent request that has already progressed past the RPC phase, so we should not delete it. Change-Id: I7ed649a5cab7f254690f329fac285128d8cd1c92
1 parent ba48137 commit b57ee44

File tree

2 files changed

+60
-4
lines changed

2 files changed

+60
-4
lines changed

nova/compute/api.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5145,12 +5145,25 @@ def _attach_volume(self, context, instance, volume, device,
51455145
# The compute node might have already created the attachment but
51465146
# we never received the answer. In this case it is safe to delete
51475147
# the attachment as nobody will ever pick it up again.
5148+
# To avoid a race-condition where a second volume-attach request
5149+
# comes in for the same volume while we wait, we verify that the
5150+
# BDM we delete has no attachment_id set. attachment_id only
5151+
# gets set later in _check_attach_and_reserve_volume().
51485152
with excutils.save_and_reraise_exception():
51495153
try:
5150-
objects.BlockDeviceMapping.get_by_volume_and_instance(
5151-
context, volume['id'], instance.uuid).destroy()
5152-
LOG.debug("Delete BDM after compute did not respond to "
5153-
f"attachment request for volume {volume['id']}")
5154+
bdm = \
5155+
objects.BlockDeviceMapping.get_by_volume_and_instance(
5156+
context, volume['id'], instance.uuid)
5157+
if bdm.attachment_id:
5158+
LOG.warning(
5159+
"BDM for volume %s has attachment_id set, "
5160+
"not deleting to avoid race-condition",
5161+
volume['id'])
5162+
else:
5163+
bdm.destroy()
5164+
LOG.debug("Delete BDM after compute did not respond "
5165+
"to attachment request for volume %s",
5166+
volume['id'])
51545167
except exception.VolumeBDMNotFound:
51555168
LOG.debug("BDM not found, ignoring removal. "
51565169
f"Error attaching volume {volume['id']}")

nova/tests/unit/compute/test_api.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,8 @@ def test_attach_volume_reserve_bdm_timeout(
537537
volume_id='fake-volume-id')
538538

539539
fake_bdm = mock.MagicMock(spec=objects.BlockDeviceMapping)
540+
# BDM has no attachment_id yet, so it's safe to delete
541+
fake_bdm.attachment_id = None
540542
mock_get_by_volume_and_instance.return_value = fake_bdm
541543
instance = self._create_instance_obj()
542544
volume = fake_volume.fake_volume(1, 'test-vol', 'test-vol',
@@ -556,6 +558,47 @@ def test_attach_volume_reserve_bdm_timeout(
556558
self.context, volume['id'], instance.uuid)
557559
fake_bdm.destroy.assert_called_once_with()
558560

561+
@mock.patch.object(compute_rpcapi.ComputeAPI, 'reserve_block_device_name')
562+
@mock.patch.object(
563+
objects.BlockDeviceMapping, 'get_by_volume_and_instance')
564+
@mock.patch.object(objects.BlockDeviceMappingList, 'get_by_volume')
565+
def test_attach_volume_reserve_bdm_timeout_with_attachment_id(
566+
self, mock_get_by_volume, mock_get_by_volume_and_instance,
567+
mock_reserve):
568+
"""Test that BDM with attachment_id is not deleted on timeout.
569+
570+
This tests the race condition fix: if a second attachment request
571+
comes in while we're waiting for the RPC timeout, the BDM might
572+
already have attachment_id set. In this case, we should NOT
573+
delete the BDM as it belongs to the second request.
574+
"""
575+
mock_get_by_volume.side_effect = exception.VolumeBDMNotFound(
576+
volume_id='fake-volume-id')
577+
578+
fake_bdm = mock.MagicMock(spec=objects.BlockDeviceMapping)
579+
# BDM has attachment_id set, indicating a second request has
580+
# already progressed past the reserve_block_device_name RPC
581+
fake_bdm.attachment_id = 'fake-attachment-id'
582+
mock_get_by_volume_and_instance.return_value = fake_bdm
583+
instance = self._create_instance_obj()
584+
volume = fake_volume.fake_volume(1, 'test-vol', 'test-vol',
585+
None, None, None, None, None)
586+
587+
mock_reserve.side_effect = oslo_exceptions.MessagingTimeout()
588+
589+
mock_volume_api = mock.patch.object(self.compute_api, 'volume_api',
590+
mock.MagicMock(spec=cinder.API))
591+
592+
with mock_volume_api as mock_v_api:
593+
mock_v_api.get.return_value = volume
594+
self.assertRaises(oslo_exceptions.MessagingTimeout,
595+
self.compute_api.attach_volume,
596+
self.context, instance, volume['id'])
597+
mock_get_by_volume_and_instance.assert_called_once_with(
598+
self.context, volume['id'], instance.uuid)
599+
# destroy() should NOT be called when attachment_id is set
600+
fake_bdm.destroy.assert_not_called()
601+
559602
@mock.patch.object(compute_rpcapi.ComputeAPI, 'reserve_block_device_name')
560603
@mock.patch.object(objects.BlockDeviceMappingList, 'get_by_volume')
561604
@mock.patch.object(compute_rpcapi.ComputeAPI, 'attach_volume')

0 commit comments

Comments
 (0)