1 |
commit: 916a0733c7201b7a8b22f5262bd5be8cbc8992a6 |
2 |
Author: Zac Medico <zmedico <AT> gentoo <DOT> org> |
3 |
AuthorDate: Sun Apr 2 20:50:09 2017 +0000 |
4 |
Commit: Zac Medico <zmedico <AT> gentoo <DOT> org> |
5 |
CommitDate: Mon Apr 3 20:07:59 2017 +0000 |
6 |
URL: https://gitweb.gentoo.org/proj/portage.git/commit/?id=916a0733 |
7 |
|
8 |
AsynchronousLock: add async_unlock method (bug 614108) |
9 |
|
10 |
Add an async_unlock method, in order to avoid event loop |
11 |
recursion which is incompatible with asyncio. |
12 |
|
13 |
X-Gentoo-bug: 614108 |
14 |
X-Gentoo-bug-url: https://bugs.gentoo.org/show_bug.cgi?id=614108 |
15 |
Acked-by: Brian Dolbec <dolsen <AT> gentoo.org> |
16 |
|
17 |
pym/_emerge/AsynchronousLock.py | 89 +++++++++++++++++++++-- |
18 |
pym/portage/tests/locks/test_asynchronous_lock.py | 15 +++- |
19 |
2 files changed, 92 insertions(+), 12 deletions(-) |
20 |
|
21 |
diff --git a/pym/_emerge/AsynchronousLock.py b/pym/_emerge/AsynchronousLock.py |
22 |
index c0b9b26dc..6a32d2d40 100644 |
23 |
--- a/pym/_emerge/AsynchronousLock.py |
24 |
+++ b/pym/_emerge/AsynchronousLock.py |
25 |
@@ -35,7 +35,7 @@ class AsynchronousLock(AsynchronousTask): |
26 |
|
27 |
__slots__ = ('path', 'scheduler',) + \ |
28 |
('_imp', '_force_async', '_force_dummy', '_force_process', \ |
29 |
- '_force_thread') |
30 |
+ '_force_thread', '_unlock_future') |
31 |
|
32 |
_use_process_by_default = True |
33 |
|
34 |
@@ -84,6 +84,11 @@ class AsynchronousLock(AsynchronousTask): |
35 |
return self.returncode |
36 |
|
37 |
def unlock(self): |
38 |
+ """ |
39 |
+ This method is deprecated in favor of async_unlock, since waiting |
40 |
+ for the child process to respond can trigger event loop recursion |
41 |
+ which is incompatible with asyncio. |
42 |
+ """ |
43 |
if self._imp is None: |
44 |
raise AssertionError('not locked') |
45 |
if isinstance(self._imp, (_LockProcess, _LockThread)): |
46 |
@@ -92,6 +97,28 @@ class AsynchronousLock(AsynchronousTask): |
47 |
unlockfile(self._imp) |
48 |
self._imp = None |
49 |
|
50 |
+ def async_unlock(self): |
51 |
+ """ |
52 |
+ Release the lock asynchronously. Release notification is available |
53 |
+ via the add_done_callback method of the returned Future instance. |
54 |
+ |
55 |
+ @returns: Future, result is None |
56 |
+ """ |
57 |
+ if self._imp is None: |
58 |
+ raise AssertionError('not locked') |
59 |
+ if self._unlock_future is not None: |
60 |
+ raise AssertionError("already unlocked") |
61 |
+ if isinstance(self._imp, (_LockProcess, _LockThread)): |
62 |
+ unlock_future = self._imp.async_unlock() |
63 |
+ else: |
64 |
+ unlockfile(self._imp) |
65 |
+ unlock_future = self.scheduler.create_future() |
66 |
+ self.scheduler.call_soon(unlock_future.set_result, None) |
67 |
+ self._imp = None |
68 |
+ self._unlock_future = unlock_future |
69 |
+ return unlock_future |
70 |
+ |
71 |
+ |
72 |
class _LockThread(AbstractPollTask): |
73 |
""" |
74 |
This uses the portage.locks module to acquire a lock asynchronously, |
75 |
@@ -105,7 +132,7 @@ class _LockThread(AbstractPollTask): |
76 |
""" |
77 |
|
78 |
__slots__ = ('path',) + \ |
79 |
- ('_force_dummy', '_lock_obj', '_thread',) |
80 |
+ ('_force_dummy', '_lock_obj', '_thread', '_unlock_future') |
81 |
|
82 |
def _start(self): |
83 |
self._registered = True |
84 |
@@ -132,13 +159,35 @@ class _LockThread(AbstractPollTask): |
85 |
pass |
86 |
|
87 |
def unlock(self): |
88 |
+ """ |
89 |
+ This method is deprecated in favor of async_unlock, for compatibility |
90 |
+ with _LockProcess. |
91 |
+ """ |
92 |
+ self._unlock() |
93 |
+ self._unlock_future.set_result(None) |
94 |
+ |
95 |
+ def _unlock(self): |
96 |
if self._lock_obj is None: |
97 |
raise AssertionError('not locked') |
98 |
if self.returncode is None: |
99 |
raise AssertionError('lock not acquired yet') |
100 |
+ if self._unlock_future is not None: |
101 |
+ raise AssertionError("already unlocked") |
102 |
+ self._unlock_future = self.scheduler.create_future() |
103 |
unlockfile(self._lock_obj) |
104 |
self._lock_obj = None |
105 |
|
106 |
+ def async_unlock(self): |
107 |
+ """ |
108 |
+ Release the lock asynchronously. Release notification is available |
109 |
+ via the add_done_callback method of the returned Future instance. |
110 |
+ |
111 |
+ @returns: Future, result is None |
112 |
+ """ |
113 |
+ self._unlock() |
114 |
+ self.scheduler.call_soon(self._unlock_future.set_result, None) |
115 |
+ return self._unlock_future |
116 |
+ |
117 |
def _unregister(self): |
118 |
self._registered = False |
119 |
|
120 |
@@ -156,7 +205,8 @@ class _LockProcess(AbstractPollTask): |
121 |
""" |
122 |
|
123 |
__slots__ = ('path',) + \ |
124 |
- ('_acquired', '_kill_test', '_proc', '_files', '_reg_id', '_unlocked') |
125 |
+ ('_acquired', '_kill_test', '_proc', '_files', |
126 |
+ '_reg_id','_unlock_future') |
127 |
|
128 |
def _start(self): |
129 |
in_pr, in_pw = os.pipe() |
130 |
@@ -223,13 +273,16 @@ class _LockProcess(AbstractPollTask): |
131 |
return |
132 |
|
133 |
if not self.cancelled and \ |
134 |
- not self._unlocked: |
135 |
+ self._unlock_future is None: |
136 |
# We don't want lost locks going unnoticed, so it's |
137 |
# only safe to ignore if either the cancel() or |
138 |
# unlock() methods have been previously called. |
139 |
raise AssertionError("lock process failed with returncode %s" \ |
140 |
% (proc.returncode,)) |
141 |
|
142 |
+ if self._unlock_future is not None: |
143 |
+ self._unlock_future.set_result(None) |
144 |
+ |
145 |
def _cancel(self): |
146 |
if self._proc is not None: |
147 |
self._proc.cancel() |
148 |
@@ -271,16 +324,36 @@ class _LockProcess(AbstractPollTask): |
149 |
os.close(pipe_in) |
150 |
|
151 |
def unlock(self): |
152 |
+ """ |
153 |
+ This method is deprecated in favor of async_unlock, since waiting |
154 |
+ for the child process to respond can trigger event loop recursion |
155 |
+ which is incompatible with asyncio. |
156 |
+ """ |
157 |
+ self._unlock() |
158 |
+ self._proc.wait() |
159 |
+ self._proc = None |
160 |
+ |
161 |
+ def _unlock(self): |
162 |
if self._proc is None: |
163 |
raise AssertionError('not locked') |
164 |
- if self.returncode is None: |
165 |
+ if not self._acquired: |
166 |
raise AssertionError('lock not acquired yet') |
167 |
if self.returncode != os.EX_OK: |
168 |
raise AssertionError("lock process failed with returncode %s" \ |
169 |
% (self.returncode,)) |
170 |
- self._unlocked = True |
171 |
+ if self._unlock_future is not None: |
172 |
+ raise AssertionError("already unlocked") |
173 |
+ self._unlock_future = self.scheduler.create_future() |
174 |
os.write(self._files['pipe_out'], b'\0') |
175 |
os.close(self._files['pipe_out']) |
176 |
self._files = None |
177 |
- self._proc.wait() |
178 |
- self._proc = None |
179 |
+ |
180 |
+ def async_unlock(self): |
181 |
+ """ |
182 |
+ Release the lock asynchronously. Release notification is available |
183 |
+ via the add_done_callback method of the returned Future instance. |
184 |
+ |
185 |
+ @returns: Future, result is None |
186 |
+ """ |
187 |
+ self._unlock() |
188 |
+ return self._unlock_future |
189 |
|
190 |
diff --git a/pym/portage/tests/locks/test_asynchronous_lock.py b/pym/portage/tests/locks/test_asynchronous_lock.py |
191 |
index 3a2ccfb84..ab67242d5 100644 |
192 |
--- a/pym/portage/tests/locks/test_asynchronous_lock.py |
193 |
+++ b/pym/portage/tests/locks/test_asynchronous_lock.py |
194 |
@@ -1,6 +1,7 @@ |
195 |
# Copyright 2010-2011 Gentoo Foundation |
196 |
# Distributed under the terms of the GNU General Public License v2 |
197 |
|
198 |
+import itertools |
199 |
import signal |
200 |
import tempfile |
201 |
|
202 |
@@ -17,7 +18,8 @@ class AsynchronousLockTestCase(TestCase): |
203 |
tempdir = tempfile.mkdtemp() |
204 |
try: |
205 |
path = os.path.join(tempdir, 'lock_me') |
206 |
- for force_async in (True, False): |
207 |
+ for force_async, async_unlock in itertools.product( |
208 |
+ (True, False), repeat=2): |
209 |
for force_dummy in (True, False): |
210 |
async_lock = AsynchronousLock(path=path, |
211 |
scheduler=scheduler, _force_async=force_async, |
212 |
@@ -26,7 +28,10 @@ class AsynchronousLockTestCase(TestCase): |
213 |
async_lock.start() |
214 |
self.assertEqual(async_lock.wait(), os.EX_OK) |
215 |
self.assertEqual(async_lock.returncode, os.EX_OK) |
216 |
- async_lock.unlock() |
217 |
+ if async_unlock: |
218 |
+ scheduler.run_until_complete(async_lock.async_unlock()) |
219 |
+ else: |
220 |
+ async_lock.unlock() |
221 |
|
222 |
async_lock = AsynchronousLock(path=path, |
223 |
scheduler=scheduler, _force_async=force_async, |
224 |
@@ -34,8 +39,10 @@ class AsynchronousLockTestCase(TestCase): |
225 |
async_lock.start() |
226 |
self.assertEqual(async_lock.wait(), os.EX_OK) |
227 |
self.assertEqual(async_lock.returncode, os.EX_OK) |
228 |
- async_lock.unlock() |
229 |
- |
230 |
+ if async_unlock: |
231 |
+ scheduler.run_until_complete(async_lock.async_unlock()) |
232 |
+ else: |
233 |
+ async_lock.unlock() |
234 |
finally: |
235 |
shutil.rmtree(tempdir) |