Merge lp:~sleepsonthefloor/nova/709057 into lp:~hudson-openstack/nova/trunk

Proposed by Anthony Young
Status: Merged
Approved by: Vish Ishaya
Approved revision: 641
Merged at revision: 644
Proposed branch: lp:~sleepsonthefloor/nova/709057
Merge into: lp:~hudson-openstack/nova/trunk
Diff against target: 150 lines (+68/-13)
3 files modified
nova/db/sqlalchemy/api.py (+14/-2)
nova/db/sqlalchemy/models.py (+5/-1)
nova/tests/test_compute.py (+49/-10)
To merge this branch: bzr merge lp:~sleepsonthefloor/nova/709057
Reviewer Review Type Date Requested Status
Soren Hansen (community) Approve
Thierry Carrez (community) gfe Approve
Devin Carlen (community) Approve
Rick Harris (community) Approve
Review via email: mp+47845@code.launchpad.net

Description of the change

Fixes bug #709057

* Mark security_group_instance_associations as deleted when instance deleted
* Mark security_group_instance_associations as deleted when security_group deleted
* Make SecurityGroup.instances mapping properly ignore deleted security_group_instance_associations
* Add tests

IMO, this is important to merge. Nebula has monitoring projects that continually launch and destroy instances, and I imagine other real-world deployments may have similar health checkers. After a few weeks such health checkers choke due to the inefficient queries related to #709057.

To post a comment you must log in.
Revision history for this message
Vish Ishaya (vishvananda) wrote :

I think this is a good candidate for an exception. It is a pretty annoying performance error for large deployments, and it is limited to the db layer.

Revision history for this message
Devin Carlen (devcamcar) wrote :

5 with session.begin():
6 instance_ref = instance_get(context, instance_id, session=session)
7 instance_ref.delete(session=session)
8 + session.execute('update security_group_instance_association'
9 + ' set deleted=1 where instance_id=:id',
10 + {'id': instance_id})

Would prefer to not see mixing of the sqlalchemy orm and raw sql code in a single operation. Eventually we should replace the raw sql with the sqlalchemy query language (which is essentially in between the orm and raw sql, a rather happy medium).

I propose making lines 6 and 7 match this style:

16 session.execute('update security_groups set deleted=1 where id=:id',
17 {'id': security_group_id})

This also has the advantage of not eating an extra query looking up the instance_ref, which instance_ref.delete currently does.

review: Needs Fixing
Revision history for this message
Thierry Carrez (ttx) wrote :

I agree this sounds like a good candidate. I'll wait for the final diff before accepting it though.

Revision history for this message
Rick Harris (rconradharris) wrote :

> + self.assert_(len(group.instances) == 0)

Micro-nit: This is usually better written with self.assertEqual; not only is it more specific, it allows the unittest library to better pretty-print an assertion error since it has access to both the LHS and RHS of the expression.

Overall this patch lgtm, though this problem feels like it would be more elegantly solved using SQLAlchemy's cascading delete feature [1].

[1] http://www.sqlalchemy.org/docs/05/ormtutorial.html - search for cascade

review: Approve
Revision history for this message
Devin Carlen (devcamcar) wrote :

We can't use casading deletes because in this case we don't want to actually destroy the rows. We are using paranoid deletions so everything needs to manually set the deleted flag.

lgtm

review: Approve
Revision history for this message
Thierry Carrez (ttx) wrote :

Exception granted if it can get another core review asap.

review: Approve (gfe)
Revision history for this message
Soren Hansen (soren) wrote :

lgtm

review: Approve
Revision history for this message
OpenStack Infra (hudson-openstack) wrote :
Download full text (21.4 KiB)

The attempt to merge lp:~sleepsonthefloor/nova/709057 into lp:nova failed. Below is the output from the failed tests.

AdminAPITest
    test_admin_disabled ok
    test_admin_enabled ok
APITest
    test_exceptions_are_converted_to_faults ok
Test
    test_authorize_token ok
    test_authorize_user ok
    test_bad_token ok
    test_bad_user ok
    test_no_user ok
    test_token_expiry ok
TestLimiter
    test_authorize_token ok
TestFaults
    test_fault_parts ok
    test_raise ok
    test_retry_header ok
FlavorsTest
    test_get_flavor_by_id ok
    test_get_flavor_list ok
GlanceImageServiceTest
    test_create ok
    test_create_and_show_non_existing_image ok
    test_delete ok
    test_update ok
ImageControllerWithGlanceServiceTest
    test_get_image_details ok
    test_get_image_index ok
LocalImageServiceTest
    test_create ok
    test_create_and_show_non_existing_image ok
    test_delete ok
    test_update ok
LimiterTest
    test_minute ok
    test_one_per_period ok
    test_second ok
    test_users_get_separate_buckets ok
    test_we_can_go_indefinitely_if_we_spread_out_requests ok
WSGIAppProxyTest
    test_200 ok
    test_403 ok
    test_failure ok
WSGIAppTest
    test_escaping ok
    test_good_urls ok
    test_invalid_methods ok
    test_invalid_urls ok
    test_response_to_delays ok
ServersTest
    test_create_backup_schedules ok
    test_create_instance ok
    test_delete_backup_schedules ok
    test_delete_server_instance ok
    test_get_all_server_details ok
    ...

lp:~sleepsonthefloor/nova/709057 updated
640. By Anthony Young

fix pep8 error :/

641. By Anthony Young

merge trunk

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'nova/db/sqlalchemy/api.py'
2--- nova/db/sqlalchemy/api.py 2011-01-27 18:48:00 +0000
3+++ nova/db/sqlalchemy/api.py 2011-02-01 17:51:07 +0000
4@@ -19,6 +19,7 @@
5 Implementation of SQLAlchemy backend.
6 """
7
8+import datetime
9 import warnings
10
11 from nova import db
12@@ -670,8 +671,14 @@
13 def instance_destroy(context, instance_id):
14 session = get_session()
15 with session.begin():
16- instance_ref = instance_get(context, instance_id, session=session)
17- instance_ref.delete(session=session)
18+ session.execute('update instances set deleted=1,'
19+ 'deleted_at=:at where id=:id',
20+ {'id': instance_id,
21+ 'at': datetime.datetime.utcnow()})
22+ session.execute('update security_group_instance_association '
23+ 'set deleted=1,deleted_at=:at where instance_id=:id',
24+ {'id': instance_id,
25+ 'at': datetime.datetime.utcnow()})
26
27
28 @require_context
29@@ -1583,6 +1590,11 @@
30 # TODO(vish): do we have to use sql here?
31 session.execute('update security_groups set deleted=1 where id=:id',
32 {'id': security_group_id})
33+ session.execute('update security_group_instance_association '
34+ 'set deleted=1,deleted_at=:at '
35+ 'where security_group_id=:id',
36+ {'id': security_group_id,
37+ 'at': datetime.datetime.utcnow()})
38 session.execute('update security_group_rules set deleted=1 '
39 'where group_id=:id',
40 {'id': security_group_id})
41
42=== modified file 'nova/db/sqlalchemy/models.py'
43--- nova/db/sqlalchemy/models.py 2011-01-18 23:51:13 +0000
44+++ nova/db/sqlalchemy/models.py 2011-02-01 17:51:07 +0000
45@@ -311,10 +311,14 @@
46 secondary="security_group_instance_association",
47 primaryjoin='and_('
48 'SecurityGroup.id == '
49- 'SecurityGroupInstanceAssociation.security_group_id,'
50+ 'SecurityGroupInstanceAssociation.security_group_id,'
51+ 'SecurityGroupInstanceAssociation.deleted == False,'
52 'SecurityGroup.deleted == False)',
53 secondaryjoin='and_('
54 'SecurityGroupInstanceAssociation.instance_id == Instance.id,'
55+ # (anthony) the condition below shouldn't be necessary now that the
56+ # association is being marked as deleted. However, removing this
57+ # may cause existing deployments to choke, so I'm leaving it
58 'Instance.deleted == False)',
59 backref='security_groups')
60
61
62=== modified file 'nova/tests/test_compute.py'
63--- nova/tests/test_compute.py 2011-01-17 18:05:26 +0000
64+++ nova/tests/test_compute.py 2011-02-01 17:51:07 +0000
65@@ -49,7 +49,7 @@
66 self.manager = manager.AuthManager()
67 self.user = self.manager.create_user('fake', 'fake', 'fake')
68 self.project = self.manager.create_project('fake', 'fake', 'fake')
69- self.context = context.get_admin_context()
70+ self.context = context.RequestContext('fake', 'fake', False)
71
72 def tearDown(self):
73 self.manager.delete_user(self.user)
74@@ -69,6 +69,13 @@
75 inst['ami_launch_index'] = 0
76 return db.instance_create(self.context, inst)['id']
77
78+ def _create_group(self):
79+ values = {'name': 'testgroup',
80+ 'description': 'testgroup',
81+ 'user_id': self.user.id,
82+ 'project_id': self.project.id}
83+ return db.security_group_create(self.context, values)
84+
85 def test_create_instance_defaults_display_name(self):
86 """Verify that an instance cannot be created without a display_name."""
87 cases = [dict(), dict(display_name=None)]
88@@ -82,21 +89,53 @@
89
90 def test_create_instance_associates_security_groups(self):
91 """Make sure create associates security groups"""
92- values = {'name': 'default',
93- 'description': 'default',
94- 'user_id': self.user.id,
95- 'project_id': self.project.id}
96- group = db.security_group_create(self.context, values)
97+ group = self._create_group()
98 ref = self.compute_api.create(
99 self.context,
100 instance_type=FLAGS.default_instance_type,
101 image_id=None,
102- security_group=['default'])
103+ security_group=['testgroup'])
104 try:
105 self.assertEqual(len(db.security_group_get_by_instance(
106- self.context, ref[0]['id'])), 1)
107- finally:
108- db.security_group_destroy(self.context, group['id'])
109+ self.context, ref[0]['id'])), 1)
110+ group = db.security_group_get(self.context, group['id'])
111+ self.assert_(len(group.instances) == 1)
112+ finally:
113+ db.security_group_destroy(self.context, group['id'])
114+ db.instance_destroy(self.context, ref[0]['id'])
115+
116+ def test_destroy_instance_disassociates_security_groups(self):
117+ """Make sure destroying disassociates security groups"""
118+ group = self._create_group()
119+
120+ ref = self.compute_api.create(
121+ self.context,
122+ instance_type=FLAGS.default_instance_type,
123+ image_id=None,
124+ security_group=['testgroup'])
125+ try:
126+ db.instance_destroy(self.context, ref[0]['id'])
127+ group = db.security_group_get(self.context, group['id'])
128+ self.assert_(len(group.instances) == 0)
129+ finally:
130+ db.security_group_destroy(self.context, group['id'])
131+
132+ def test_destroy_security_group_disassociates_instances(self):
133+ """Make sure destroying security groups disassociates instances"""
134+ group = self._create_group()
135+
136+ ref = self.compute_api.create(
137+ self.context,
138+ instance_type=FLAGS.default_instance_type,
139+ image_id=None,
140+ security_group=['testgroup'])
141+
142+ try:
143+ db.security_group_destroy(self.context, group['id'])
144+ group = db.security_group_get(context.get_admin_context(
145+ read_deleted=True), group['id'])
146+ self.assert_(len(group.instances) == 0)
147+ finally:
148 db.instance_destroy(self.context, ref[0]['id'])
149
150 def test_run_terminate(self):