Merge lp:~allenap/maas/fix-image-imports 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: 4295
Proposed branch: lp:~allenap/maas/fix-image-imports
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 268 lines (+82/-35)
8 files modified
src/maasserver/bootresources.py (+9/-4)
src/maasserver/testing/dblocks.py (+45/-0)
src/maasserver/tests/test_bootresources.py (+3/-3)
src/maasserver/utils/orm.py (+2/-1)
src/maasserver/utils/tests/test_dblocks.py (+1/-25)
src/maasserver/utils/tests/test_orm.py (+19/-0)
src/maasserver/utils/tests/test_threads.py (+2/-0)
src/maasserver/utils/threads.py (+1/-2)
To merge this branch: bzr merge lp:~allenap/maas/fix-image-imports
Reviewer Review Type Date Requested Status
Andres Rodriguez (community) Approve
Mike Pontillo (community) Approve
Review via email: mp+272079@code.launchpad.net

Commit message

Ensure that _import_resources() is never called within a transaction.

Previously this requirement was not asserted, tested, or documented so it was only a matter of time before a regression was introduced.

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

This alone is the fix:

     return deferToThreadPool(
         reactor, reactor.threadpoolForDatabase,
-        transactional(func), *args, **kwargs)
+        func, *args, **kwargs)

The rest is tests, docs, and preventing _import_resources() from causing
a regression again.

Revision history for this message
Mike Pontillo (mpontillo) wrote :

Looks good.

review: Approve
Revision history for this message
Andres Rodriguez (andreserl) wrote :

lgtm!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/bootresources.py'
2--- src/maasserver/bootresources.py 2015-09-22 12:21:54 +0000
3+++ src/maasserver/bootresources.py 2015-09-23 10:38:06 +0000
4@@ -71,6 +71,7 @@
5 )
6 from maasserver.utils.orm import (
7 get_one,
8+ in_transaction,
9 transactional,
10 )
11 from maasserver.utils.threads import deferToDatabase
12@@ -920,10 +921,8 @@
13 kill_event.wait()
14
15
16-# FIXME: How this function deals with transactions is not clear. It uses the
17-# ORM without ensuring there's a transaction. It also does network IO and we
18-# don't want to be holding open a transaction for an indefinite time while
19-# that happens.
20+# FIXME: This does network IO while holding two database connections and two
21+# transactions. This is bad for concurrency.
22 @synchronous
23 def _import_resources(force=False):
24 """Import boot resources.
25@@ -931,10 +930,16 @@
26 Pulls the sources from `BootSource`. This only starts the process if
27 some SYNCED `BootResource` already exist.
28
29+ This MUST be called from outside of a database transaction.
30+
31 :param force: True will force the import, even if no SYNCED `BootResource`
32 exist. This is used because we want the user to start the first import
33 action, not let it run automatically.
34 """
35+ assert not in_transaction(), (
36+ "_import_resources() must not be called within a preexisting "
37+ "transaction; it manages its own.")
38+
39 # If the lock is already held, then import is already running.
40 if locks.import_images.is_locked():
41 maaslog.debug("Skipping import as another import is already running.")
42
43=== added file 'src/maasserver/testing/dblocks.py'
44--- src/maasserver/testing/dblocks.py 1970-01-01 00:00:00 +0000
45+++ src/maasserver/testing/dblocks.py 2015-09-23 10:38:06 +0000
46@@ -0,0 +1,45 @@
47+# Copyright 2015 Canonical Ltd. This software is licensed under the
48+# GNU Affero General Public License version 3 (see the file LICENSE).
49+
50+"""Helpers for testing database locks and related."""
51+
52+from __future__ import (
53+ absolute_import,
54+ print_function,
55+ unicode_literals,
56+ )
57+
58+str = None
59+
60+__metaclass__ = type
61+__all__ = [
62+ "lock_held_in_other_thread",
63+]
64+
65+from contextlib import contextmanager
66+import threading
67+
68+from maasserver.utils.orm import transactional
69+
70+
71+@contextmanager
72+def lock_held_in_other_thread(lock, timeout=10):
73+ """Hold `lock` in another thread."""
74+ held = threading.Event()
75+ done = threading.Event()
76+
77+ @transactional
78+ def hold():
79+ with lock:
80+ held.set()
81+ done.wait(timeout)
82+
83+ thread = threading.Thread(target=hold)
84+ thread.start()
85+
86+ held.wait(timeout)
87+ try:
88+ yield
89+ finally:
90+ done.set()
91+ thread.join()
92
93=== modified file 'src/maasserver/tests/test_bootresources.py'
94--- src/maasserver/tests/test_bootresources.py 2015-09-15 11:29:37 +0000
95+++ src/maasserver/tests/test_bootresources.py 2015-09-23 10:38:06 +0000
96@@ -61,6 +61,7 @@
97 )
98 from maasserver.rpc.testing.fixtures import MockLiveRegionToClusterRPCFixture
99 from maasserver.testing.config import RegionConfigurationFixture
100+from maasserver.testing.dblocks import lock_held_in_other_thread
101 from maasserver.testing.eventloop import (
102 RegionEventLoopFixture,
103 RunningEventLoopFixture,
104@@ -1144,9 +1145,8 @@
105 def test__import_resources_exits_early_if_lock_held(self):
106 has_synced_resources = self.patch_autospec(
107 bootresources, "has_synced_resources")
108- with transaction.atomic():
109- with bootresources.locks.import_images:
110- bootresources._import_resources(force=True)
111+ with lock_held_in_other_thread(bootresources.locks.import_images):
112+ bootresources._import_resources(force=True)
113 # The test for already-synced resources is not called if the
114 # lock is already held.
115 self.assertThat(has_synced_resources, MockNotCalled())
116
117=== modified file 'src/maasserver/utils/orm.py'
118--- src/maasserver/utils/orm.py 2015-09-11 19:21:06 +0000
119+++ src/maasserver/utils/orm.py 2015-09-23 10:38:06 +0000
120@@ -22,6 +22,7 @@
121 'get_exception_class',
122 'get_first',
123 'get_one',
124+ 'in_transaction',
125 'is_serialization_failure',
126 'macs_contain',
127 'macs_do_not_contain',
128@@ -552,7 +553,7 @@
129 depth = depth - 1
130
131
132-def in_transaction(connection):
133+def in_transaction(connection=connection):
134 """Is `connection` in the midst of a transaction?
135
136 This only enquires as to Django's perspective on the situation. It does
137
138=== modified file 'src/maasserver/utils/tests/test_dblocks.py'
139--- src/maasserver/utils/tests/test_dblocks.py 2015-09-15 17:27:21 +0000
140+++ src/maasserver/utils/tests/test_dblocks.py 2015-09-23 10:38:06 +0000
141@@ -20,18 +20,17 @@
142 )
143 from random import randint
144 import sys
145-import threading
146
147 from django.db import (
148 connection,
149 transaction,
150 )
151+from maasserver.testing.dblocks import lock_held_in_other_thread
152 from maasserver.testing.testcase import (
153 MAASServerTestCase,
154 MAASTransactionServerTestCase,
155 )
156 from maasserver.utils import dblocks
157-from maasserver.utils.orm import transactional
158 from testtools.matchers import Equals
159
160
161@@ -79,29 +78,6 @@
162 query["sql"] for query in connection.queries)
163
164
165-@contextmanager
166-def lock_held_in_other_thread(lock, timeout=10):
167- """Hold `lock` in another thread."""
168- held = threading.Event()
169- done = threading.Event()
170-
171- @transactional
172- def hold():
173- with lock:
174- held.set()
175- done.wait(timeout)
176-
177- thread = threading.Thread(target=hold)
178- thread.start()
179-
180- held.wait(timeout)
181- try:
182- yield
183- finally:
184- done.set()
185- thread.join()
186-
187-
188 class TestDatabaseLock(MAASTransactionServerTestCase):
189
190 scenarios = tuple(
191
192=== modified file 'src/maasserver/utils/tests/test_orm.py'
193--- src/maasserver/utils/tests/test_orm.py 2015-09-11 21:30:32 +0000
194+++ src/maasserver/utils/tests/test_orm.py 2015-09-23 10:38:06 +0000
195@@ -47,6 +47,7 @@
196 get_one,
197 get_psycopg2_exception,
198 get_psycopg2_serialization_exception,
199+ in_transaction,
200 is_serialization_failure,
201 macs_contain,
202 macs_do_not_contain,
203@@ -834,6 +835,24 @@
204 context_manager.__exit__, MockCalledOnceWith(None, None, None))
205
206
207+class TestInTransaction(DjangoTransactionTestCase):
208+ """Tests for `in_transaction`."""
209+
210+ def test__true_within_atomic_block(self):
211+ with transaction.atomic():
212+ self.assertTrue(in_transaction())
213+
214+ def test__true_when_legacy_transaction_is_active(self):
215+ transaction.enter_transaction_management()
216+ try:
217+ self.assertTrue(in_transaction())
218+ finally:
219+ transaction.leave_transaction_management()
220+
221+ def test__false_when_no_transaction_is_active(self):
222+ self.assertFalse(in_transaction())
223+
224+
225 class TestValidateInTransaction(DjangoTransactionTestCase):
226 """Tests for `validate_in_transaction`."""
227
228
229=== modified file 'src/maasserver/utils/tests/test_threads.py'
230--- src/maasserver/utils/tests/test_threads.py 2015-09-11 19:21:51 +0000
231+++ src/maasserver/utils/tests/test_threads.py 2015-09-23 10:38:06 +0000
232@@ -116,6 +116,7 @@
233 @inlineCallbacks
234 def test__defers_to_database_threadpool(self):
235
236+ @orm.transactional
237 def call_in_database_thread(a, b):
238 orm.validate_in_transaction(connection)
239 return sentinel.called, a, b
240@@ -132,6 +133,7 @@
241 @inlineCallbacks
242 def test__calls_out_to_database_threadpool(self):
243
244+ @orm.transactional
245 def call_in_database_thread(a, b):
246 orm.validate_in_transaction(connection)
247 return sentinel.bar, a, b
248
249=== modified file 'src/maasserver/utils/threads.py'
250--- src/maasserver/utils/threads.py 2015-09-11 19:21:51 +0000
251+++ src/maasserver/utils/threads.py 2015-09-23 10:38:06 +0000
252@@ -32,7 +32,6 @@
253 ExclusivelyConnected,
254 FullyConnected,
255 TotallyDisconnected,
256- transactional,
257 )
258 from provisioningserver.utils.twisted import (
259 asynchronous,
260@@ -147,7 +146,7 @@
261 """Call `func` in a thread where database activity is permitted."""
262 return deferToThreadPool(
263 reactor, reactor.threadpoolForDatabase,
264- transactional(func), *args, **kwargs)
265+ func, *args, **kwargs)
266
267
268 def callOutToDatabase(thing, func, *args, **kwargs):