Skip to content

Commit 4f17ea2

Browse files
committed
Add regression test for bug 1914777
This adds two tests to cover a regression where racing create and delete requests could result in a user receiving a 500 error when attempting to delete an instance: Unexpected exception in API method: AttributeError: 'NoneType' object has no attribute 'uuid' Related-Bug: #1914777 Change-Id: I8249c572c6f727ef4ca434843106b9b57c47e585 (cherry picked from commit f7975d6)
1 parent a2beea0 commit 4f17ea2

File tree

1 file changed

+151
-0
lines changed

1 file changed

+151
-0
lines changed
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
2+
# not use this file except in compliance with the License. You may obtain
3+
# a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
# License for the specific language governing permissions and limitations
11+
# under the License.
12+
13+
import mock
14+
15+
from nova import context as nova_context
16+
from nova import exception
17+
from nova import objects
18+
from nova import test
19+
from nova.tests import fixtures as nova_fixtures
20+
from nova.tests.functional.api import client
21+
from nova.tests.functional import integrated_helpers
22+
from nova.tests.unit import policy_fixture
23+
24+
25+
class TestDeleteWhileBooting(test.TestCase,
26+
integrated_helpers.InstanceHelperMixin):
27+
"""This tests race scenarios where an instance is deleted while booting.
28+
29+
In these scenarios, the nova-api service is racing with nova-conductor
30+
service; nova-conductor is in the middle of booting the instance when
31+
nova-api begins fulfillment of a delete request. As the two services
32+
delete records out from under each other, both services need to handle
33+
it properly such that a delete request will always be fulfilled.
34+
35+
Another scenario where two requests can race and delete things out from
36+
under each other is if two or more delete requests are racing while the
37+
instance is booting.
38+
39+
In order to force things into states where bugs have occurred, we must
40+
mock some object retrievals from the database to simulate the different
41+
points at which a delete request races with a create request or another
42+
delete request. We aim to mock only the bare minimum necessary to recreate
43+
the bug scenarios.
44+
"""
45+
def setUp(self):
46+
super(TestDeleteWhileBooting, self).setUp()
47+
self.useFixture(policy_fixture.RealPolicyFixture())
48+
self.useFixture(nova_fixtures.NeutronFixture(self))
49+
self.useFixture(nova_fixtures.GlanceFixture(self))
50+
51+
api_fixture = self.useFixture(nova_fixtures.OSAPIFixture(
52+
api_version='v2.1'))
53+
self.api = api_fixture.api
54+
55+
self.ctxt = nova_context.get_context()
56+
57+
# We intentionally do not start a conductor or scheduler service, since
58+
# our goal is to simulate an instance that has not been scheduled yet.
59+
60+
# Kick off a server create request and move on once it's in the BUILD
61+
# state. Since we have no conductor or scheduler service running, the
62+
# server will "hang" in an unscheduled state for testing.
63+
self.server = self._create_server(expected_state='BUILD')
64+
# Simulate that a different request has deleted the build request
65+
# record after this delete request has begun processing. (The first
66+
# lookup of the build request occurs in the servers API to get the
67+
# instance object in order to delete it).
68+
# We need to get the build request now before we mock the method.
69+
self.br = objects.BuildRequest.get_by_instance_uuid(
70+
self.ctxt, self.server['id'])
71+
72+
@mock.patch('nova.objects.build_request.BuildRequest.get_by_instance_uuid')
73+
def test_build_request_and_instance_not_found(self, mock_get_br):
74+
"""This tests a scenario where another request has deleted the build
75+
request record and the instance record ahead of us.
76+
"""
77+
# The first lookup at the beginning of the delete request in the
78+
# ServersController succeeds and the second lookup to handle "delete
79+
# while booting" in compute/api fails after a different request has
80+
# deleted it.
81+
br_not_found = exception.BuildRequestNotFound(uuid=self.server['id'])
82+
mock_get_br.side_effect = [self.br, br_not_found]
83+
# FIXME(melwitt): Delete request fails due to the AttributeError.
84+
ex = self.assertRaises(
85+
client.OpenStackApiException, self._delete_server, self.server)
86+
self.assertEqual(500, ex.response.status_code)
87+
self.assertIn('AttributeError', str(ex))
88+
# FIXME(melwitt): Uncomment when the bug is fixed.
89+
# self._delete_server(self.server)
90+
91+
@mock.patch('nova.objects.build_request.BuildRequest.get_by_instance_uuid')
92+
@mock.patch('nova.objects.InstanceMapping.get_by_instance_uuid')
93+
@mock.patch('nova.objects.instance.Instance.get_by_uuid')
94+
def test_deleting_instance_at_the_same_time(self, mock_get_i, mock_get_im,
95+
mock_get_br):
96+
"""This tests the scenario where another request is trying to delete
97+
the instance record at the same time we are, while the instance is
98+
booting. An example of this: while the create and delete are running at
99+
the same time, the delete request deletes the build request, the create
100+
request finds the build request already deleted when it tries to delete
101+
it. The create request deletes the instance record and then delete
102+
request tries to lookup the instance after it deletes the build
103+
request. Its attempt to lookup the instance fails because the create
104+
request already deleted it.
105+
"""
106+
# First lookup at the beginning of the delete request in the
107+
# ServersController succeeds, second lookup to handle "delete while
108+
# booting" in compute/api fails after the conductor has deleted it.
109+
br_not_found = exception.BuildRequestNotFound(uuid=self.server['id'])
110+
mock_get_br.side_effect = [self.br, br_not_found]
111+
# Simulate the instance transitioning from having no cell assigned to
112+
# having a cell assigned while the delete request is being processed.
113+
# First lookup of the instance mapping has the instance unmapped (no
114+
# cell) and subsequent lookups have the instance mapped to cell1.
115+
no_cell_im = objects.InstanceMapping(
116+
context=self.ctxt, instance_uuid=self.server['id'],
117+
cell_mapping=None)
118+
has_cell_im = objects.InstanceMapping(
119+
context=self.ctxt, instance_uuid=self.server['id'],
120+
cell_mapping=self.cell_mappings['cell1'])
121+
mock_get_im.side_effect = [
122+
no_cell_im, has_cell_im, has_cell_im, has_cell_im]
123+
# Simulate that the instance object has been created by the conductor
124+
# in the create path while the delete request is being processed.
125+
# First lookups are before the instance has been deleted and the last
126+
# lookup is after the conductor has deleted the instance. Use the build
127+
# request to make an instance object for testing.
128+
i = self.br.get_new_instance(self.ctxt)
129+
i_not_found = exception.InstanceNotFound(instance_id=self.server['id'])
130+
mock_get_i.side_effect = [i, i, i, i_not_found]
131+
132+
# Simulate that the conductor is running instance_destroy at the same
133+
# time as we are.
134+
def fake_instance_destroy(*args, **kwargs):
135+
# NOTE(melwitt): This is a misleading exception, as it is not only
136+
# raised when a constraint on 'host' is not met, but also when two
137+
# instance_destroy calls are racing. In this test, the soft delete
138+
# returns 0 rows affected because another request soft deleted the
139+
# record first.
140+
raise exception.ObjectActionError(
141+
action='destroy', reason='host changed')
142+
143+
self.stub_out(
144+
'nova.objects.instance.Instance.destroy', fake_instance_destroy)
145+
# FIXME(melwitt): Delete request fails due to the AttributeError.
146+
ex = self.assertRaises(
147+
client.OpenStackApiException, self._delete_server, self.server)
148+
self.assertEqual(500, ex.response.status_code)
149+
self.assertIn('AttributeError', str(ex))
150+
# FIXME(melwitt): Uncomment when the bug is fixed.
151+
# self._delete_server(self.server)

0 commit comments

Comments
 (0)