Skip to content

Commit 9a1ef7e

Browse files
authored
Merge pull request #527 from OCA/16.0
Syncing from upstream OCA/queue (16.0)
2 parents 06dd044 + 9f5bf99 commit 9a1ef7e

File tree

17 files changed

+292
-150
lines changed

17 files changed

+292
-150
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@ addon | version | maintainers | summary
2323
--- | --- | --- | ---
2424
[base_export_async](base_export_async/) | 16.0.1.2.0 | | Asynchronous export with job queue
2525
[base_import_async](base_import_async/) | 16.0.1.2.1 | | Import CSV files in the background
26-
[queue_job](queue_job/) | 16.0.2.12.0 | <a href='https://github.com/guewen'><img src='https://github.com/guewen.png' width='32' height='32' style='border-radius:50%;' alt='guewen'/></a> | Job Queue
26+
[queue_job](queue_job/) | 16.0.2.13.0 | <a href='https://github.com/guewen'><img src='https://github.com/guewen.png' width='32' height='32' style='border-radius:50%;' alt='guewen'/></a> <a href='https://github.com/sbidoul'><img src='https://github.com/sbidoul.png' width='32' height='32' style='border-radius:50%;' alt='sbidoul'/></a> | Job Queue
2727
[queue_job_batch](queue_job_batch/) | 16.0.1.0.1 | | Job Queue Batch
2828
[queue_job_cron](queue_job_cron/) | 16.0.2.1.0 | | Scheduled Actions as Queue Jobs
2929
[queue_job_cron_jobrunner](queue_job_cron_jobrunner/) | 16.0.1.1.0 | <a href='https://github.com/ivantodorovich'><img src='https://github.com/ivantodorovich.png' width='32' height='32' style='border-radius:50%;' alt='ivantodorovich'/></a> | Run jobs without a dedicated JobRunner
3030
[queue_job_subscribe](queue_job_subscribe/) | 16.0.1.1.0 | | Control which users are subscribed to queue job notifications
3131
[queue_job_web_notify](queue_job_web_notify/) | 16.0.1.0.0 | | This module allows to display a notification to the related user of a failed job. It uses the web_notify notification feature.
32-
[test_queue_job](test_queue_job/) | 16.0.2.4.0 | | Queue Job Tests
32+
[test_queue_job](test_queue_job/) | 16.0.2.5.0 | <a href='https://github.com/sbidoul'><img src='https://github.com/sbidoul.png' width='32' height='32' style='border-radius:50%;' alt='sbidoul'/></a> | Queue Job Tests
3333
[test_queue_job_batch](test_queue_job_batch/) | 16.0.1.0.0 | | Test Job Queue Batch
3434

3535

queue_job/README.rst

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Job Queue
1111
!! This file is generated by oca-gen-addon-readme !!
1212
!! changes will be overwritten. !!
1313
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
14-
!! source digest: sha256:b92d06dbbf161572f2bf02e0c6a59282cea11cc5e903378094bead986f0125de
14+
!! source digest: sha256:6846860017b2a564dba3eb31b31d701c7c08c9f2bda739e3e0c3a03e07ef22f9
1515
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
1616
1717
.. |badge1| image:: https://img.shields.io/badge/maturity-Mature-brightgreen.png
@@ -697,10 +697,13 @@ promote its widespread use.
697697
.. |maintainer-guewen| image:: https://github.com/guewen.png?size=40px
698698
:target: https://github.com/guewen
699699
:alt: guewen
700+
.. |maintainer-sbidoul| image:: https://github.com/sbidoul.png?size=40px
701+
:target: https://github.com/sbidoul
702+
:alt: sbidoul
700703
701-
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
704+
Current `maintainers <https://odoo-community.org/page/maintainer-role>`__:
702705
703-
|maintainer-guewen|
706+
|maintainer-guewen| |maintainer-sbidoul|
704707
705708
This module is part of the `OCA/queue <https://github.com/OCA/queue/tree/16.0/queue_job>`_ project on GitHub.
706709

queue_job/__manifest__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
{
44
"name": "Job Queue",
5-
"version": "16.0.2.12.0",
5+
"version": "16.0.2.13.0",
66
"author": "Camptocamp,ACSONE SA/NV,Odoo Community Association (OCA)",
77
"website": "https://github.com/OCA/queue",
88
"license": "LGPL-3",
@@ -29,7 +29,7 @@
2929
},
3030
"installable": True,
3131
"development_status": "Mature",
32-
"maintainers": ["guewen"],
32+
"maintainers": ["guewen", "sbidoul"],
3333
"post_init_hook": "post_init_hook",
3434
"post_load": "post_load",
3535
}

queue_job/controllers/main.py

Lines changed: 78 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,48 @@
2626

2727

2828
class RunJobController(http.Controller):
29-
def _try_perform_job(self, env, job):
30-
"""Try to perform the job."""
29+
@classmethod
30+
def _acquire_job(cls, env: api.Environment, job_uuid: str) -> Job | None:
31+
"""Acquire a job for execution.
32+
33+
- make sure it is in ENQUEUED state
34+
- mark it as STARTED and commit the state change
35+
- acquire the job lock
36+
37+
If successful, return the Job instance, otherwise return None. This
38+
function may fail to acquire the job is not in the expected state or is
39+
already locked by another worker.
40+
"""
41+
env.cr.execute(
42+
"SELECT uuid FROM queue_job WHERE uuid=%s AND state=%s "
43+
"FOR NO KEY UPDATE SKIP LOCKED",
44+
(job_uuid, ENQUEUED),
45+
)
46+
if not env.cr.fetchone():
47+
_logger.warning(
48+
"was requested to run job %s, but it does not exist, "
49+
"or is not in state %s, or is being handled by another worker",
50+
job_uuid,
51+
ENQUEUED,
52+
)
53+
return None
54+
job = Job.load(env, job_uuid)
55+
assert job and job.state == ENQUEUED
3156
job.set_started()
3257
job.store()
3358
env.cr.commit()
34-
job.lock()
59+
if not job.lock():
60+
_logger.warning(
61+
"was requested to run job %s, but it could not be locked",
62+
job_uuid,
63+
)
64+
return None
65+
return job
3566

67+
@classmethod
68+
def _try_perform_job(cls, env, job):
69+
"""Try to perform the job, mark it done and commit if successful."""
3670
_logger.debug("%s started", job)
37-
3871
job.perform()
3972
# Triggers any stored computed fields before calling 'set_done'
4073
# so that will be part of the 'exec_time'
@@ -45,18 +78,20 @@ def _try_perform_job(self, env, job):
4578
env.cr.commit()
4679
_logger.debug("%s done", job)
4780

48-
def _enqueue_dependent_jobs(self, env, job):
81+
@classmethod
82+
def _enqueue_dependent_jobs(cls, env, job):
4983
tries = 0
5084
while True:
5185
try:
52-
job.enqueue_waiting()
86+
with job.env.cr.savepoint():
87+
job.enqueue_waiting()
5388
except OperationalError as err:
5489
# Automatically retry the typical transaction serialization
5590
# errors
5691
if err.pgcode not in PG_CONCURRENCY_ERRORS_TO_RETRY:
5792
raise
5893
if tries >= DEPENDS_MAX_TRIES_ON_CONCURRENCY_FAILURE:
59-
_logger.info(
94+
_logger.error(
6095
"%s, maximum number of tries reached to update dependencies",
6196
errorcodes.lookup(err.pgcode),
6297
)
@@ -74,17 +109,8 @@ def _enqueue_dependent_jobs(self, env, job):
74109
else:
75110
break
76111

77-
@http.route(
78-
"/queue_job/runjob",
79-
type="http",
80-
auth="none",
81-
save_session=False,
82-
readonly=False,
83-
)
84-
def runjob(self, db, job_uuid, **kw):
85-
http.request.session.db = db
86-
env = http.request.env(user=SUPERUSER_ID)
87-
112+
@classmethod
113+
def _runjob(cls, env: api.Environment, job: Job) -> None:
88114
def retry_postpone(job, message, seconds=None):
89115
job.env.clear()
90116
with registry(job.env.cr.dbname).cursor() as new_cr:
@@ -93,26 +119,9 @@ def retry_postpone(job, message, seconds=None):
93119
job.set_pending(reset_retry=False)
94120
job.store()
95121

96-
# ensure the job to run is in the correct state and lock the record
97-
env.cr.execute(
98-
"SELECT state FROM queue_job WHERE uuid=%s AND state=%s FOR UPDATE",
99-
(job_uuid, ENQUEUED),
100-
)
101-
if not env.cr.fetchone():
102-
_logger.warning(
103-
"was requested to run job %s, but it does not exist, "
104-
"or is not in state %s",
105-
job_uuid,
106-
ENQUEUED,
107-
)
108-
return ""
109-
110-
job = Job.load(env, job_uuid)
111-
assert job and job.state == ENQUEUED
112-
113122
try:
114123
try:
115-
self._try_perform_job(env, job)
124+
cls._try_perform_job(env, job)
116125
except OperationalError as err:
117126
# Automatically retry the typical transaction serialization
118127
# errors
@@ -141,7 +150,6 @@ def retry_postpone(job, message, seconds=None):
141150
# traceback in the logs we should have the traceback when all
142151
# retries are exhausted
143152
env.cr.rollback()
144-
return ""
145153

146154
except (FailedJobError, Exception) as orig_exception:
147155
buff = StringIO()
@@ -151,19 +159,18 @@ def retry_postpone(job, message, seconds=None):
151159
job.env.clear()
152160
with registry(job.env.cr.dbname).cursor() as new_cr:
153161
job.env = job.env(cr=new_cr)
154-
vals = self._get_failure_values(job, traceback_txt, orig_exception)
162+
vals = cls._get_failure_values(job, traceback_txt, orig_exception)
155163
job.set_failed(**vals)
156164
job.store()
157165
buff.close()
158166
raise
159167

160168
_logger.debug("%s enqueue depends started", job)
161-
self._enqueue_dependent_jobs(env, job)
169+
cls._enqueue_dependent_jobs(env, job)
162170
_logger.debug("%s enqueue depends done", job)
163171

164-
return ""
165-
166-
def _get_failure_values(self, job, traceback_txt, orig_exception):
172+
@classmethod
173+
def _get_failure_values(cls, job, traceback_txt, orig_exception):
167174
"""Collect relevant data from exception."""
168175
exception_name = orig_exception.__class__.__name__
169176
if hasattr(orig_exception, "__module__"):
@@ -177,6 +184,22 @@ def _get_failure_values(self, job, traceback_txt, orig_exception):
177184
"exc_message": exc_message,
178185
}
179186

187+
@http.route(
188+
"/queue_job/runjob",
189+
type="http",
190+
auth="none",
191+
save_session=False,
192+
readonly=False,
193+
)
194+
def runjob(self, db, job_uuid, **kw):
195+
http.request.session.db = db
196+
env = http.request.env(user=SUPERUSER_ID)
197+
job = self._acquire_job(env, job_uuid)
198+
if not job:
199+
return ""
200+
self._runjob(env, job)
201+
return ""
202+
180203
# flake8: noqa: C901
181204
@http.route("/queue_job/create_test_job", type="http", auth="user")
182205
def create_test_job(
@@ -187,6 +210,7 @@ def create_test_job(
187210
description="Test job",
188211
size=1,
189212
failure_rate=0,
213+
job_duration=0,
190214
):
191215
"""Create test jobs
192216
@@ -207,6 +231,12 @@ def create_test_job(
207231
except (ValueError, TypeError):
208232
failure_rate = 0
209233

234+
if job_duration is not None:
235+
try:
236+
job_duration = float(job_duration)
237+
except (ValueError, TypeError):
238+
job_duration = 0
239+
210240
if not (0 <= failure_rate <= 1):
211241
raise BadRequest("failure_rate must be between 0 and 1")
212242

@@ -235,6 +265,7 @@ def create_test_job(
235265
channel=channel,
236266
description=description,
237267
failure_rate=failure_rate,
268+
job_duration=job_duration,
238269
)
239270

240271
if size > 1:
@@ -245,6 +276,7 @@ def create_test_job(
245276
channel=channel,
246277
description=description,
247278
failure_rate=failure_rate,
279+
job_duration=job_duration,
248280
)
249281
return ""
250282

@@ -256,6 +288,7 @@ def _create_single_test_job(
256288
description="Test job",
257289
size=1,
258290
failure_rate=0,
291+
job_duration=0,
259292
):
260293
delayed = (
261294
http.request.env["queue.job"]
@@ -265,7 +298,7 @@ def _create_single_test_job(
265298
channel=channel,
266299
description=description,
267300
)
268-
._test_job(failure_rate=failure_rate)
301+
._test_job(failure_rate=failure_rate, job_duration=job_duration)
269302
)
270303
return "job uuid: %s" % (delayed.db_record().uuid,)
271304

@@ -279,6 +312,7 @@ def _create_graph_test_jobs(
279312
channel=None,
280313
description="Test job",
281314
failure_rate=0,
315+
job_duration=0,
282316
):
283317
model = http.request.env["queue.job"]
284318
current_count = 0
@@ -301,7 +335,7 @@ def _create_graph_test_jobs(
301335
max_retries=max_retries,
302336
channel=channel,
303337
description="%s #%d" % (description, current_count),
304-
)._test_job(failure_rate=failure_rate)
338+
)._test_job(failure_rate=failure_rate, job_duration=job_duration)
305339
)
306340

307341
grouping = random.choice(possible_grouping_methods)

queue_job/job.py

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ def load_many(cls, env, job_uuids):
236236
recordset = cls.db_records_from_uuids(env, job_uuids)
237237
return {cls._load_from_db_record(record) for record in recordset}
238238

239-
def add_lock_record(self):
239+
def add_lock_record(self) -> None:
240240
"""
241241
Create row in db to be locked while the job is being performed.
242242
"""
@@ -256,13 +256,11 @@ def add_lock_record(self):
256256
[self.uuid],
257257
)
258258

259-
def lock(self):
260-
"""
261-
Lock row of job that is being performed
259+
def lock(self) -> bool:
260+
"""Lock row of job that is being performed.
262261
263-
If a job cannot be locked,
264-
it means that the job wasn't started,
265-
a RetryableJobError is thrown.
262+
Return False if a job cannot be locked: it means that the job is not in
263+
STARTED state or is already locked by another worker.
266264
"""
267265
self.env.cr.execute(
268266
"""
@@ -278,18 +276,15 @@ def lock(self):
278276
queue_job
279277
WHERE
280278
uuid = %s
281-
AND state='started'
279+
AND state = %s
282280
)
283-
FOR UPDATE;
281+
FOR NO KEY UPDATE SKIP LOCKED;
284282
""",
285-
[self.uuid],
283+
[self.uuid, STARTED],
286284
)
287285

288286
# 1 job should be locked
289-
if 1 != len(self.env.cr.fetchall()):
290-
raise RetryableJobError(
291-
f"Trying to lock job that wasn't started, uuid: {self.uuid}"
292-
)
287+
return bool(self.env.cr.fetchall())
293288

294289
@classmethod
295290
def _load_from_db_record(cls, job_db_record):

0 commit comments

Comments
 (0)