diff --git a/content/contributing/development/coding_guidelines.rst b/content/contributing/development/coding_guidelines.rst index 07a7c17ab..09d3a8f92 100644 --- a/content/contributing/development/coding_guidelines.rst +++ b/content/contributing/development/coding_guidelines.rst @@ -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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/content/developer/reference/backend/actions.rst b/content/developer/reference/backend/actions.rst index 1f3858ef0..e8a90637e 100644 --- a/content/developer/reference/backend/actions.rst +++ b/content/developer/reference/backend/actions.rst @@ -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 -------- diff --git a/content/developer/reference/backend/orm.rst b/content/developer/reference/backend/orm.rst index d16537969..695ff1f55 100644 --- a/content/developer/reference/backend/orm.rst +++ b/content/developer/reference/backend/orm.rst @@ -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 diff --git a/content/developer/reference/backend/orm/changelog.rst b/content/developer/reference/backend/orm/changelog.rst index 6e26fd97d..13386ea78 100644 --- a/content/developer/reference/backend/orm/changelog.rst +++ b/content/developer/reference/backend/orm/changelog.rst @@ -4,6 +4,11 @@ Changelog ========= +Odoo Online version 18.3 +======================== + +- New CRON API. TODO + Odoo Online version 18.2 ========================