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
=== modified file 'nova/db/sqlalchemy/api.py'
--- nova/db/sqlalchemy/api.py 2011-01-27 18:48:00 +0000
+++ nova/db/sqlalchemy/api.py 2011-02-01 17:51:07 +0000
@@ -19,6 +19,7 @@
19Implementation of SQLAlchemy backend.19Implementation of SQLAlchemy backend.
20"""20"""
2121
22import datetime
22import warnings23import warnings
2324
24from nova import db25from nova import db
@@ -670,8 +671,14 @@
670def instance_destroy(context, instance_id):671def instance_destroy(context, instance_id):
671 session = get_session()672 session = get_session()
672 with session.begin():673 with session.begin():
673 instance_ref = instance_get(context, instance_id, session=session)674 session.execute('update instances set deleted=1,'
674 instance_ref.delete(session=session)675 'deleted_at=:at where id=:id',
676 {'id': instance_id,
677 'at': datetime.datetime.utcnow()})
678 session.execute('update security_group_instance_association '
679 'set deleted=1,deleted_at=:at where instance_id=:id',
680 {'id': instance_id,
681 'at': datetime.datetime.utcnow()})
675682
676683
677@require_context684@require_context
@@ -1583,6 +1590,11 @@
1583 # TODO(vish): do we have to use sql here?1590 # TODO(vish): do we have to use sql here?
1584 session.execute('update security_groups set deleted=1 where id=:id',1591 session.execute('update security_groups set deleted=1 where id=:id',
1585 {'id': security_group_id})1592 {'id': security_group_id})
1593 session.execute('update security_group_instance_association '
1594 'set deleted=1,deleted_at=:at '
1595 'where security_group_id=:id',
1596 {'id': security_group_id,
1597 'at': datetime.datetime.utcnow()})
1586 session.execute('update security_group_rules set deleted=1 '1598 session.execute('update security_group_rules set deleted=1 '
1587 'where group_id=:id',1599 'where group_id=:id',
1588 {'id': security_group_id})1600 {'id': security_group_id})
15891601
=== modified file 'nova/db/sqlalchemy/models.py'
--- nova/db/sqlalchemy/models.py 2011-01-18 23:51:13 +0000
+++ nova/db/sqlalchemy/models.py 2011-02-01 17:51:07 +0000
@@ -311,10 +311,14 @@
311 secondary="security_group_instance_association",311 secondary="security_group_instance_association",
312 primaryjoin='and_('312 primaryjoin='and_('
313 'SecurityGroup.id == '313 'SecurityGroup.id == '
314 'SecurityGroupInstanceAssociation.security_group_id,'314 'SecurityGroupInstanceAssociation.security_group_id,'
315 'SecurityGroupInstanceAssociation.deleted == False,'
315 'SecurityGroup.deleted == False)',316 'SecurityGroup.deleted == False)',
316 secondaryjoin='and_('317 secondaryjoin='and_('
317 'SecurityGroupInstanceAssociation.instance_id == Instance.id,'318 'SecurityGroupInstanceAssociation.instance_id == Instance.id,'
319 # (anthony) the condition below shouldn't be necessary now that the
320 # association is being marked as deleted. However, removing this
321 # may cause existing deployments to choke, so I'm leaving it
318 'Instance.deleted == False)',322 'Instance.deleted == False)',
319 backref='security_groups')323 backref='security_groups')
320324
321325
=== modified file 'nova/tests/test_compute.py'
--- nova/tests/test_compute.py 2011-01-17 18:05:26 +0000
+++ nova/tests/test_compute.py 2011-02-01 17:51:07 +0000
@@ -49,7 +49,7 @@
49 self.manager = manager.AuthManager()49 self.manager = manager.AuthManager()
50 self.user = self.manager.create_user('fake', 'fake', 'fake')50 self.user = self.manager.create_user('fake', 'fake', 'fake')
51 self.project = self.manager.create_project('fake', 'fake', 'fake')51 self.project = self.manager.create_project('fake', 'fake', 'fake')
52 self.context = context.get_admin_context()52 self.context = context.RequestContext('fake', 'fake', False)
5353
54 def tearDown(self):54 def tearDown(self):
55 self.manager.delete_user(self.user)55 self.manager.delete_user(self.user)
@@ -69,6 +69,13 @@
69 inst['ami_launch_index'] = 069 inst['ami_launch_index'] = 0
70 return db.instance_create(self.context, inst)['id']70 return db.instance_create(self.context, inst)['id']
7171
72 def _create_group(self):
73 values = {'name': 'testgroup',
74 'description': 'testgroup',
75 'user_id': self.user.id,
76 'project_id': self.project.id}
77 return db.security_group_create(self.context, values)
78
72 def test_create_instance_defaults_display_name(self):79 def test_create_instance_defaults_display_name(self):
73 """Verify that an instance cannot be created without a display_name."""80 """Verify that an instance cannot be created without a display_name."""
74 cases = [dict(), dict(display_name=None)]81 cases = [dict(), dict(display_name=None)]
@@ -82,21 +89,53 @@
8289
83 def test_create_instance_associates_security_groups(self):90 def test_create_instance_associates_security_groups(self):
84 """Make sure create associates security groups"""91 """Make sure create associates security groups"""
85 values = {'name': 'default',92 group = self._create_group()
86 'description': 'default',
87 'user_id': self.user.id,
88 'project_id': self.project.id}
89 group = db.security_group_create(self.context, values)
90 ref = self.compute_api.create(93 ref = self.compute_api.create(
91 self.context,94 self.context,
92 instance_type=FLAGS.default_instance_type,95 instance_type=FLAGS.default_instance_type,
93 image_id=None,96 image_id=None,
94 security_group=['default'])97 security_group=['testgroup'])
95 try:98 try:
96 self.assertEqual(len(db.security_group_get_by_instance(99 self.assertEqual(len(db.security_group_get_by_instance(
97 self.context, ref[0]['id'])), 1)100 self.context, ref[0]['id'])), 1)
98 finally:101 group = db.security_group_get(self.context, group['id'])
99 db.security_group_destroy(self.context, group['id'])102 self.assert_(len(group.instances) == 1)
103 finally:
104 db.security_group_destroy(self.context, group['id'])
105 db.instance_destroy(self.context, ref[0]['id'])
106
107 def test_destroy_instance_disassociates_security_groups(self):
108 """Make sure destroying disassociates security groups"""
109 group = self._create_group()
110
111 ref = self.compute_api.create(
112 self.context,
113 instance_type=FLAGS.default_instance_type,
114 image_id=None,
115 security_group=['testgroup'])
116 try:
117 db.instance_destroy(self.context, ref[0]['id'])
118 group = db.security_group_get(self.context, group['id'])
119 self.assert_(len(group.instances) == 0)
120 finally:
121 db.security_group_destroy(self.context, group['id'])
122
123 def test_destroy_security_group_disassociates_instances(self):
124 """Make sure destroying security groups disassociates instances"""
125 group = self._create_group()
126
127 ref = self.compute_api.create(
128 self.context,
129 instance_type=FLAGS.default_instance_type,
130 image_id=None,
131 security_group=['testgroup'])
132
133 try:
134 db.security_group_destroy(self.context, group['id'])
135 group = db.security_group_get(context.get_admin_context(
136 read_deleted=True), group['id'])
137 self.assert_(len(group.instances) == 0)
138 finally:
100 db.instance_destroy(self.context, ref[0]['id'])139 db.instance_destroy(self.context, ref[0]['id'])
101140
102 def test_run_terminate(self):141 def test_run_terminate(self):