syrusakbary / promise

Ultra-performant Promise implementation in Python
MIT License
362 stars 76 forks source link

Support tracing on rejection? #10

Open plesner opened 8 years ago

plesner commented 8 years ago

I had to implement a service for a company recently and used promises from this library as the core way to organize asynchronous operations. It works great and is running happily in production.

However, as soon as you then-chain more than a few promises, understanding the reasons for rejections becomes super painful and time-consuming. One thing that would have helped me a lot during development was if I could trace how rejection propagates through chains of promises. I would suggest adding support for this in some form.

Here's a really simple example of what I have in mind. Say you create a chain of promises,

    p0 = Promise()
    p1 = p0.then(partial(add, 1))
    p2 = p1.then(partial(add, 2))
    p3 = p2.then(partial(add, 3))

and then you reject the "root",

    p0.reject(MyError('D'))

If I only have p3 somewhere else in the program it becomes really hard to understand that it was rejected through p0. What I'd like is a way to get a trace from p3, something like

  File "/home/tundra/Workspace/python-promise/tests/test_tracing.py", line 54, in run_test_chained_rejection
    p0.reject(MyError('D'))
--- caused by ---
  ... repeated frames elided ...
  File "/home/tundra/Workspace/python-promise/tests/test_tracing.py", line 54, in run_test_chained_rejection
    return run_test_chained_rejection(remaining_calls - 1)
  File "/home/tundra/Workspace/python-promise/tests/test_tracing.py", line 56, in run_test_chained_rejection
    p1 = p0.then(partial(add, 1))
--- caused by ---
  ... repeated frames elided ...
  File "/home/tundra/Workspace/python-promise/tests/test_tracing.py", line 54, in run_test_chained_rejection
    return run_test_chained_rejection(remaining_calls - 1)
  File "/home/tundra/Workspace/python-promise/tests/test_tracing.py", line 57, in run_test_chained_rejection
    p2 = p1.then(partial(add, 2))
--- caused by ---
  ... repeated frames elided ...
  File "/home/tundra/Workspace/python-promise/tests/test_tracing.py", line 54, in run_test_chained_rejection
    return run_test_chained_rejection(remaining_calls - 1)
  File "/home/tundra/Workspace/python-promise/tests/test_tracing.py", line 58, in run_test_chained_rejection
    p3 = p2.then(partial(add, 3))

This way you can follow the chain of promises backwards and get to the original cause. If I was to build support for this that you could optionally enable (because it may be too expensive to keep enabled always) do you think you might be interested in including it in the library?

syrusakbary commented 8 years ago

Yeah, that would be a great addition to the library! :)

plesner commented 8 years ago

Cool. I'm already playing around with an implementation (plesner/python-promise#1), I'll send you a pull request when it's complete. I've made it as a separate module, promise.tracing, that works exactly the same as promise (almost all the code is shared) so you can easily flip between the two. But I'm happy to do it in a different way if you have any suggestions.

plesner commented 8 years ago

I've spent some time trying to implement this and it's turned out to be trickier than I expected.

Collecting stack traces and building the trace isn't too hard but the top of all traces will be 2-3 frames from within the promise library which makes them cluttered and hard to read. I haven't been able to find a way to avoid those without adding extra arguments to init, reject, etc. which feels like a hack and pollutes the external api.

I may be able to come up with a different approach but if I don't follow up with an implementation it'll be because I can't find a solution to this problem.

addyess commented 7 years ago

Does this implement what you wanted? https://github.com/syrusakbary/promise/pull/18