|
27 | 27 | STACK_DEBUG = logging.DEBUG - 1 # heavy-duty debugging |
28 | 28 |
|
29 | 29 |
|
30 | | -class _TracebackLogger: |
31 | | - """Helper to log a traceback upon destruction if not cleared. |
32 | | -
|
33 | | - This solves a nasty problem with Futures and Tasks that have an |
34 | | - exception set: if nobody asks for the exception, the exception is |
35 | | - never logged. This violates the Zen of Python: 'Errors should |
36 | | - never pass silently. Unless explicitly silenced.' |
37 | | -
|
38 | | - However, we don't want to log the exception as soon as |
39 | | - set_exception() is called: if the calling code is written |
40 | | - properly, it will get the exception and handle it properly. But |
41 | | - we *do* want to log it if result() or exception() was never called |
42 | | - -- otherwise developers waste a lot of time wondering why their |
43 | | - buggy code fails silently. |
44 | | -
|
45 | | - An earlier attempt added a __del__() method to the Future class |
46 | | - itself, but this backfired because the presence of __del__() |
47 | | - prevents garbage collection from breaking cycles. A way out of |
48 | | - this catch-22 is to avoid having a __del__() method on the Future |
49 | | - class itself, but instead to have a reference to a helper object |
50 | | - with a __del__() method that logs the traceback, where we ensure |
51 | | - that the helper object doesn't participate in cycles, and only the |
52 | | - Future has a reference to it. |
53 | | -
|
54 | | - The helper object is added when set_exception() is called. When |
55 | | - the Future is collected, and the helper is present, the helper |
56 | | - object is also collected, and its __del__() method will log the |
57 | | - traceback. When the Future's result() or exception() method is |
58 | | - called (and a helper object is present), it removes the helper |
59 | | - object, after calling its clear() method to prevent it from |
60 | | - logging. |
61 | | -
|
62 | | - One downside is that we do a fair amount of work to extract the |
63 | | - traceback from the exception, even when it is never logged. It |
64 | | - would seem cheaper to just store the exception object, but that |
65 | | - references the traceback, which references stack frames, which may |
66 | | - reference the Future, which references the _TracebackLogger, and |
67 | | - then the _TracebackLogger would be included in a cycle, which is |
68 | | - what we're trying to avoid! As an optimization, we don't |
69 | | - immediately format the exception; we only do the work when |
70 | | - activate() is called, which call is delayed until after all the |
71 | | - Future's callbacks have run. Since usually a Future has at least |
72 | | - one callback (typically set by 'yield from') and usually that |
73 | | - callback extracts the callback, thereby removing the need to |
74 | | - format the exception. |
75 | | -
|
76 | | - PS. I don't claim credit for this solution. I first heard of it |
77 | | - in a discussion about closing files when they are collected. |
78 | | - """ |
79 | | - |
80 | | - __slots__ = ('loop', 'source_traceback', 'exc', 'tb') |
81 | | - |
82 | | - def __init__(self, future, exc): |
83 | | - self.loop = future._loop |
84 | | - self.source_traceback = future._source_traceback |
85 | | - self.exc = exc |
86 | | - self.tb = None |
87 | | - |
88 | | - def activate(self): |
89 | | - exc = self.exc |
90 | | - if exc is not None: |
91 | | - self.exc = None |
92 | | - self.tb = traceback.format_exception(exc.__class__, exc, |
93 | | - exc.__traceback__) |
94 | | - |
95 | | - def clear(self): |
96 | | - self.exc = None |
97 | | - self.tb = None |
98 | | - |
99 | | - def __del__(self): |
100 | | - if self.tb: |
101 | | - msg = 'Future/Task exception was never retrieved\n' |
102 | | - if self.source_traceback: |
103 | | - src = ''.join(traceback.format_list(self.source_traceback)) |
104 | | - msg += 'Future/Task created at (most recent call last):\n' |
105 | | - msg += '%s\n' % src.rstrip() |
106 | | - msg += ''.join(self.tb).rstrip() |
107 | | - self.loop.call_exception_handler({'message': msg}) |
108 | | - |
109 | | - |
110 | 30 | class Future: |
111 | 31 | """This class is *almost* compatible with concurrent.futures.Future. |
112 | 32 |
|
@@ -164,25 +84,21 @@ def __init__(self, *, loop=None): |
164 | 84 | def __repr__(self): |
165 | 85 | return '<%s %s>' % (self.__class__.__name__, ' '.join(self._repr_info())) |
166 | 86 |
|
167 | | - # On Python 3.3 and older, objects with a destructor part of a reference |
168 | | - # cycle are never destroyed. It's not more the case on Python 3.4 thanks |
169 | | - # to the PEP 442. |
170 | | - if compat.PY34: |
171 | | - def __del__(self): |
172 | | - if not self._log_traceback: |
173 | | - # set_exception() was not called, or result() or exception() |
174 | | - # has consumed the exception |
175 | | - return |
176 | | - exc = self._exception |
177 | | - context = { |
178 | | - 'message': ('%s exception was never retrieved' |
179 | | - % self.__class__.__name__), |
180 | | - 'exception': exc, |
181 | | - 'future': self, |
182 | | - } |
183 | | - if self._source_traceback: |
184 | | - context['source_traceback'] = self._source_traceback |
185 | | - self._loop.call_exception_handler(context) |
| 87 | + def __del__(self): |
| 88 | + if not self._log_traceback: |
| 89 | + # set_exception() was not called, or result() or exception() |
| 90 | + # has consumed the exception |
| 91 | + return |
| 92 | + exc = self._exception |
| 93 | + context = { |
| 94 | + 'message': ('%s exception was never retrieved' |
| 95 | + % self.__class__.__name__), |
| 96 | + 'exception': exc, |
| 97 | + 'future': self, |
| 98 | + } |
| 99 | + if self._source_traceback: |
| 100 | + context['source_traceback'] = self._source_traceback |
| 101 | + self._loop.call_exception_handler(context) |
186 | 102 |
|
187 | 103 | def cancel(self): |
188 | 104 | """Cancel the future and schedule callbacks. |
@@ -317,13 +233,7 @@ def set_exception(self, exception): |
317 | 233 | self._exception = exception |
318 | 234 | self._state = _FINISHED |
319 | 235 | self._schedule_callbacks() |
320 | | - if compat.PY34: |
321 | | - self._log_traceback = True |
322 | | - else: |
323 | | - self._tb_logger = _TracebackLogger(self, exception) |
324 | | - # Arrange for the logger to be activated after all callbacks |
325 | | - # have had a chance to call result() or exception(). |
326 | | - self._loop.call_soon(self._tb_logger.activate) |
| 236 | + self._log_traceback = True |
327 | 237 |
|
328 | 238 | def __iter__(self): |
329 | 239 | if not self.done(): |
|
0 commit comments