Skip to content

Commit 018331b

Browse files
committed
Add 'Run in new cursor' option on job functions
It is forbidden to commit inside a job, because it releases the job lock and can cause it to start again, while still being run, by the dead jobs requeuer. For some use cases, it may actually be legitimate, or at least be needed in the short term before actual updates in the code. A new option on the job function, false by default, allow to run the job in a new transaction, at the cost of an additional connection + transaction overhead. Related to #889
1 parent 283136f commit 018331b

6 files changed

Lines changed: 38 additions & 9 deletions

File tree

queue_job/controllers/main.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ def _prevent_commit(cr):
3838
def forbidden_commit(*args, **kwargs):
3939
raise RuntimeError(
4040
"Commit is forbidden in queue jobs. "
41-
"If the current job is a cron running as queue job, "
42-
"modify it to run as a normal cron."
41+
'You may want to enable the "Run in new cursor" option on the Job '
42+
"Function. Alternatively, if the current job is a cron running as "
43+
"queue job, you can modify it to run as a normal cron."
4344
)
4445

4546
original_commit = cr.commit

queue_job/job.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import sys
99
import uuid
1010
import weakref
11+
from contextlib import contextmanager, nullcontext
1112
from datetime import datetime, timedelta
1213
from random import randint
1314

@@ -406,10 +407,6 @@ def __init__(
406407
self.method_name = func.__name__
407408
self.recordset = recordset
408409

409-
self.env = env
410-
self.job_model = self.env["queue.job"]
411-
self.job_model_name = "queue.job"
412-
413410
self.job_config = (
414411
self.env["queue.job.function"].sudo().job_config(self.job_function_name)
415412
)
@@ -487,7 +484,12 @@ def perform(self):
487484
"""
488485
self.retry += 1
489486
try:
490-
self.result = self.func(*tuple(self.args), **self.kwargs)
487+
if self.job_config.run_in_new_cursor:
488+
env_context_manager = self._with_temporary_env()
489+
else:
490+
env_context_manager = nullcontext()
491+
with env_context_manager:
492+
self.result = self.func(*tuple(self.args), **self.kwargs)
491493
except RetryableJobError as err:
492494
if err.ignore_retry:
493495
self.retry -= 1
@@ -507,6 +509,14 @@ def perform(self):
507509

508510
return self.result
509511

512+
@contextmanager
513+
def _with_temporary_env(self):
514+
with self.env.registry.cursor() as new_cr:
515+
env = self.recordset.env
516+
self.recordset = self.recordset.with_env(env(cr=new_cr))
517+
yield
518+
self.recordset = self.recordset.with_env(env)
519+
510520
def _get_common_dependent_jobs_query(self):
511521
return """
512522
UPDATE queue_job
@@ -665,6 +675,10 @@ def __hash__(self):
665675
def db_record(self):
666676
return self.db_records_from_uuids(self.env, [self.uuid])
667677

678+
@property
679+
def env(self):
680+
return self.recordset.env
681+
668682
@property
669683
def func(self):
670684
recordset = self.recordset.with_context(job_uuid=self.uuid)

queue_job/models/queue_job_function.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ class QueueJobFunction(models.Model):
2828
"related_action_enable "
2929
"related_action_func_name "
3030
"related_action_kwargs "
31-
"job_function_id ",
31+
"job_function_id "
32+
"run_in_new_cursor",
3233
)
3334

3435
def _default_channel(self):
@@ -79,6 +80,13 @@ def _default_channel(self):
7980
"enable, func_name, kwargs.\n"
8081
"See the module description for details.",
8182
)
83+
run_in_new_cursor = fields.Boolean(
84+
string="Run in new cursor",
85+
help="When enabled, the job will be executed in a new database cursor. "
86+
"This is necessary when the job function needs to commit the transaction "
87+
"during its execution, at the cost of an additional database connection "
88+
"and transaction management overhead.",
89+
)
8290

8391
@api.depends("model_id.model", "method")
8492
def _compute_name(self):
@@ -149,6 +157,7 @@ def job_default_config(self):
149157
related_action_func_name=None,
150158
related_action_kwargs={},
151159
job_function_id=None,
160+
run_in_new_cursor=False,
152161
)
153162

154163
def _parse_retry_pattern(self):
@@ -184,6 +193,7 @@ def job_config(self, name):
184193
related_action_func_name=config.related_action.get("func_name"),
185194
related_action_kwargs=config.related_action.get("kwargs", {}),
186195
job_function_id=config.id,
196+
run_in_new_cursor=config.run_in_new_cursor,
187197
)
188198

189199
def _retry_pattern_format_error_message(self):

queue_job/tests/common.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ def _add_job(self, *args, **kwargs):
276276

277277
def _prepare_context(self, job):
278278
# pylint: disable=context-overridden
279-
job_model = job.job_model.with_context({})
279+
job_model = job.env["queue.job"].with_context({})
280280
field_records = job_model._fields["records"]
281281
# Filter the context to simulate store/load of the job
282282
job.recordset = field_records.convert_to_write(job.recordset, job_model)

queue_job/tests/test_model_job_function.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def test_function_job_config(self):
4242
' "func_name": "related_action_foo",'
4343
' "kwargs": {"b": 1}}'
4444
),
45+
"run_in_new_cursor": True,
4546
}
4647
)
4748
self.assertEqual(
@@ -53,5 +54,6 @@ def test_function_job_config(self):
5354
related_action_func_name="related_action_foo",
5455
related_action_kwargs={"b": 1},
5556
job_function_id=job_function.id,
57+
run_in_new_cursor=True,
5658
),
5759
)

queue_job/views/queue_job_function_views.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<field name="model_id" required="1" />
1111
<field name="method" required="1" />
1212
<field name="channel_id" />
13+
<field name="run_in_new_cursor" />
1314
<field name="edit_retry_pattern" widget="ace" />
1415
<field name="edit_related_action" widget="ace" />
1516
</group>
@@ -24,6 +25,7 @@
2425
<list>
2526
<field name="name" />
2627
<field name="channel_id" />
28+
<field name="run_in_new_cursor" />
2729
</list>
2830
</field>
2931
</record>

0 commit comments

Comments
 (0)