Skip to content

Commit ab9fcc2

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 ab9fcc2

File tree

2 files changed

+60
-5
lines changed

2 files changed

+60
-5
lines changed

nova/compute/api.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5145,12 +5145,24 @@ 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.debug("BDM for volume %s has attachment_id set, "
5159+
"not deleting to avoid race-condition",
5160+
volume['id'])
5161+
else:
5162+
bdm.destroy()
5163+
LOG.debug("Delete BDM after compute did not respond "
5164+
"to attachment request for volume %s",
5165+
volume['id'])
51545166
except exception.VolumeBDMNotFound:
51555167
LOG.debug("BDM not found, ignoring removal. "
51565168
f"Error attaching volume {volume['id']}")

nova/tests/unit/compute/test_api.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,48 @@ 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
542+
mock_get_by_volume_and_instance.return_value = fake_bdm
543+
instance = self._create_instance_obj()
544+
volume = fake_volume.fake_volume(1, 'test-vol', 'test-vol',
545+
None, None, None, None, None)
546+
547+
mock_reserve.side_effect = oslo_exceptions.MessagingTimeout()
548+
549+
mock_volume_api = mock.patch.object(self.compute_api, 'volume_api',
550+
mock.MagicMock(spec=cinder.API))
551+
552+
with mock_volume_api as mock_v_api:
553+
mock_v_api.get.return_value = volume
554+
self.assertRaises(oslo_exceptions.MessagingTimeout,
555+
self.compute_api.attach_volume,
556+
self.context, instance, volume['id'])
557+
mock_get_by_volume_and_instance.assert_called_once_with(
558+
self.context, volume['id'], instance.uuid)
559+
fake_bdm.destroy.assert_called_once_with()
560+
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'
540582
mock_get_by_volume_and_instance.return_value = fake_bdm
541583
instance = self._create_instance_obj()
542584
volume = fake_volume.fake_volume(1, 'test-vol', 'test-vol',
@@ -554,7 +596,8 @@ def test_attach_volume_reserve_bdm_timeout(
554596
self.context, instance, volume['id'])
555597
mock_get_by_volume_and_instance.assert_called_once_with(
556598
self.context, volume['id'], instance.uuid)
557-
fake_bdm.destroy.assert_called_once_with()
599+
# destroy() should NOT be called when attachment_id is set
600+
fake_bdm.destroy.assert_not_called()
558601

559602
@mock.patch.object(compute_rpcapi.ComputeAPI, 'reserve_block_device_name')
560603
@mock.patch.object(objects.BlockDeviceMappingList, 'get_by_volume')

0 commit comments

Comments
 (0)