Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit fea6a10

Browse files
committed
asyncio: sync with Tulip
Improve stability of the proactor event loop, especially operations on overlapped objects: * Tulip issue 195: Don't call UnregisterWait() twice if a _WaitHandleFuture is cancelled twice to fix a crash. * IocpProactor.close(): cancel futures to cancel overlapped operations, instead of cancelling directly overlapped operations. Future objects may not call ov.cancel() if the future was cancelled or if the overlapped was already cancelled. The cancel() method of the future may also catch exceptions. Log also errors on cancellation. * tests: rename "f" to "fut" * Add a __repr__() method to IocpProactor * Add a destructor to IocpProactor which closes it * _OverlappedFuture.cancel() doesn't cancel the overlapped anymore if it is done: if it is already cancelled or completed. Log also an error if the cancellation failed. * Add the address of the overlapped object in repr(_OverlappedFuture) * _OverlappedFuture truncates the source traceback to hide the call to the parent constructor (useless in debug).
1 parent 92639cc commit fea6a10

2 files changed

Lines changed: 66 additions & 30 deletions

File tree

Lib/asyncio/windows_events.py

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -38,25 +38,33 @@ class _OverlappedFuture(futures.Future):
3838

3939
def __init__(self, ov, *, loop=None):
4040
super().__init__(loop=loop)
41+
if self._source_traceback:
42+
del self._source_traceback[-1]
4143
self.ov = ov
4244

4345
def __repr__(self):
4446
info = [self._state.lower()]
45-
if self.ov.pending:
46-
info.append('overlapped=pending')
47-
else:
48-
info.append('overlapped=completed')
47+
state = 'pending' if self.ov.pending else 'completed'
48+
info.append('overlapped=<%s, %#x>' % (state, self.ov.address))
4949
if self._state == futures._FINISHED:
5050
info.append(self._format_result())
5151
if self._callbacks:
5252
info.append(self._format_callbacks())
5353
return '<%s %s>' % (self.__class__.__name__, ' '.join(info))
5454

5555
def cancel(self):
56-
try:
57-
self.ov.cancel()
58-
except OSError:
59-
pass
56+
if not self.done():
57+
try:
58+
self.ov.cancel()
59+
except OSError as exc:
60+
context = {
61+
'message': 'Cancelling an overlapped future failed',
62+
'exception': exc,
63+
'future': self,
64+
}
65+
if self._source_traceback:
66+
context['source_traceback'] = self._source_traceback
67+
self._loop.call_exception_handler(context)
6068
return super().cancel()
6169

6270

@@ -67,13 +75,20 @@ def __init__(self, wait_handle, *, loop=None):
6775
super().__init__(loop=loop)
6876
self._wait_handle = wait_handle
6977

70-
def cancel(self):
71-
super().cancel()
78+
def _unregister(self):
79+
if self._wait_handle is None:
80+
return
7281
try:
7382
_overlapped.UnregisterWait(self._wait_handle)
7483
except OSError as e:
7584
if e.winerror != _overlapped.ERROR_IO_PENDING:
7685
raise
86+
# ERROR_IO_PENDING is not an error, the wait was unregistered
87+
self._wait_handle = None
88+
89+
def cancel(self):
90+
self._unregister()
91+
super().cancel()
7792

7893

7994
class PipeServer(object):
@@ -208,6 +223,11 @@ def __init__(self, concurrency=0xffffffff):
208223
self._registered = weakref.WeakSet()
209224
self._stopped_serving = weakref.WeakSet()
210225

226+
def __repr__(self):
227+
return ('<%s overlapped#=%s result#=%s>'
228+
% (self.__class__.__name__, len(self._cache),
229+
len(self._results)))
230+
211231
def set_loop(self, loop):
212232
self._loop = loop
213233

@@ -353,12 +373,7 @@ def wait_for_handle(self, handle, timeout=None):
353373
f = _WaitHandleFuture(wh, loop=self._loop)
354374

355375
def finish_wait_for_handle(trans, key, ov):
356-
if not f.cancelled():
357-
try:
358-
_overlapped.UnregisterWait(wh)
359-
except OSError as e:
360-
if e.winerror != _overlapped.ERROR_IO_PENDING:
361-
raise
376+
f._unregister()
362377
# Note that this second wait means that we should only use
363378
# this with handles types where a successful wait has no
364379
# effect. So events or processes are all right, but locks
@@ -455,17 +470,25 @@ def _stop_serving(self, obj):
455470

456471
def close(self):
457472
# Cancel remaining registered operations.
458-
for address, (f, ov, obj, callback) in list(self._cache.items()):
473+
for address, (fut, ov, obj, callback) in list(self._cache.items()):
459474
if obj is None:
460475
# The operation was started with connect_pipe() which
461476
# queues a task to Windows' thread pool. This cannot
462477
# be cancelled, so just forget it.
463478
del self._cache[address]
464479
else:
465480
try:
466-
ov.cancel()
467-
except OSError:
468-
pass
481+
fut.cancel()
482+
except OSError as exc:
483+
if self._loop is not None:
484+
context = {
485+
'message': 'Cancelling a future failed',
486+
'exception': exc,
487+
'future': fut,
488+
}
489+
if fut._source_traceback:
490+
context['source_traceback'] = fut._source_traceback
491+
self._loop.call_exception_handler(context)
469492

470493
while self._cache:
471494
if not self._poll(1):
@@ -476,6 +499,9 @@ def close(self):
476499
_winapi.CloseHandle(self._iocp)
477500
self._iocp = None
478501

502+
def __del__(self):
503+
self.close()
504+
479505

480506
class _WindowsSubprocessTransport(base_subprocess.BaseSubprocessTransport):
481507

Lib/test/test_asyncio/test_windows_events.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -96,36 +96,46 @@ def test_wait_for_handle(self):
9696

9797
# Wait for unset event with 0.5s timeout;
9898
# result should be False at timeout
99-
f = self.loop._proactor.wait_for_handle(event, 0.5)
99+
fut = self.loop._proactor.wait_for_handle(event, 0.5)
100100
start = self.loop.time()
101-
self.loop.run_until_complete(f)
101+
self.loop.run_until_complete(fut)
102102
elapsed = self.loop.time() - start
103-
self.assertFalse(f.result())
103+
self.assertFalse(fut.result())
104104
self.assertTrue(0.48 < elapsed < 0.9, elapsed)
105105

106106
_overlapped.SetEvent(event)
107107

108108
# Wait for for set event;
109109
# result should be True immediately
110-
f = self.loop._proactor.wait_for_handle(event, 10)
110+
fut = self.loop._proactor.wait_for_handle(event, 10)
111111
start = self.loop.time()
112-
self.loop.run_until_complete(f)
112+
self.loop.run_until_complete(fut)
113113
elapsed = self.loop.time() - start
114-
self.assertTrue(f.result())
114+
self.assertTrue(fut.result())
115115
self.assertTrue(0 <= elapsed < 0.3, elapsed)
116116

117-
_overlapped.ResetEvent(event)
117+
# Tulip issue #195: cancelling a done _WaitHandleFuture must not crash
118+
fut.cancel()
119+
120+
def test_wait_for_handle_cancel(self):
121+
event = _overlapped.CreateEvent(None, True, False, None)
122+
self.addCleanup(_winapi.CloseHandle, event)
118123

119124
# Wait for unset event with a cancelled future;
120125
# CancelledError should be raised immediately
121-
f = self.loop._proactor.wait_for_handle(event, 10)
122-
f.cancel()
126+
fut = self.loop._proactor.wait_for_handle(event, 10)
127+
fut.cancel()
123128
start = self.loop.time()
124129
with self.assertRaises(asyncio.CancelledError):
125-
self.loop.run_until_complete(f)
130+
self.loop.run_until_complete(fut)
126131
elapsed = self.loop.time() - start
127132
self.assertTrue(0 <= elapsed < 0.1, elapsed)
128133

134+
# Tulip issue #195: cancelling a _WaitHandleFuture twice must not crash
135+
fut = self.loop._proactor.wait_for_handle(event)
136+
fut.cancel()
137+
fut.cancel()
138+
129139

130140
if __name__ == '__main__':
131141
unittest.main()

0 commit comments

Comments
 (0)