Skip to content

[19.0] [FIX] queue_job: set date_cancelled on cancelled dependent jobs - #1015

Open
ivantodorovich wants to merge 1 commit into
OCA:19.0from
camptocamp:19.0-fix-queue_job-cancel-dependent-date
Open

ivantodorovich wants to merge 1 commit into
OCA:19.0from
camptocamp:19.0-fix-queue_job-cancel-dependent-date

Conversation

@ivantodorovich

@ivantodorovich ivantodorovich commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor
  • Cancellation now cascades. Cancelling a job used to cancel only its direct dependents: in A → B → C, cancelling A left C in wait_dependencies forever.
    Test: test_button_cancel_dependencies_cascade
  • A job is cancelled as soon as one of its dependencies is cancelled. It used to be cancelled only when all of them were, so a job with one cancelled and one done (or running) dependency waited forever.
    Tests: test_button_cancel_dependencies_other_parent_done, test_button_done_dependencies_other_parent_cancelled
  • Cancelled dependents get a date_cancelled, the same as the cancelled job. It was left empty, so the autovacuum never removed them.
    Tests: test_button_cancel_dependencies, test_button_cancel_dependencies_cascade
  • Cancelled dependents get a result telling why they were cancelled.
    Tests: test_button_cancel_dependencies_cascade, test_button_cancel_dependencies_selected
  • Cancelling a job along with its dependent cancels the dependent once. It used to be cancelled a second time as part of the selection, overwriting its date and result.
    Test: test_button_cancel_dependencies_selected

_get_common_dependent_jobs_query is replaced by the searchable dependency_ids / dependent_ids computed fields on queue.job, used to enqueue and cancel dependent jobs through the ORM.
Test: test_search_dependencies

@OCA-git-bot

Copy link
Copy Markdown
Contributor

Hi @sbidoul, @guewen,
some modules you are maintaining are being modified, check this out!

@OCA-git-bot OCA-git-bot added series:19.0 mod:queue_job Module queue_job mod:test_queue_job Module test_queue_job labels Oct 20, 2026

@florentx florentx left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use same date_cancelled as parent job, and do it a little bit differently:

--- a/queue_job/job.py
+++ b/queue_job/job.py
@@ -545,10 +545,10 @@ class Job:
             finally:
                 self._env = env
 
-    def _get_common_dependent_jobs_query(self):
-        return """
+    def _get_common_dependent_jobs_query(self, sql_set_clause):
+        return f"""
             UPDATE queue_job
-            SET state = %s
+            SET {sql_set_clause}
             FROM (
             SELECT child.id, array_agg(parent.state) as parent_states
             FROM queue_job job
@@ -579,13 +579,15 @@ class Job:
         return any(self.__reverse_depends_on_uuids)
 
     def enqueue_waiting(self):
-        sql = self._get_common_dependent_jobs_query()
+        sql = self._get_common_dependent_jobs_query("state = %s")
         self.env.cr.execute(sql, (PENDING, self.uuid, DONE, WAIT_DEPENDENCIES))
         self.env["queue.job"].invalidate_model(["state"])
 
     def cancel_dependent_jobs(self):
-        sql = self._get_common_dependent_jobs_query()
-        self.env.cr.execute(sql, (CANCELLED, self.uuid, CANCELLED, WAIT_DEPENDENCIES))
+        assert self.state == CANCELLED, "Job is not cancelled"
+        sql = self._get_common_dependent_jobs_query("state = %s, date_cancelled = %s")
+        sql_args = (CANCELLED, self.date_cancelled, self.uuid, CANCELLED, WAIT_DEPENDENCIES)
+        self.env.cr.execute(sql, sql_args)
         self.env["queue.job"].invalidate_model(["state"])
 
     def store(self):

@ivantodorovich
ivantodorovich force-pushed the 19.0-fix-queue_job-cancel-dependent-date branch from 61d0bd3 to 6063e7d Compare October 22, 2026 13:38
@ivantodorovich
ivantodorovich force-pushed the 19.0-fix-queue_job-cancel-dependent-date branch from 6063e7d to f073767 Compare October 22, 2026 13:53
@ivantodorovich

Copy link
Copy Markdown
Contributor Author

Hi @florentx

I digged a bit deeper and found other issues with the current implementation, all described in the PR description and covered by new unit tests. So I decided to refactor and use the orm and computed fields instead of this query.

@ivantodorovich
ivantodorovich force-pushed the 19.0-fix-queue_job-cancel-dependent-date branch from f073767 to 7127cc5 Compare October 22, 2026 14:03
- Cancelling a job now cancels its dependents all the way down. Before,
  only direct dependents were cancelled: in A -> B -> C, cancelling A
  left C waiting forever.
  Test: test_button_cancel_dependencies_cascade
- A job is cancelled as soon as one of its dependencies is cancelled.
  Before, it was only cancelled when all of them were, so a job with one
  cancelled and one done (or running) dependency waited forever.
  Tests: test_button_cancel_dependencies_other_parent_done,
  test_button_done_dependencies_other_parent_cancelled
- Cancelled dependents get a date_cancelled, the same as the cancelled
  job. It was left empty, so the autovacuum never removed them.
  Tests: test_button_cancel_dependencies,
  test_button_cancel_dependencies_cascade
- Cancelled dependents get a result telling why they were cancelled.
  Tests: test_button_cancel_dependencies_cascade,
  test_button_cancel_dependencies_selected
- Cancelling a job along with its dependent cancels the dependent once.
  Before, it was cancelled a second time as part of the selection,
  overwriting its date and result.
  Test: test_button_cancel_dependencies_selected

_get_common_dependent_jobs_query is replaced by the searchable
dependency_ids and dependent_ids computed fields on queue.job, used to
enqueue and cancel dependent jobs through the ORM.
Test: test_search_dependencies
@ivantodorovich
ivantodorovich force-pushed the 19.0-fix-queue_job-cancel-dependent-date branch from 7127cc5 to a472979 Compare October 22, 2026 14:33
Comment thread queue_job/job.py
A dependent job can no longer run once one of its dependencies is
cancelled, whatever the state of the others.
"""
dependents = self.db_record().dependent_ids

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sudo() needed for multicompany ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method run in the env of the job's user, which may not have access to queue.job.
db_record does the same for the same reason:

@divad1196 divad1196 Oct 23, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought that sudo() was missing here in fact.
But I didn't know that db_records called db_records_from_uuids which did the sudo().

So all good

Comment thread queue_job/job.py
) jobs
WHERE
queue_job.id = jobs.id
AND %s = ALL(jobs.parent_states)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is changing:
Before: We cancel a job only if all its parents were cancelled
After: We cancel all the dependents immediately

This has an impact when a jobs X depends on 2 other jobs Y and Z.

Lost benefit

Before: if we cancel Y and requeue it before cancelling Z, then X was not cancelled
After: X is cancelled when Y is cancelled.

This can be useful when things are stuck IMO

Fixed bug

If Y was cancelled but Z ran, then X would never be cancelled and never run -> stuck

2 options

Accept the loss

If we are fine losing the benefit, we can go this way

Keep both benefits

We should cancel X once:

  • All parents have been handled (cancelled, done)
  • at least one parent is cancelled

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixing the bug is precisely my intention here.
Why would you cancel before requeuing? I don't think we loose anything here

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the potential reason:

  • cancelled by mistake
  • batch cancel + selective requeue
  • attempt to unstuck a job ?

IMO, it's not a big loss, we can go with that,
but it's worth mentioning that the behavior is changing if someone is wondering.

Comment thread queue_job/job.py
),
}
)
dependents = dependents.dependent_ids

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to add the recursion.

NOTE: the The Power of 10 would suggest to define a max number of iteration, in particular when we don't ensure no cycle ever happen.

How deep do we expect to go? If considerably deep: SQL recursioin would be better.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It always ends. Each pass cancels every job it keeps, and only waiting jobs are kept, so each job is handled at most once. That holds even with a cycle, e.g: a.on_done(b); b.on_done(a), cancelling one cancels the other, then the loop stops.
The depth is the graph's depth, so I wouldn't enforce a maximum. Anyways, in practice, it's never that deep.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In theory yes,
but nobody writes a recursion thinking it will never end and all it cost is to replace while dependents with

for _ in range(MAX_DEPTH):
    if not dependents:
        break

I won't push on this if you want to keep it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants