Merge lp:~allenap/maas/rpc-create-table-race into lp:~maas-committers/maas/trunk

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: 2217
Proposed branch: lp:~allenap/maas/rpc-create-table-race
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 109 lines (+45/-3)
4 files modified
src/maasserver/locks.py (+4/-0)
src/maasserver/rpc/regionservice.py (+5/-1)
src/maasserver/rpc/tests/test_regionservice.py (+32/-1)
src/maasserver/utils/dblocks.py (+4/-1)
To merge this branch: bzr merge lp:~allenap/maas/rpc-create-table-race
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) Approve
Review via email: mp+213539@code.launchpad.net

Commit message

Take an advisory lock to prevent concurrent creation of the eventloops table.

Creating tables in PostgreSQL is a transactional operation like any
other. If the isolation level is not sufficient - the default in Django
- it is susceptible to races. Using a higher isolation level may lead to
serialisation failures, for example. Advisory locking side-steps this.

To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Actually postgres does guard against races when creating new tables. The operation is transactional, much like a normal data manipulation statement. I suspect the problem happens at a higher level — possibly because of the lack of a transaction.

Revision history for this message
Gavin Panella (allenap) wrote :

> Actually postgres does guard against races when creating new tables. The
> operation is transactional, much like a normal data manipulation statement. I
> suspect the problem happens at a higher level — possibly because of the lack
> of a transaction.

I've changed the comment. Can you see if that's okay?

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Thank you. Very happy to see this resolved in the proper way.

One small note about the test: I'd prefer a patch for _do_create that merely recorded the result of is_locked(), followed by regular straight-line test code at the end to assert that the value was True. A sensible default (e.g. None) will also protect the test from possible future code changes that might cut _do_create out of the call chain.

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (17.3 KiB)

The attempt to merge lp:~allenap/maas/rpc-create-table-race into lp:maas failed. Below is the output from the failed tests.

Ign http://security.ubuntu.com trusty-security InRelease
Hit http://security.ubuntu.com trusty-security Release.gpg
Ign http://nova.clouds.archive.ubuntu.com trusty InRelease
Hit http://security.ubuntu.com trusty-security Release
Ign http://nova.clouds.archive.ubuntu.com trusty-updates InRelease
Get:1 http://nova.clouds.archive.ubuntu.com trusty Release.gpg [933 B]
Hit http://nova.clouds.archive.ubuntu.com trusty-updates Release.gpg
Get:2 http://nova.clouds.archive.ubuntu.com trusty Release [58.5 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty-updates Release
Hit http://security.ubuntu.com trusty-security/main Sources
Hit http://security.ubuntu.com trusty-security/universe Sources
Hit http://security.ubuntu.com trusty-security/main amd64 Packages
Hit http://security.ubuntu.com trusty-security/universe amd64 Packages
Hit http://security.ubuntu.com trusty-security/main Translation-en
Hit http://security.ubuntu.com trusty-security/universe Translation-en
Get:3 http://nova.clouds.archive.ubuntu.com trusty/main Sources [1,075 kB]
Ign http://security.ubuntu.com trusty-security/main Translation-en_US
Ign http://security.ubuntu.com trusty-security/universe Translation-en_US
Get:4 http://nova.clouds.archive.ubuntu.com trusty/universe Sources [6,404 kB]
Get:5 http://nova.clouds.archive.ubuntu.com trusty/main amd64 Packages [1,367 kB]
Get:6 http://nova.clouds.archive.ubuntu.com trusty/universe amd64 Packages [5,863 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Sources
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Sources
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Translation-en
Ign http://nova.clouds.archive.ubuntu.com trusty/main Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty-updates/universe Translation-en_US
Fetched 14.8 MB in 5s (2,613 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
     --no-install-recommends install apache2 bind9 bind9utils build-essential bzr-builddeb curl daemontools debhelper dh-apport distro-info dnsutils firefox freeipmi-tools ipython isc-dhcp-common libjs-raphael libjs-yui3-full libjs-yui3-min libpq-dev make postgresql python-amqplib python-bzrlib python-celery python-convoy python-crochet python-cssselect python-curtin python-dev python-distro-info python-django python-django-piston python-django-south python-djorm-ext-pgarray python-docutils python-formencode python-httplib2 python-jinja2 python-jsonschema python-lockfile python-lxml python-netaddr python-net...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/locks.py'
2--- src/maasserver/locks.py 2014-03-25 21:48:21 +0000
3+++ src/maasserver/locks.py 2014-04-01 17:26:35 +0000
4@@ -13,6 +13,7 @@
5
6 __metaclass__ = type
7 __all__ = [
8+ "eventloop",
9 "security",
10 "startup",
11 ]
12@@ -25,3 +26,6 @@
13 # Lock around performing critical security-related operations, like
14 # generating or signing certificates.
15 security = DatabaseLock(2)
16+
17+# Lock used when starting up the event-loop.
18+eventloop = DatabaseLock(3)
19
20=== modified file 'src/maasserver/rpc/regionservice.py'
21--- src/maasserver/rpc/regionservice.py 2014-03-20 22:44:06 +0000
22+++ src/maasserver/rpc/regionservice.py 2014-04-01 17:26:35 +0000
23@@ -30,7 +30,10 @@
24 connection,
25 transaction,
26 )
27-from maasserver import eventloop
28+from maasserver import (
29+ eventloop,
30+ locks,
31+ )
32 from maasserver.utils import synchronised
33 from provisioningserver.rpc import (
34 cluster,
35@@ -322,6 +325,7 @@
36
37 @synchronous
38 @synchronised(lock)
39+ @synchronised(locks.eventloop)
40 @transactional
41 def prepare(self):
42 """Ensure that the ``eventloops`` table exists.
43
44=== modified file 'src/maasserver/rpc/tests/test_regionservice.py'
45--- src/maasserver/rpc/tests/test_regionservice.py 2014-03-29 17:30:11 +0000
46+++ src/maasserver/rpc/tests/test_regionservice.py 2014-04-01 17:26:35 +0000
47@@ -25,7 +25,10 @@
48 from crochet import wait_for_reactor
49 from django.db import connection
50 from django.db.utils import ProgrammingError
51-from maasserver import eventloop
52+from maasserver import (
53+ eventloop,
54+ locks,
55+ )
56 from maasserver.rpc import regionservice
57 from maasserver.rpc.regionservice import (
58 Region,
59@@ -638,6 +641,34 @@
60 cursor.execute("SELECT * FROM eventloops")
61 self.assertEqual([], list(cursor))
62
63+ def test_prepare_holds_startup_lock(self):
64+ # Creating tables in PostgreSQL is a transactional operation
65+ # like any other. If the isolation level is not sufficient - the
66+ # default in Django - it is susceptible to races. Using a higher
67+ # isolation level may lead to serialisation failures, for
68+ # example. However, PostgreSQL provides advisory locking
69+ # functions, and that's what RegionAdvertisingService.prepare
70+ # takes advantage of to prevent concurrent creation of the
71+ # eventloops table.
72+
73+ # A record of the lock's status, populated when a custom
74+ # patched-in _do_create() is called.
75+ locked = []
76+
77+ def _do_create(cursor):
78+ locked.append(locks.eventloop.is_locked())
79+
80+ service = RegionAdvertisingService()
81+ service._do_create = _do_create
82+
83+ # The lock is not held before and after prepare() is called.
84+ self.assertFalse(locks.eventloop.is_locked())
85+ service.prepare()
86+ self.assertFalse(locks.eventloop.is_locked())
87+
88+ # The lock was held when _do_create() was called.
89+ self.assertEqual([True], locked)
90+
91 def test_update(self):
92 example_addresses = [
93 (factory.getRandomIPAddress(), factory.getRandomPort()),
94
95=== modified file 'src/maasserver/utils/dblocks.py'
96--- src/maasserver/utils/dblocks.py 2014-03-25 19:18:19 +0000
97+++ src/maasserver/utils/dblocks.py 2014-04-01 17:26:35 +0000
98@@ -70,7 +70,10 @@
99 self.__class__.__name__, self[0], self[1])
100
101 def is_locked(self):
102- stmt = "SELECT 1 FROM pg_locks WHERE classid = %s AND objid = %s"
103+ stmt = (
104+ "SELECT 1 FROM pg_locks"
105+ " WHERE classid = %s AND objid = %s AND granted"
106+ )
107 with closing(connection.cursor()) as cursor:
108 cursor.execute(stmt, self)
109 return len(cursor.fetchall()) >= 1