[IMP] base: ir.cron documentation

New API

task-4472661
This commit is contained in:
Krzysztof Magusiak (krma) 2025-01-31 16:42:01 +00:00
parent 57977d2a10
commit 6a4d0d9d98
4 changed files with 128 additions and 33 deletions

View File

@ -658,9 +658,15 @@ Never commit the transaction
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The Odoo framework is in charge of providing the transactional context for
all RPC calls. The principle is that a new database cursor is opened at the
beginning of each RPC call, and committed when the call has returned, just
before transmitting the answer to the RPC client, approximately like this:
all RPC calls.
All ``cr.commit()`` calls outside of the server framework from now on must
have an **explicit comment** explaining why they are absolutely necessary, why
they are indeed correct, and why they do not break the transactions. Otherwise
they can and will be removed!
The principle is that a new database cursor is opened at the beginning of each
RPC call, and committed when the call has returned, just before transmitting the
answer to the RPC client, approximately like this:
.. code-block:: python
@ -682,8 +688,8 @@ If any error occurs during the execution of the RPC call, the transaction is
rolled back atomically, preserving the state of the system.
Similarly, the system also provides a dedicated transaction during the execution
of tests suites, so it can be rolled back or not depending on the server
startup options.
of tests suites or scheduled actions, so it can be rolled back or not depending
on the server startup options.
The consequence is that if you manually call ``cr.commit()`` anywhere there is
a very high chance that you will break the system in various ways, because you
@ -697,9 +703,9 @@ among others:
during the transaction)
Here is the very simple rule:
You should **NEVER** call ``cr.commit()`` yourself, **UNLESS** you have
created your own database cursor explicitly! And the situations where you
need to do that are exceptional!
You should **NEVER** call ``cr.commit()`` or ``cr.rollback()`` yourself,
**UNLESS** you have created your own database cursor explicitly!
And the situations where you need to do that are exceptional!
And by the way if you did create your own cursor, then you need to handle
error cases and proper rollback, as well as properly close the cursor when
@ -707,20 +713,59 @@ Here is the very simple rule:
And contrary to popular belief, you do not even need to call ``cr.commit()``
in the following situations:
- in the ``_auto_init()`` method of an *models.Model* object: this is taken
care of by the addons initialization method, or by the ORM transaction when
creating custom models
care of by the addons initialization method, or by the ORM transaction when
creating custom models
- in reports: the ``commit()`` is handled by the framework too, so you can
update the database even from within a report
update the database even from within a report
- within *models.Transient* methods: these methods are called exactly like
regular *models.Model* ones, within a transaction and with the corresponding
``cr.commit()/rollback()`` at the end
regular *models.Model* ones, within a transaction and with the corresponding
``cr.commit()/rollback()`` at the end
- etc. (see general rule above if you are in doubt!)
All ``cr.commit()`` calls outside of the server framework from now on must
have an **explicit comment** explaining why they are absolutely necessary, why
they are indeed correct, and why they do not break the transactions. Otherwise
they can and will be removed !
Avoid catching exceptions
~~~~~~~~~~~~~~~~~~~~~~~~~
As ever, avoid catching too broadly.
You may catch too much and prevent error handling from being done properly and
uncaught exceptions are already logged or handled by the framework.
You should be specific of the types you are catching and handle them
accordingly and you should limit the scope of your try-catch block as much
as possible.
.. code-block:: python
# BAD CODE
try:
do_something()
except Exception as e:
# if we caught a ValidationError, we did not rollback and we left the
# ORM in an undefined state
_logger.warning(e)
If you are handling framework exceptions, you must at least use **savepoints**
to isolate your function as much as possible.
It will flush the computations when entering the block and rollback changes
properly in case of exceptions.
.. code-block:: python
try:
with self.env.cr.savepoint():
do_stuff()
except ...:
...
.. warning::
After you start more than 64 savepoints during a single transaction,
PostgreSQL will slow down.
If you process records and savepoint in a loop, for example when processing
records one by one for a batch, limit the size of the batch.
If you have more records, maybe the function should become a schedule job
or you have to accept the performance penalty.
Use translation method correctly
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -442,34 +442,78 @@ Actions triggered automatically on a predefined frequency.
Priority of the action when executing multiple actions at the same time
Advanced use: Batching
Writing cron functions
----------------------
When executing a scheduled action, it's recommended to try batching progress in order
to avoid hogging a worker for a long period of time and possibly running into timeout exceptions.
When executing a scheduled action, it's recommended to try batching progress in
order to avoid hogging a worker for a long period of time and possibly running
into timeout exceptions. Therefore you should split the processing so that each
call will progress in the work to be done.
Odoo provides a simple API for scheduled action batching;
It is the responsiblity of the framework to call the function as many times as
needed to process remaining work.
When writing a such a function, you should focus on processing a single batch.
A batch should process one or many records and should generally take no more
than *a few seconds*. Work will be committed by the framework after each batch.
.. code-block:: python
self.env['ir.cron']._notify_progress(done=XX:int, remaining=XX:int)
@api.cron('xml_id...')
def _cron_do_something(self, limit=300): # some default batch limit, allows for tweaking
domain = [('state', '=', 'ready')]
records = self.search(domain, limit=limit)
records.do_something()
# notify progression
remaining = 0 if len(records) == limit else self.search_count(domain)
self.env.cron_progress(done=len(records), remaining=remaining)
This method allows the scheduler to know if progress was made and whether there is
still remaining work that must be done.
By default, if the API is used, the scheduler tries to process 10 batches in one sitting.
If there are still remaining tasks after those 10 batches, a new cron call will be executed as
soon as possible.
Advanced use: Triggers
----------------------
For more complex use cases, Odoo provides a more advanced way to trigger
scheduled actions directly from business code.
There are some cases, where you may still want to keep resources between cron calls.
In such a case, the ``@api.cron`` decorator understands yielding work that should
be processed in isolation.
The scheduler is still responsible of scheduling the work and stopping when needed.
The following shows how to commit after each processed record while keeping an
open connection.
.. code-block:: python
action_record._trigger(at=XX:date)
@api.cron('xml_id...')
def _cron_do_something(self):
# limit is less important as we already give back control to the scheduler
domain = [('state', '=', 'ready')]
records = self.search(domain)
self.env.cron_progress(remaining=len(records))
with open_some_connection() as conn:
def process_record(record):
# with_prefetch: keep prefetch to the single processed value
# exists: record may disappear while looping and committing
# if possible, we could lock the record for update
# filtered_domain: record may have changed
record = record.with_prefetch().exists().filtered_domain(domain)
if record:
record.do_something(conn)
self.env.cron_progress(1)
for record in records:
try:
yield lambda: process_record(record)
except Exception:
raise # you may catch it an continue safely
Running cron functions
----------------------
There are two ways to run functions: immediate (in current thread) or trigger
to start at a given time or as soon as possible.
.. code-block:: python
# run now (still in a separate transaction)
cron_record.method_direct_trigger()
# trigger
self._cron_do_something.trigger()
Security
--------

View File

@ -649,7 +649,7 @@ Method decorators
=================
.. automodule:: odoo.api
:members: depends, depends_context, constrains, onchange, autovacuum, model, model_create_multi, private, ondelete
:members: depends, depends_context, constrains, onchange, autovacuum, cron, model, model_create_multi, private, ondelete
.. .. currentmodule:: odoo.api
@ -659,6 +659,7 @@ Method decorators
.. .. autodata:: constrains
.. .. autodata:: onchange
.. .. autodata:: autovacuum
.. .. autodata:: cron
.. todo:: With sphinx 2.0 : autodecorator

View File

@ -4,6 +4,11 @@
Changelog
=========
Odoo Online version 18.3
========================
- New CRON API. TODO
Odoo Online version 18.2
========================