Merge lp:~allenap/maas/database-locks-revisited--1.5 into lp:maas/1.5

Proposed by Gavin Panella
Status: Merged
Approved by: Julian Edwards
Approved revision: no longer in the source branch.
Merged at revision: 2285
Proposed branch: lp:~allenap/maas/database-locks-revisited--1.5
Merge into: lp:maas/1.5
Diff against target: 166 lines (+52/-8)
4 files modified
src/maasserver/rpc/regionservice.py (+1/-1)
src/maasserver/rpc/tests/test_regionservice.py (+2/-4)
src/maasserver/utils/dblocks.py (+31/-2)
src/maasserver/utils/tests/test_dblocks.py (+18/-1)
To merge this branch: bzr merge lp:~allenap/maas/database-locks-revisited--1.5
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+221783@code.launchpad.net

Commit message

Backport of trunk r2395: Tighten up function and use of DatabaseLock.

Previously Django's aggressive connection closing policy would prevent session-level locks from being maintained.

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/rpc/regionservice.py'
2--- src/maasserver/rpc/regionservice.py 2014-04-01 17:26:20 +0000
3+++ src/maasserver/rpc/regionservice.py 2014-06-02 20:22:06 +0000
4@@ -325,8 +325,8 @@
5
6 @synchronous
7 @synchronised(lock)
8+ @transactional
9 @synchronised(locks.eventloop)
10- @transactional
11 def prepare(self):
12 """Ensure that the ``eventloops`` table exists.
13
14
15=== modified file 'src/maasserver/rpc/tests/test_regionservice.py'
16--- src/maasserver/rpc/tests/test_regionservice.py 2014-04-15 21:04:34 +0000
17+++ src/maasserver/rpc/tests/test_regionservice.py 2014-06-02 20:22:06 +0000
18@@ -49,6 +49,7 @@
19 common,
20 exceptions,
21 )
22+from provisioningserver.rpc.interfaces import IConnection
23 from provisioningserver.rpc.region import (
24 Identify,
25 ReportBootImages,
26@@ -82,6 +83,7 @@
27 from twisted.internet.threads import deferToThread
28 from twisted.protocols import amp
29 from twisted.python import log
30+from zope.interface.verify import verifyObject
31
32
33 class TestRegionProtocol_Identify(MAASTestCase):
34@@ -195,10 +197,6 @@
35 return d.addCallback(check)
36
37
38-from provisioningserver.rpc.interfaces import IConnection
39-from zope.interface.verify import verifyObject
40-
41-
42 class TestRegionServer(MAASServerTestCase):
43
44 def test_interfaces(self):
45
46=== modified file 'src/maasserver/utils/dblocks.py'
47--- src/maasserver/utils/dblocks.py 2014-04-01 17:23:19 +0000
48+++ src/maasserver/utils/dblocks.py 2014-06-02 20:22:06 +0000
49@@ -14,6 +14,8 @@
50 __metaclass__ = type
51 __all__ = [
52 "DatabaseLock",
53+ "DatabaseLockAttemptOutsideTransaction",
54+ "DatabaseLockNotHeld",
55 ]
56
57 from contextlib import closing
58@@ -26,6 +28,19 @@
59 classid = 20120116
60
61
62+class DatabaseLockAttemptOutsideTransaction(Exception):
63+ """A locking attempt was made outside of a transaction.
64+
65+ :class:`DatabaseLock` should only be used within a transaction.
66+ Django agressively closes connections outside of atomic blocks to
67+ the extent that session-level locks are rendered unreliable at best.
68+ """
69+
70+
71+class DatabaseLockNotHeld(Exception):
72+ """A particular lock was not held."""
73+
74+
75 class DatabaseLock(tuple):
76 """An advisory lock held in the database.
77
78@@ -58,12 +73,16 @@
79 return super(cls, DatabaseLock).__new__(cls, (classid, objid))
80
81 def __enter__(self):
82+ if not connection.in_atomic_block:
83+ raise DatabaseLockAttemptOutsideTransaction(self)
84 with closing(connection.cursor()) as cursor:
85 cursor.execute("SELECT pg_advisory_lock(%s, %s)", self)
86
87 def __exit__(self, *exc_info):
88 with closing(connection.cursor()) as cursor:
89 cursor.execute("SELECT pg_advisory_unlock(%s, %s)", self)
90+ if cursor.fetchone() != (True,):
91+ raise DatabaseLockNotHeld(self)
92
93 def __repr__(self):
94 return b"<%s classid=%d objid=%d>" % (
95@@ -71,8 +90,18 @@
96
97 def is_locked(self):
98 stmt = (
99- "SELECT 1 FROM pg_locks"
100- " WHERE classid = %s AND objid = %s AND granted"
101+ "SELECT 1 FROM pg_locks, pg_database"
102+ " WHERE pg_locks.locktype = 'advisory'"
103+ " AND pg_locks.classid = %s"
104+ " AND pg_locks.objid = %s"
105+ # objsubid is 2 when using the 2-argument version of the
106+ # pg_advisory_* locking functions.
107+ " AND pg_locks.objsubid = 2"
108+ " AND pg_locks.granted"
109+ # Advisory locks are local to each database so we join to
110+ # pg_databases to discover the OID of the currrent database.
111+ " AND pg_locks.database = pg_database.oid"
112+ " AND pg_database.datname = current_database()"
113 )
114 with closing(connection.cursor()) as cursor:
115 cursor.execute(stmt, self)
116
117=== modified file 'src/maasserver/utils/tests/test_dblocks.py'
118--- src/maasserver/utils/tests/test_dblocks.py 2014-03-24 10:43:54 +0000
119+++ src/maasserver/utils/tests/test_dblocks.py 2014-06-02 20:22:06 +0000
120@@ -16,7 +16,10 @@
121
122 from contextlib import closing
123
124-from django.db import connection
125+from django.db import (
126+ connection,
127+ transaction,
128+ )
129 from maasserver.utils import dblocks
130 from maastesting.testcase import MAASTestCase
131
132@@ -40,6 +43,7 @@
133 lock = dblocks.DatabaseLock(self.getUniqueInteger())
134 self.assertEqual(lock, (lock.classid, lock.objid))
135
136+ @transaction.atomic
137 def test_lock_actually_locked(self):
138 objid = self.getUniqueInteger()
139 lock = dblocks.DatabaseLock(objid)
140@@ -55,6 +59,7 @@
141 locks_released = locks_held - locks_held_after
142 self.assertEqual({objid}, locks_released)
143
144+ @transaction.atomic
145 def test_is_locked(self):
146 objid = self.getUniqueInteger()
147 lock = dblocks.DatabaseLock(objid)
148@@ -64,6 +69,18 @@
149 self.assertTrue(lock.is_locked())
150 self.assertFalse(lock.is_locked())
151
152+ def test_obtaining_lock_fails_when_outside_of_transaction(self):
153+ objid = self.getUniqueInteger()
154+ lock = dblocks.DatabaseLock(objid)
155+ self.assertRaises(
156+ dblocks.DatabaseLockAttemptOutsideTransaction,
157+ lock.__enter__)
158+
159+ def test_releasing_lock_fails_when_lock_not_held(self):
160+ objid = self.getUniqueInteger()
161+ lock = dblocks.DatabaseLock(objid)
162+ self.assertRaises(dblocks.DatabaseLockNotHeld, lock.__exit__)
163+
164 def test_repr(self):
165 lock = dblocks.DatabaseLock(self.getUniqueInteger())
166 self.assertEqual(

Subscribers

People subscribed via source and target branches

to all changes: