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

Skip to content

Commit ff827f0

Browse files
committed
asyncio: New error handling API. Issue #20681.
1 parent 065efc3 commit ff827f0

15 files changed

Lines changed: 491 additions & 99 deletions

Lib/asyncio/base_events.py

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ def __init__(self):
122122
self._internal_fds = 0
123123
self._running = False
124124
self._clock_resolution = time.get_clock_info('monotonic').resolution
125+
self._exception_handler = None
125126

126127
def _make_socket_transport(self, sock, protocol, waiter=None, *,
127128
extra=None, server=None):
@@ -254,7 +255,7 @@ def call_at(self, when, callback, *args):
254255
"""Like call_later(), but uses an absolute time."""
255256
if tasks.iscoroutinefunction(callback):
256257
raise TypeError("coroutines cannot be used with call_at()")
257-
timer = events.TimerHandle(when, callback, args)
258+
timer = events.TimerHandle(when, callback, args, self)
258259
heapq.heappush(self._scheduled, timer)
259260
return timer
260261

@@ -270,7 +271,7 @@ def call_soon(self, callback, *args):
270271
"""
271272
if tasks.iscoroutinefunction(callback):
272273
raise TypeError("coroutines cannot be used with call_soon()")
273-
handle = events.Handle(callback, args)
274+
handle = events.Handle(callback, args, self)
274275
self._ready.append(handle)
275276
return handle
276277

@@ -625,6 +626,97 @@ def subprocess_exec(self, protocol_factory, program, *args, stdin=subprocess.PIP
625626
protocol, popen_args, False, stdin, stdout, stderr, bufsize, **kwargs)
626627
return transport, protocol
627628

629+
def set_exception_handler(self, handler):
630+
"""Set handler as the new event loop exception handler.
631+
632+
If handler is None, the default exception handler will
633+
be set.
634+
635+
If handler is a callable object, it should have a
636+
matching signature to '(loop, context)', where 'loop'
637+
will be a reference to the active event loop, 'context'
638+
will be a dict object (see `call_exception_handler()`
639+
documentation for details about context).
640+
"""
641+
if handler is not None and not callable(handler):
642+
raise TypeError('A callable object or None is expected, '
643+
'got {!r}'.format(handler))
644+
self._exception_handler = handler
645+
646+
def default_exception_handler(self, context):
647+
"""Default exception handler.
648+
649+
This is called when an exception occurs and no exception
650+
handler is set, and can be called by a custom exception
651+
handler that wants to defer to the default behavior.
652+
653+
context parameter has the same meaning as in
654+
`call_exception_handler()`.
655+
"""
656+
message = context.get('message')
657+
if not message:
658+
message = 'Unhandled exception in event loop'
659+
660+
exception = context.get('exception')
661+
if exception is not None:
662+
exc_info = (type(exception), exception, exception.__traceback__)
663+
else:
664+
exc_info = False
665+
666+
log_lines = [message]
667+
for key in sorted(context):
668+
if key in {'message', 'exception'}:
669+
continue
670+
log_lines.append('{}: {!r}'.format(key, context[key]))
671+
672+
logger.error('\n'.join(log_lines), exc_info=exc_info)
673+
674+
def call_exception_handler(self, context):
675+
"""Call the current event loop exception handler.
676+
677+
context is a dict object containing the following keys
678+
(new keys maybe introduced later):
679+
- 'message': Error message;
680+
- 'exception' (optional): Exception object;
681+
- 'future' (optional): Future instance;
682+
- 'handle' (optional): Handle instance;
683+
- 'protocol' (optional): Protocol instance;
684+
- 'transport' (optional): Transport instance;
685+
- 'socket' (optional): Socket instance.
686+
687+
Note: this method should not be overloaded in subclassed
688+
event loops. For any custom exception handling, use
689+
`set_exception_handler()` method.
690+
"""
691+
if self._exception_handler is None:
692+
try:
693+
self.default_exception_handler(context)
694+
except Exception:
695+
# Second protection layer for unexpected errors
696+
# in the default implementation, as well as for subclassed
697+
# event loops with overloaded "default_exception_handler".
698+
logger.error('Exception in default exception handler',
699+
exc_info=True)
700+
else:
701+
try:
702+
self._exception_handler(self, context)
703+
except Exception as exc:
704+
# Exception in the user set custom exception handler.
705+
try:
706+
# Let's try default handler.
707+
self.default_exception_handler({
708+
'message': 'Unhandled error in exception handler',
709+
'exception': exc,
710+
'context': context,
711+
})
712+
except Exception:
713+
# Guard 'default_exception_handler' in case it's
714+
# overloaded.
715+
logger.error('Exception in default exception handler '
716+
'while handling an unexpected error '
717+
'in custom exception handler',
718+
exc_info=True)
719+
628720
def _add_callback(self, handle):
629721
"""Add a Handle to ready or scheduled."""
630722
assert isinstance(handle, events.Handle), 'A Handle is required here'

Lib/asyncio/events.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@
1919
class Handle:
2020
"""Object returned by callback registration methods."""
2121

22-
__slots__ = ['_callback', '_args', '_cancelled']
22+
__slots__ = ['_callback', '_args', '_cancelled', '_loop']
2323

24-
def __init__(self, callback, args):
24+
def __init__(self, callback, args, loop):
2525
assert not isinstance(callback, Handle), 'A Handle is not a callback'
26+
self._loop = loop
2627
self._callback = callback
2728
self._args = args
2829
self._cancelled = False
@@ -39,9 +40,14 @@ def cancel(self):
3940
def _run(self):
4041
try:
4142
self._callback(*self._args)
42-
except Exception:
43-
logger.exception('Exception in callback %s %r',
44-
self._callback, self._args)
43+
except Exception as exc:
44+
msg = 'Exception in callback {}{!r}'.format(self._callback,
45+
self._args)
46+
self._loop.call_exception_handler({
47+
'message': msg,
48+
'exception': exc,
49+
'handle': self,
50+
})
4551
self = None # Needed to break cycles when an exception occurs.
4652

4753

@@ -50,9 +56,9 @@ class TimerHandle(Handle):
5056

5157
__slots__ = ['_when']
5258

53-
def __init__(self, when, callback, args):
59+
def __init__(self, when, callback, args, loop):
5460
assert when is not None
55-
super().__init__(callback, args)
61+
super().__init__(callback, args, loop)
5662

5763
self._when = when
5864

@@ -328,6 +334,17 @@ def add_signal_handler(self, sig, callback, *args):
328334
def remove_signal_handler(self, sig):
329335
raise NotImplementedError
330336

337+
# Error handlers.
338+
339+
def set_exception_handler(self, handler):
340+
raise NotImplementedError
341+
342+
def default_exception_handler(self, context):
343+
raise NotImplementedError
344+
345+
def call_exception_handler(self, context):
346+
raise NotImplementedError
347+
331348

332349
class AbstractEventLoopPolicy:
333350
"""Abstract policy for accessing the event loop."""

Lib/asyncio/futures.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,10 @@ class itself, but instead to have a reference to a helper object
8383
in a discussion about closing files when they are collected.
8484
"""
8585

86-
__slots__ = ['exc', 'tb']
86+
__slots__ = ['exc', 'tb', 'loop']
8787

88-
def __init__(self, exc):
88+
def __init__(self, exc, loop):
89+
self.loop = loop
8990
self.exc = exc
9091
self.tb = None
9192

@@ -102,8 +103,11 @@ def clear(self):
102103

103104
def __del__(self):
104105
if self.tb:
105-
logger.error('Future/Task exception was never retrieved:\n%s',
106-
''.join(self.tb))
106+
msg = 'Future/Task exception was never retrieved:\n{tb}'
107+
context = {
108+
'message': msg.format(tb=''.join(self.tb)),
109+
}
110+
self.loop.call_exception_handler(context)
107111

108112

109113
class Future:
@@ -173,8 +177,12 @@ def __del__(self):
173177
# has consumed the exception
174178
return
175179
exc = self._exception
176-
logger.error('Future/Task exception was never retrieved:',
177-
exc_info=(exc.__class__, exc, exc.__traceback__))
180+
context = {
181+
'message': 'Future/Task exception was never retrieved',
182+
'exception': exc,
183+
'future': self,
184+
}
185+
self._loop.call_exception_handler(context)
178186

179187
def cancel(self):
180188
"""Cancel the future and schedule callbacks.
@@ -309,7 +317,7 @@ def set_exception(self, exception):
309317
if _PY34:
310318
self._log_traceback = True
311319
else:
312-
self._tb_logger = _TracebackLogger(exception)
320+
self._tb_logger = _TracebackLogger(exception, self._loop)
313321
# Arrange for the logger to be activated after all callbacks
314322
# have had a chance to call result() or exception().
315323
self._loop.call_soon(self._tb_logger.activate)

Lib/asyncio/proactor_events.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,12 @@ def close(self):
5656

5757
def _fatal_error(self, exc):
5858
if not isinstance(exc, (BrokenPipeError, ConnectionResetError)):
59-
logger.exception('Fatal error for %s', self)
59+
self._loop.call_exception_handler({
60+
'message': 'Fatal transport error',
61+
'exception': exc,
62+
'transport': self,
63+
'protocol': self._protocol,
64+
})
6065
self._force_close(exc)
6166

6267
def _force_close(self, exc):
@@ -103,17 +108,27 @@ def _maybe_pause_protocol(self):
103108
self._protocol_paused = True
104109
try:
105110
self._protocol.pause_writing()
106-
except Exception:
107-
logger.exception('pause_writing() failed')
111+
except Exception as exc:
112+
self._loop.call_exception_handler({
113+
'message': 'protocol.pause_writing() failed',
114+
'exception': exc,
115+
'transport': self,
116+
'protocol': self._protocol,
117+
})
108118

109119
def _maybe_resume_protocol(self):
110120
if (self._protocol_paused and
111121
self.get_write_buffer_size() <= self._low_water):
112122
self._protocol_paused = False
113123
try:
114124
self._protocol.resume_writing()
115-
except Exception:
116-
logger.exception('resume_writing() failed')
125+
except Exception as exc:
126+
self._loop.call_exception_handler({
127+
'message': 'protocol.resume_writing() failed',
128+
'exception': exc,
129+
'transport': self,
130+
'protocol': self._protocol,
131+
})
117132

118133
def set_write_buffer_limits(self, high=None, low=None):
119134
if high is None:
@@ -465,9 +480,13 @@ def loop(f=None):
465480
conn, protocol,
466481
extra={'peername': addr}, server=server)
467482
f = self._proactor.accept(sock)
468-
except OSError:
483+
except OSError as exc:
469484
if sock.fileno() != -1:
470-
logger.exception('Accept failed')
485+
self.call_exception_handler({
486+
'message': 'Accept failed',
487+
'exception': exc,
488+
'socket': sock,
489+
})
471490
sock.close()
472491
except futures.CancelledError:
473492
sock.close()

Lib/asyncio/selector_events.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,11 @@ def _accept_connection(self, protocol_factory, sock,
112112
# Some platforms (e.g. Linux keep reporting the FD as
113113
# ready, so we remove the read handler temporarily.
114114
# We'll try again in a while.
115-
logger.exception('Accept out of system resource (%s)', exc)
115+
self.call_exception_handler({
116+
'message': 'socket.accept() out of system resource',
117+
'exception': exc,
118+
'socket': sock,
119+
})
116120
self.remove_reader(sock.fileno())
117121
self.call_later(constants.ACCEPT_RETRY_DELAY,
118122
self._start_serving,
@@ -132,7 +136,7 @@ def _accept_connection(self, protocol_factory, sock,
132136

133137
def add_reader(self, fd, callback, *args):
134138
"""Add a reader callback."""
135-
handle = events.Handle(callback, args)
139+
handle = events.Handle(callback, args, self)
136140
try:
137141
key = self._selector.get_key(fd)
138142
except KeyError:
@@ -167,7 +171,7 @@ def remove_reader(self, fd):
167171

168172
def add_writer(self, fd, callback, *args):
169173
"""Add a writer callback.."""
170-
handle = events.Handle(callback, args)
174+
handle = events.Handle(callback, args, self)
171175
try:
172176
key = self._selector.get_key(fd)
173177
except KeyError:
@@ -364,17 +368,27 @@ def _maybe_pause_protocol(self):
364368
self._protocol_paused = True
365369
try:
366370
self._protocol.pause_writing()
367-
except Exception:
368-
logger.exception('pause_writing() failed')
371+
except Exception as exc:
372+
self._loop.call_exception_handler({
373+
'message': 'protocol.pause_writing() failed',
374+
'exception': exc,
375+
'transport': self,
376+
'protocol': self._protocol,
377+
})
369378

370379
def _maybe_resume_protocol(self):
371380
if (self._protocol_paused and
372381
self.get_write_buffer_size() <= self._low_water):
373382
self._protocol_paused = False
374383
try:
375384
self._protocol.resume_writing()
376-
except Exception:
377-
logger.exception('resume_writing() failed')
385+
except Exception as exc:
386+
self._loop.call_exception_handler({
387+
'message': 'protocol.resume_writing() failed',
388+
'exception': exc,
389+
'transport': self,
390+
'protocol': self._protocol,
391+
})
378392

379393
def set_write_buffer_limits(self, high=None, low=None):
380394
if high is None:
@@ -435,7 +449,12 @@ def close(self):
435449
def _fatal_error(self, exc):
436450
# Should be called from exception handler only.
437451
if not isinstance(exc, (BrokenPipeError, ConnectionResetError)):
438-
logger.exception('Fatal error for %s', self)
452+
self._loop.call_exception_handler({
453+
'message': 'Fatal transport error',
454+
'exception': exc,
455+
'transport': self,
456+
'protocol': self._protocol,
457+
})
439458
self._force_close(exc)
440459

441460
def _force_close(self, exc):

0 commit comments

Comments
 (0)