Gentoo Archives: gentoo-portage-dev

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

Replies