Skip to content
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

Merged
merged 4 commits into from
Dec 21, 2023
Merged

Issue 871 #874

merged 4 commits into from
Dec 21, 2023

Conversation

ashleyheath
Copy link
Contributor

Closes #871

Successful PR Checklist:

  • Tests
    • (not applicable?)
  • Documentation
    • (not applicable?)

@ashleyheath ashleyheath requested a review from a team as a code owner December 14, 2023 16:37
Copy link

Coverage report

The coverage rate went from 99.33% to 99.33% ⬆️
The branch rate is 98%.

100% of new lines are covered.

Diff Coverage details (click to unfold)

procrastinate/retry.py

100% of new lines are covered (100% of the complete file).

procrastinate/tasks.py

100% of new lines are covered (100% of the complete file).

procrastinate/worker.py

100% of new lines are covered (100% of the complete file).

Copy link
Member

@ewjoachim ewjoachim left a 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).

9f5e5be

Comment on lines 168 to 173
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
Copy link
Member

@ewjoachim ewjoachim Dec 16, 2023

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):
Copy link
Member

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
Copy link
Member

@ewjoachim ewjoachim Dec 16, 2023

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

@ewjoachim
Copy link
Member

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!

@ashleyheath
Copy link
Contributor Author

Morning @ewjoachim! Your changes make perfect sense; very happy for you to merge with your additions

@ewjoachim ewjoachim merged commit 49c43ae into procrastinate-org:main Dec 21, 2023
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Worker.run_job() not resiliant to asyncio cancellation
2 participants