-
Notifications
You must be signed in to change notification settings - Fork 56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Issue 871 #874
Issue 871 #874
Conversation
for more information, see https://pre-commit.ci
Coverage reportThe coverage rate went from
Diff Coverage details (click to unfold)procrastinate/retry.py
procrastinate/tasks.py
procrastinate/worker.py
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A very promising PR :) We're not far from being in a mergeable state. I've included comments, and on a copy of this branch, I've added a commit for how I would have seen it (it's a suggestion more than anything mandatory, but I have the feeling that it's slightly simpler). Feel free to cherry-pick and/or take part of it.
My idea is that we had 2 "failing" outcomes to run_job
: fail with retry and fail without retry (2 exceptions). We now have 4 outcomes: critical/non-critical fail with/without retry. This smells a lot like the "inheritence vs composition debate", and indicates (to me) that the difference between retry and fail should be a difference of an attribute, not of a subtype. Sadly, we can't easily change RetryStrategy.get_retry_exception
's return type because that's part of the public API (though we could change Task.get_retry_exception
's signature, might be better that what I did here).
procrastinate/worker.py
Outdated
def find_exception_to_re_raise(ex: ProcrastinateException) -> Optional[BaseException]: | ||
# If the job raises a BaseException that is _not_ an Exception | ||
# (e.g. a CancelledError, SystemExit, etc.) then we want to persist the | ||
# outcome of the job before propagating the exception further up the | ||
# call stack. | ||
return ex.__cause__ if not isinstance(e.__cause__, Exception) else None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I though that throughout the whole codebase, all the exceptions were always named exc
. It turns out there are a handful of exceptions, but I'd rather we try and keep it that way (you don't have to fix the other non-exc
exceptions, of course, but it's perfectly fine if you choose to do it, otherwise, I might or might not do it later)
Note
this whole function is removed in the commit I suggested
test_worker.run_job.assert_called_with(job=job, worker_id=0) | ||
assert connector.jobs[1]["status"] == "todo" | ||
assert connector.jobs[1]["scheduled_at"] == scheduled_at | ||
assert connector.jobs[1]["attempts"] == 1 | ||
|
||
|
||
async def test_run_job(app): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(should we also test run_job
when the job raises a BaseException ? That was you original bug)
@@ -201,6 +214,9 @@ async def process_job(self, job: jobs.Job, worker_id: int = 0) -> None: | |||
# Remove job information from the current context | |||
self.context_for_worker(worker_id=worker_id, reset=True) | |||
|
|||
if exception_to_re_raise is not None: | |||
raise exception_to_re_raise |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm personally extremely wary about the idea of raising or returning from within a finally:
block. This is one of the lesser know python gotchas that can bite very hard.
try:
0/0
except DivisionError:
raise ValueError
finally:
return True
The example above is making the ValueError exception disappear completely. Poof. Nobody can except it. Gone with the wind (same happens if you raise from within the finally).
Given that "Errors should never pass silently", I've always considered this a very dangerous practice.
For example: imagine I add the following log line:
try:
await self.run_job(job=job, worker_id=worker_id)
status = jobs.Status.SUCCEEDED
except exceptions.JobError as e:
status = jobs.Status.FAILED
exception_to_re_raise = find_exception_to_re_raise(e)
logger.errorr("Ooops, something went bad")
finally:
# ...
if exception_to_re_raise is not None:
raise exception_to_re_raise
I would never find my log, though the rest would work. Because I typed errorr
instead of error
, the code produces: AttributeError: 'RootLogger' object has no attribute 'errorr'. Did you mean: 'error'?
but that exception vanishes when we raise in the finally. I'll never know that my code has a bug.
What you really want to do is raise in the except:
block. It will actually do the raising after the finally
block is executed.
Note
I took care of this in the commit I suggested
I've added a commit for taking care of tests in my branch (https://github.com/procrastinate-org/procrastinate/commits/issue-871-ewjoachim/) (both fixing what my previous commit has broken, and adding the test I mentioned). As it stands, I'd be ready to merge our 2 commits together. But this is your PR, so I'll wait for you opinion before doing so :) And rest assured it's perfectly ok to not agree with my changes! |
Morning @ewjoachim! Your changes make perfect sense; very happy for you to merge with your additions |
Closes #871
Successful PR Checklist: