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