Ticket #2639 (closed defect: fixed )

Opened 2 years ago

Last modified 2 years ago

Allow access to the traceback associated with an error thrown into an inlineCallback

Reported by: Peaker Assigned to: Peaker
Type: defect Priority: highest
Milestone: Component: core
Keywords: inlineCallback Cc: indigo, therve, peaker
Branch: Author:
Launchpad Bug:

Description

Currently, when an inlineCallback "yields" a deferred, and that deferred errback's, the yield is thrown the deferred's failure via the defer.py line:

result = g.throw(result.type, result.value, result.tb)

result.tb may be an informative traceback, in which case, yay: no problem exists.

However, result.tb is simply None when the result was errback'd before the addBoth done by inlineCallback (because 'tb' attribute is only kept alive for calls under the stack of the defer.errback(value) call, and this stack includes calls to handlers only if they already were registered when the errback(..) was shot).

I realize that 'tb' is a dangerous thing to keep out of this stack context, but its being None makes the exception raised in the generator have a wrong traceback that contains the yield itself as the source of the exception, rather than the original failure traceback.

It would be useful, if there was a way to map the exception raised by the generator (which, as shown by the quoted defer.py line would be: result.type, result.value) back to "result". This would be useful because then one could use result.frames (via result.printTraceback) to see the interesting information in the original traceback that really caused the problem, rather than just the traceback associated with the "yield" upwards.

There are, however, two problems:

  1. We cannot provide any extra information besides the exception itself (result.type, result.value) to throw(), it will only throw exception objects.
  2. We cannot put more information in the exception object itself in the form of attributes (monkey-patching the exception object is ugly at best, and simply invalid at worst).

In order to associate the "result" and its interesting traceback information with the exception, I propose creating a global WeakKeyDictionary mapping in failure.py, that maps exception objects to Failure objects. I don't believe this to be problematic, memory-wise (in weird use-cases, this may cause the livelyhood of a Failure object beyond its current form, if the exception is kept alive, but I think this use-case is both unuseful and not very problematic).

A Failure object that is created for an exception that already had an associated Failure, would remap the exception, while holding a "prev" attribute for the old Failure. This would in effect cause a chain of failures.

This "chain" would allow traceback printers to work correctly, with all traceback information, whereever Python forced a Failure object to be translated into a naked exception object (of which g.throw is a major case).

I have attached an example to show the cases of informative/uninformative tracebacks when using inlineCallbacks. I also attached a patch draft to failure.py that adds this. It resolves the lack of traceback information issue on the given example.py, but it creates a redundant "chained" traceback when "tb" was not None. I think a simple solution would be to never use the "tb" attribute (i.e: convert the quoted line to g.throw(result.type, result.value, None)).

Another note is that the failure printer I modified, had code to deal with failure chaining (a failure "value" attribute referring to another failure), however this seems impossible, because the Failure __init__ code specifically checks if the value isinstance of Failure, and if so, copies its __dict__, overriding "value" to be an exception object. For now, I just added my "chain" as another chain being printed, but the original chain printing code was never really called, as far as I can see.

Whew, that was long... What do you think?

Attachments

example.py (0.7 kB) - added by Peaker 2 years ago.
Demonstration of the problem with inlineCallbacks and tracebacks
traceback.diff (2.4 kB) - added by Peaker 2 years ago.
A patch that fixes the problem by chaining failures
traceback_norefcycle.diff (2.4 kB) - added by Peaker 2 years ago.
The tracebacks fix without refcycle bug
inlineCallbacks.patch (4.6 kB) - added by Peaker 2 years ago.
Patch to the inlinecallback-traceback-2639 branch that handles throws into generators in the general case (including preservation of tracebacks in inlineCallbacks)
inlineCallbacks.2.patch (4.7 kB) - added by Peaker 2 years ago.
Added docstring to the new test

Change History

  2007-05-13 02:44:13+00:00 changed by Peaker

  • attachment example.py added

Demonstration of the problem with inlineCallbacks and tracebacks

  2007-05-13 02:45:45+00:00 changed by Peaker

  • attachment traceback.diff added

A patch that fixes the problem by chaining failures

  2007-05-13 04:15:29+00:00 changed by Peaker

Oops. There's a serious mistake in there.

A WeakKeyDictionary? will still create strong references back to the Failure objects, and the Failure objects have strong references to the Exception objects, so a reference cycle will still be created.

There is no good solution to this problem (except for fixing Python to handle ref-cycles properly), but a bad solution does exist: Use a WeakValueDictionary? in place of the WeakKeyDictionary?. This means that the Failure will not be kept alive at all by the exception's mapping to it, which means the traceback (frames) data is lost if the Failure is gc'd. As long as the Failure creator makes sure a strong reference still exists by the time the traceback is printed, full information can be printed. Since deferred refer to the failure when it is being handled in callbacks, there's no problem in the specified use case (example.py).

A WeakValueDictionary? also allows us to get rid of ExcKey? and use a simple tuple instead.

I verified that after running the example, the weakvaluedictionary contains no items, so there seems to be no ref-cycle remaining.

The problem regarding the life of Failure and availability of tracebacks may have to be documented somewhere, if this patch is accepted.

  2007-05-13 04:16:28+00:00 changed by Peaker

  • attachment traceback_norefcycle.diff added

The tracebacks fix without refcycle bug

  2007-05-13 16:22:22+00:00 changed by Peaker

  • keywords changed from inlineCallbacks tracebacks to inlineCallbacks tracebacks review

  2007-05-13 17:04:45+00:00 changed by therve

  • keywords changed from inlineCallbacks tracebacks review to inlineCallbacks
  • owner changed from glyph to therve

  2007-05-13 20:03:51+00:00 changed by therve

  • owner changed from therve to radix

  2007-05-13 22:41:30+00:00 changed by jknight

We should also make sure creating Failure objects doesn't get even more excessively expensive than it already is.

  2007-05-18 15:36:49+00:00 changed by radix

  • status changed from new to assigned

I've got a branch in-development at inlinecallback-traceback-2639.

It actually fixes the old deferredGenerator to provide better tracebacks already, but not yet for inlineCallbacks, because the way g.throw works causes differently-shaped tracebacks and I haven't figured out all the cases yet. I'll probably work on it again this weekend.

  2007-05-24 17:10:09+00:00 changed by indigo

  • cc set to indigo

I just encountered the same problem; I wonder if #2495 is of any help. I didn't go deeply into the details, but it provides a way to get a fake sort of traceback when tb is None with Failure.getTracebackObject().

  2007-07-03 21:43:19+00:00 changed by Peaker

  • attachment inlineCallbacks.patch added

Patch to the inlinecallback-traceback-2639 branch that handles throws into generators in the general case (including preservation of tracebacks in inlineCallbacks)

  2007-07-03 21:53:43+00:00 changed by Peaker

  • attachment inlineCallbacks.2.patch added

Added docstring to the new test

  2007-07-03 22:33:56+00:00 changed by Peaker

  • keywords changed from inlineCallbacks to inlineCallbacks review

Please review the entire work in the branch (the above patch was already applied), in order to commit it into trunk.

  2007-07-03 22:50:08+00:00 changed by Peaker

  • owner deleted
  • status changed from assigned to new

  2007-07-06 06:59:18+00:00 changed by therve

  • cc changed from indigo to indigo, therve
  • keywords changed from inlineCallbacks review to inlineCallbacks
  • owner set to Peaker

Some comments:

  • the '@' syntax can't be used to keep compatibility with 2.3 (@classmethod on findFailure). I guess that doesn't apply to @defer.inlineCallbacks in generator_failure_tests.
  • you can call a classmethod directly on self (instead of doing self.__class__). Furthermore, I don't really see the point of using a classmethod here.
  • the lines
        if frame.f_code is Failure.raiseException.func_code:
            return frame.f_locals['self']
    

in findFailure are not tested.

  • don't use basestring, (str, unicode) is fine.
  • please use assertEquals instead of assertEqual.
  • generator_failure_tests needs a copyright notice. Other files should update their year info.

Overall, there are very good tests, but the findFailure method is insane. We may not have the choice here though.

  2007-07-06 13:45:54+00:00 changed by exarkun

There's a pretty big open question about what the performance implications of this change are, specifically the findFailure method. Generally I don't think it's worth considering performance before correctness, but since the correctness here is only in the form of debugging information for a convenience API, and the performance implications could be potentially staggering for every single Twisted application, it's probably worth checking out in this case.

The decorator syntax shouldn't be used anywhere, even in generator_failure_tests.

Also I prefer assertEqual over assertEquals. ;)

  2007-07-08 00:30:52+00:00 changed by Peaker

  • keywords changed from inlineCallbacks to inlineCallbacks review

Thanks for the comments therve/exarkun.

Fixes:

  1. I fixed the @ syntax in both failure.py (classmethod) and the generator tests.
  2. Coverage issue was due to a bug in test_failure (findFailure tests weren't marked as a TestCase?), fixed.
  3. The irrelevant basestring change was reverted.
  4. self.__class__.clsmethod was converted to self.clsmethod.

I have wondered myself whether the proper place for my inlineCallbacks test is in test_defer or here (as this is but one case of throwExceptionIntoGenerator use, even if the only one in Twisted), but I guess it doesn't matter much.

  2007-07-08 13:04:40+00:00 changed by exarkun

  • owner deleted

re-assign tickets to nobody or a reviewer when you put them back up for review.

  2007-07-10 21:22:52+00:00 changed by Peaker

  • cc changed from indigo, therve to indigo, therve, peaker

I forgot to also mention that the doc/core/benchmarks failure benchmarks have no noticable performance decrease on my machine, for these changes.

  2007-07-11 12:23:02+00:00 changed by exarkun

  • priority changed from normal to highest

follow-up: ↓ 18   2007-07-31 16:15:17+00:00 changed by jknight

  • keywords changed from inlineCallbacks review to inlineCallback
  • owner set to Peaker

I thought I was going to hate this change but it actually looks pretty sane. I expect the overhead of walking the frames in the new findFailure function will be completely negligible compared to the other crap done by Failure (which does usually also include traversing said frames).

I would suggest making findFailure private (aka: _findFailure). I don't see any need for it to be part of the API.

Other than that, no objections here.

  2007-07-31 18:07:16+00:00 changed by Peaker

  • keywords changed from inlineCallback to inlineCallback review
  • owner deleted

in reply to: ↑ 16   2007-08-01 21:18:56+00:00 changed by Peaker

Replying to jknight:

I thought I was going to hate this change but it actually looks pretty sane. I expect the overhead of walking the frames in the new findFailure function will be completely negligible compared to the other crap done by Failure (which does usually also include traversing said frames). I would suggest making findFailure private (aka: _findFailure). I don't see any need for it to be part of the API. Other than that, no objections here.

Now that that was fixed, a stamp of approval? :-)

  2007-08-06 20:00:58+00:00 changed by Peaker

Another note: psyco makes frames behave differently. When psyco is enabled and compiles the Failure methods, the frames of the Failure.raise/throw* functions don't contain 'self' in their locals() (which becomes empty).

A workaround of using psyco.cannotcompile() on the relevant Failure functions is not a good solution, because merely importing psyco (in order to call cannotcompile) makes pdb unusable.

This problem can be delegated to a user, who can call cannotcompile() on the failure methods himself, or a utility method to do this (importing psyco locally) can be provided.

Any feedback about which method is preferrable?

follow-up: ↓ 21   2007-08-07 00:32:50+00:00 changed by jknight

Given that psycho makes debugging impossible anyhow, I'd suggest just making sure this fails gracefully when locals() doesn't contain self. (that is: the program continues working, but the useful failure tracebacks won't be present).

in reply to: ↑ 20   2007-08-08 19:47:37+00:00 changed by Peaker

Replying to jknight:

Given that psycho makes debugging impossible anyhow, I'd suggest just making sure this fails gracefully when locals() doesn't contain self. (that is: the program continues working, but the useful failure tracebacks won't be present).

Okay, I converted findFailure's locals lookups to use get, and return None if the frame locals does not contain 'self'. Also added a comment above that all to explain the reasoning behind that get().

  2007-08-10 03:40:27+00:00 changed by jknight

  • keywords changed from inlineCallback review to inlineCallback
  • owner set to Peaker

It all looks good to me, +1 for merging.

  2007-08-20 20:39:42+00:00 changed by Peaker

  • status changed from new to closed
  • resolution set to fixed

  2007-08-20 21:38:05+00:00 changed by exarkun

Branch was merged at [21026].

Note: See TracTickets for help on using tickets.