diff options
| author | Julien Castiaux <juc@odoo.com> | 2020-03-17 11:58:14 +0000 |
|---|---|---|
| committer | Raphael Collet <rco@odoo.com> | 2020-05-19 13:38:19 +0000 |
| commit | c5d3a109f530cd0763a1c3860086b5b7abef1b64 (patch) | |
| tree | 559f2f3e61c0e54ca40c25a7740e6dff2a768b9f | |
| parent | 64873d225ede01eb3116773b2ff0aed278ff0e6a (diff) | |
[REF] ir.autovacuum: declarative garbage collector registration
The ir.autovacuum model purpose is to run several garbage collecting
operations like removing files from the filestore when no attachment
references them anymore.
The precedent strategy to register new garbage collection tasks was to
override the `power_on` method and to imperatively execute a vacuum
cleaning method on a given model. All calls were executed in a single
SQL transaction without any error handling, meaning a single fail during
any call resulted in a complete failure of the entire vacuum cleaning
chain.
We introduce a new `@autovacuum` api decorator, its purpose it to
register garbage collecting methods that will be safely executed in
their own transaction by the vacuum cleaner. In order to ensure this
new strategy is used, we deprecate `power_on` extensions.
By the way, garbage-collecting methods can be quite heavy and we don't
want users to directly call them. We now ensure they are private.
closes odoo/odoo#47842
Task: 2154079
Signed-off-by: Raphael Collet (rco) <rco@openerp.com>
Co-authored-by: Raphael Collet <rco@odoo.com>
Co-authored-by: Olivier Dony <odo@odoo.com>
29 files changed, 159 insertions, 220 deletions
diff --git a/addons/bus/models/__init__.py b/addons/bus/models/__init__.py index dee2676aa52..5f99c824cac 100644 --- a/addons/bus/models/__init__.py +++ b/addons/bus/models/__init__.py @@ -3,4 +3,3 @@ from . import bus from . import bus_presence from . import res_users from . import res_partner -from . import ir_autovacuum
\ No newline at end of file diff --git a/addons/bus/models/bus.py b/addons/bus/models/bus.py index 7e0831fe9b7..1e62ad6eb48 100644 --- a/addons/bus/models/bus.py +++ b/addons/bus/models/bus.py @@ -37,8 +37,8 @@ class ImBus(models.Model): channel = fields.Char('Channel') message = fields.Char('Message') - @api.model - def gc(self): + @api.autovacuum + def _gc_messages(self): timeout_ago = datetime.datetime.utcnow()-datetime.timedelta(seconds=TIMEOUT*2) domain = [('create_date', '<', timeout_ago.strftime(DEFAULT_SERVER_DATETIME_FORMAT))] return self.sudo().search(domain).unlink() diff --git a/addons/bus/models/ir_autovacuum.py b/addons/bus/models/ir_autovacuum.py deleted file mode 100644 index feb0497ff96..00000000000 --- a/addons/bus/models/ir_autovacuum.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, models - -class AutoVacuum(models.AbstractModel): - _inherit = 'ir.autovacuum' - - @api.model - def power_on(self, *args, **kwargs): - self.env['bus.bus'].gc() - return super(AutoVacuum, self).power_on(*args, **kwargs) diff --git a/addons/event/models/__init__.py b/addons/event/models/__init__.py index 4aefa5d6068..6dcac1874dd 100644 --- a/addons/event/models/__init__.py +++ b/addons/event/models/__init__.py @@ -9,4 +9,3 @@ from . import event_tag from . import event_ticket from . import res_config_settings from . import res_partner -from . import ir_autovacuum diff --git a/addons/event/models/event_event.py b/addons/event/models/event_event.py index db3eaff8364..e5518122df8 100644 --- a/addons/event/models/event_event.py +++ b/addons/event/models/event_event.py @@ -496,3 +496,13 @@ class EventEvent(models.Model): result[event.id] = cal.serialize().encode('utf-8') return result + + @api.autovacuum + def _gc_mark_events_done(self): + """ move every ended events in the next 'ended stage' """ + ended_events = self.env['event.event'].search([ + ('date_end', '<', fields.Datetime.now()), + ('stage_id.pipe_end', '=', False), + ]) + if ended_events: + ended_events.action_set_done() diff --git a/addons/event/models/ir_autovacuum.py b/addons/event/models/ir_autovacuum.py deleted file mode 100644 index d58a788e6c8..00000000000 --- a/addons/event/models/ir_autovacuum.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, models, fields - - -class AutoVacuum(models.AbstractModel): - _inherit = 'ir.autovacuum' - - @api.model - def power_on(self, *args, **kwargs): - """ - Cron which drags all events which end date is < now (= passed) - into the first next (by sequence) stage defined as "Ended" - (if they are not already in an ended stage) - """ - ended_events = self.env['event.event'].search([('date_end', '<', fields.Datetime.now()), ('stage_id.pipe_end', '=', False)]) - - if ended_events: - ended_events.action_set_done() - - return super(AutoVacuum, self).power_on(*args, **kwargs) diff --git a/addons/im_livechat/models/__init__.py b/addons/im_livechat/models/__init__.py index f70bcc5f3c3..bc392a29c43 100644 --- a/addons/im_livechat/models/__init__.py +++ b/addons/im_livechat/models/__init__.py @@ -2,7 +2,6 @@ from . import res_users from . import res_partner from . import im_livechat_channel -from . import ir_autovacuum from . import mail_channel from . import rating from . import digest diff --git a/addons/im_livechat/models/ir_autovacuum.py b/addons/im_livechat/models/ir_autovacuum.py deleted file mode 100644 index 43877cf9944..00000000000 --- a/addons/im_livechat/models/ir_autovacuum.py +++ /dev/null @@ -1,12 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, models - -class AutoVacuum(models.AbstractModel): - _inherit = 'ir.autovacuum' - - @api.model - def power_on(self, *args, **kwargs): - self.env['mail.channel'].remove_empty_livechat_sessions() - self.env['mail.channel.partner'].unpin_old_livechat_sessions() - return super(AutoVacuum, self).power_on(*args, **kwargs) diff --git a/addons/im_livechat/models/mail_channel.py b/addons/im_livechat/models/mail_channel.py index dae1d70cb81..acadce91521 100644 --- a/addons/im_livechat/models/mail_channel.py +++ b/addons/im_livechat/models/mail_channel.py @@ -6,8 +6,8 @@ from odoo import api, fields, models, _ class ChannelPartner(models.Model): _inherit = 'mail.channel.partner' - @api.model - def unpin_old_livechat_sessions(self): + @api.autovacuum + def _gc_unpin_livechat_sessions(self): """ Unpin livechat sessions with no activity for at least one day to clean the operator's interface """ self.env.cr.execute(""" @@ -116,8 +116,8 @@ class MailChannel(models.Model): return self.anonymous_name return _("Visitor") - @api.model - def remove_empty_livechat_sessions(self): + @api.autovacuum + def _gc_empty_livechat_sessions(self): hours = 1 # never remove empty session created within the last hour self.env.cr.execute(""" SELECT id as id diff --git a/addons/mail/models/__init__.py b/addons/mail/models/__init__.py index 822e28d5e2b..38b80e6dc12 100644 --- a/addons/mail/models/__init__.py +++ b/addons/mail/models/__init__.py @@ -27,7 +27,6 @@ from . import update from . import ir_action_act_window from . import ir_actions from . import ir_attachment -from . import ir_autovacuum from . import ir_config_parameter from . import ir_http from . import ir_model diff --git a/addons/mail/models/ir_autovacuum.py b/addons/mail/models/ir_autovacuum.py deleted file mode 100644 index f3e561c5c41..00000000000 --- a/addons/mail/models/ir_autovacuum.py +++ /dev/null @@ -1,13 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import api, models - - -class AutoVacuum(models.AbstractModel): - _inherit = 'ir.autovacuum' - - @api.model - def power_on(self, *args, **kwargs): - self.env['mail.thread']._garbage_collect_attachments() - return super(AutoVacuum, self).power_on(*args, **kwargs) diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 349396ee0a4..5786a4ed357 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -478,25 +478,6 @@ class MailThread(models.AbstractModel): return self @api.model - def _garbage_collect_attachments(self): - """ Garbage collect lost mail attachments. Those are attachments - - linked to res_model 'mail.compose.message', the composer wizard - - with res_id 0, because they were created outside of an existing - wizard (typically user input through Chatter or reports - created on-the-fly by the templates) - - unused since at least one day (create_date and write_date) - """ - limit_date = datetime.datetime.utcnow() - datetime.timedelta(days=1) - limit_date_str = datetime.datetime.strftime(limit_date, tools.DEFAULT_SERVER_DATETIME_FORMAT) - self.env['ir.attachment'].search([ - ('res_model', '=', 'mail.compose.message'), - ('res_id', '=', 0), - ('create_date', '<', limit_date_str), - ('write_date', '<', limit_date_str)] - ).unlink() - return True - - @api.model def get_mail_message_access(self, res_ids, operation, model_name=None): """ mail.message check permission rules for related document. This method is meant to be inherited in order to implement addons-specific behavior. diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index 6c2e83baaec..f66bdba6557 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -516,3 +516,20 @@ class MailComposer(models.TransientModel): values[res_id] = res_id_values return multi_mode and values or values[res_ids[0]] + + @api.autovacuum + def _gc_lost_attachments(self): + """ Garbage collect lost mail attachments. Those are attachments + - linked to res_model 'mail.compose.message', the composer wizard + - with res_id 0, because they were created outside of an existing + wizard (typically user input through Chatter or reports + created on-the-fly by the templates) + - unused since at least one day (create_date and write_date) + """ + limit_date = fields.Datetime.subtract(fields.Datetime.now(), days=1) + self.env['ir.attachment'].search([ + ('res_model', '=', self._name), + ('res_id', '=', 0), + ('create_date', '<', limit_date), + ('write_date', '<', limit_date)] + ).unlink() diff --git a/addons/pos_mercury/models/pos_mercury.py b/addons/pos_mercury/models/pos_mercury.py index a95dba493d6..900e696f533 100644 --- a/addons/pos_mercury/models/pos_mercury.py +++ b/addons/pos_mercury/models/pos_mercury.py @@ -76,12 +76,3 @@ class PosOrder(models.Model): }) return fields - - -class AutoVacuum(models.AbstractModel): - _inherit = 'ir.autovacuum' - - @api.model - def power_on(self, *args, **kwargs): - self.env['pos_mercury.mercury_transaction'].cleanup_old_tokens() - return super(AutoVacuum, self).power_on(*args, **kwargs) diff --git a/addons/pos_mercury/models/pos_mercury_transaction.py b/addons/pos_mercury/models/pos_mercury_transaction.py index bb538285ecb..44ca5b89b23 100644 --- a/addons/pos_mercury/models/pos_mercury_transaction.py +++ b/addons/pos_mercury/models/pos_mercury_transaction.py @@ -114,8 +114,8 @@ class MercuryTransaction(models.Model): # One time (the ones we use) Vantiv tokens are required to be # deleted after 6 months - @api.model - def cleanup_old_tokens(self): + @api.autovacuum + def _gc_old_tokens(self): expired_creation_date = (date.today() - timedelta(days=6 * 30)).strftime(DEFAULT_SERVER_DATETIME_FORMAT) for order in self.env['pos.order'].search([('create_date', '<', expired_creation_date)]): diff --git a/addons/website_sale_coupon/models/__init__.py b/addons/website_sale_coupon/models/__init__.py index d31916a0b3c..28c756b4fed 100644 --- a/addons/website_sale_coupon/models/__init__.py +++ b/addons/website_sale_coupon/models/__init__.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import ir_autovacuum from . import sale_coupon_program from . import sale_order diff --git a/addons/website_sale_coupon/models/ir_autovacuum.py b/addons/website_sale_coupon/models/ir_autovacuum.py deleted file mode 100644 index 62b8b18cc48..00000000000 --- a/addons/website_sale_coupon/models/ir_autovacuum.py +++ /dev/null @@ -1,13 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import api, models - - -class AutoVacuum(models.AbstractModel): - _inherit = 'ir.autovacuum' - - @api.model - def power_on(self, *args, **kwargs): - self.env['sale.order']._garbage_collector(*args, **kwargs) - return super(AutoVacuum, self).power_on(*args, **kwargs) diff --git a/addons/website_sale_coupon/models/sale_order.py b/addons/website_sale_coupon/models/sale_order.py index 322a58d5322..08ff012371a 100644 --- a/addons/website_sale_coupon/models/sale_order.py +++ b/addons/website_sale_coupon/models/sale_order.py @@ -77,8 +77,8 @@ class SaleOrder(models.Model): free_shipping_product_ids = free_shipping_prgs_ids.mapped('discount_line_product_id') return self.order_line.filtered(lambda l: l.product_id in free_shipping_product_ids) - @api.model - def _garbage_collector(self, *args, **kwargs): + @api.autovacuum + def _gc_abandoned_coupons(self, *args, **kwargs): """Remove/free coupon from abandonned ecommerce order.""" ICP = self.env['ir.config_parameter'] validity = ICP.get_param('website_sale_coupon.abandonned_coupon_validity', 4) diff --git a/addons/website_sale_coupon/tests/test_shop_sale_coupon.py b/addons/website_sale_coupon/tests/test_shop_sale_coupon.py index aed73c9ebb1..2bc2e0ddef6 100644 --- a/addons/website_sale_coupon/tests/test_shop_sale_coupon.py +++ b/addons/website_sale_coupon/tests/test_shop_sale_coupon.py @@ -138,7 +138,7 @@ class TestWebsiteSaleCoupon(TransactionCase): self.assertEqual(self.coupon.state, 'used') # 3. Test recent order -> Should not be removed - order._garbage_collector() + order._gc_abandoned_coupons() self.assertEqual(len(order.applied_coupon_ids), 1, "The coupon shouldn't have been removed from the order no more than 4 days") self.assertEqual(self.coupon.state, 'used', "Should not have been changed") @@ -149,14 +149,14 @@ class TestWebsiteSaleCoupon(TransactionCase): order.flush() query = """UPDATE %s SET write_date = %%s WHERE id = %%s""" % (order._table,) self.env.cr.execute(query, (fields.Datetime.to_string(fields.datetime.now() - timedelta(days=4, hours=2)), order.id)) - order._garbage_collector() + order._gc_abandoned_coupons() self.assertEqual(len(order.applied_coupon_ids), 1, "The coupon shouldn't have been removed from the order the order is 4 days old but icp validity is 5 days") self.assertEqual(self.coupon.state, 'used', "Should not have been changed (2)") # 5. Test order with no ICP and older then 4 default days -> Should be removed icp_validity.unlink() - order._garbage_collector() + order._gc_abandoned_coupons() self.assertEqual(len(order.applied_coupon_ids), 0, "The coupon should've been removed from the order as more than 4 days") self.assertEqual(self.coupon.state, 'new', "Should have been reset.") diff --git a/addons/website_sale_wishlist/models/__init__.py b/addons/website_sale_wishlist/models/__init__.py index 18c7fb89feb..474c0adee9e 100644 --- a/addons/website_sale_wishlist/models/__init__.py +++ b/addons/website_sale_wishlist/models/__init__.py @@ -1,4 +1,3 @@ # -*- coding: utf-8 -*- -from . import ir_autovacuum from . import product_wishlist from . import res_users diff --git a/addons/website_sale_wishlist/models/ir_autovacuum.py b/addons/website_sale_wishlist/models/ir_autovacuum.py deleted file mode 100644 index acfa23c6ad9..00000000000 --- a/addons/website_sale_wishlist/models/ir_autovacuum.py +++ /dev/null @@ -1,13 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import api, models - - -class AutoVacuum(models.AbstractModel): - _inherit = 'ir.autovacuum' - - @api.model - def power_on(self, *args, **kwargs): - self.env['product.wishlist']._garbage_collector(*args, **kwargs) - return super(AutoVacuum, self).power_on(*args, **kwargs) diff --git a/addons/website_sale_wishlist/models/product_wishlist.py b/addons/website_sale_wishlist/models/product_wishlist.py index b7cbe5e884d..9439b5261d7 100644 --- a/addons/website_sale_wishlist/models/product_wishlist.py +++ b/addons/website_sale_wishlist/models/product_wishlist.py @@ -61,8 +61,8 @@ class ProductWishlist(models.Model): session_wishes.write({"partner_id": self.env.user.partner_id.id}) request.session.pop('wishlist_ids') - @api.model - def _garbage_collector(self, *args, **kwargs): + @api.autovacuum + def _gc_sessions(self, *args, **kwargs): """Remove wishlists for unexisting sessions.""" self.with_context(active_test=False).search([ ("create_date", "<", fields.Datetime.to_string(datetime.now() - timedelta(weeks=kwargs.get('wishlist_week', 5)))), diff --git a/doc/reference/orm.rst b/doc/reference/orm.rst index ddc5918553b..b6161c7f88b 100644 --- a/doc/reference/orm.rst +++ b/doc/reference/orm.rst @@ -531,7 +531,7 @@ Method decorators ================= .. automodule:: odoo.api - :members: depends, depends_context, constrains, onchange, returns, model, model_create_multi + :members: depends, depends_context, constrains, onchange, returns, autovacuum, model, model_create_multi .. .. currentmodule:: odoo.api @@ -540,6 +540,7 @@ Method decorators .. .. autodata:: constrains .. .. autodata:: onchange .. .. autodata:: returns +.. .. autodata:: autovacuum .. todo:: With sphinx 2.0 : autodecorator diff --git a/odoo/addons/base/data/ir_cron_data.xml b/odoo/addons/base/data/ir_cron_data.xml index 52e07855b63..fb9cd881dd0 100644 --- a/odoo/addons/base/data/ir_cron_data.xml +++ b/odoo/addons/base/data/ir_cron_data.xml @@ -4,7 +4,7 @@ <field name="name">Base: Auto-vacuum internal data</field> <field name="model_id" ref="model_ir_autovacuum"/> <field name="state">code</field> - <field name="code">model.power_on()</field> + <field name="code">model._run_vacuum_cleaner()</field> <field name='interval_number'>1</field> <field name='interval_type'>days</field> <field name="numbercall">-1</field> diff --git a/odoo/addons/base/models/ir_attachment.py b/odoo/addons/base/models/ir_attachment.py index 594d6bc8dbe..87e29406c6c 100644 --- a/odoo/addons/base/models/ir_attachment.py +++ b/odoo/addons/base/models/ir_attachment.py @@ -134,8 +134,8 @@ class IrAttachment(models.Model): os.makedirs(dirname) open(full_path, 'ab').close() - @api.model - def _file_gc(self): + @api.autovacuum + def _gc_file_store(self): """ Perform the garbage collection of the filestore. """ if self._storage() != 'file': return diff --git a/odoo/addons/base/models/ir_autovacuum.py b/odoo/addons/base/models/ir_autovacuum.py index 6b57ed1523f..cf42af5b094 100644 --- a/odoo/addons/base/models/ir_autovacuum.py +++ b/odoo/addons/base/models/ir_autovacuum.py @@ -1,7 +1,10 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import inspect import logging +import warnings +import traceback from odoo import api, models from odoo.exceptions import AccessDenied @@ -9,38 +12,50 @@ from odoo.exceptions import AccessDenied _logger = logging.getLogger(__name__) +def is_autovacuum(func): + """ Return whether ``func`` is an autovacuum method. """ + return callable(func) and getattr(func, '_autovacuum', False) + + class AutoVacuum(models.AbstractModel): - """ Expose the vacuum method to the cron jobs mechanism. """ + """ Helper model to the ``@api.autovacuum`` method decorator. """ _name = 'ir.autovacuum' _description = 'Automatic Vacuum' - @api.model - def _gc_transient_models(self): - for mname in self.env: - model = self.env[mname] - if model.is_transient(): - try: - with self._cr.savepoint(): - model._transient_vacuum(force=True) - except Exception as e: - _logger.warning("Failed to clean transient model %s\n%s", model, str(e)) - - @api.model - def _gc_user_logs(self): - self._cr.execute(""" - DELETE FROM res_users_log log1 WHERE EXISTS ( - SELECT 1 FROM res_users_log log2 - WHERE log1.create_uid = log2.create_uid - AND log1.create_date < log2.create_date - ) - """) - _logger.info("GC'd %d user log entries", self._cr.rowcount) + def _run_vacuum_cleaner(self): + """ + Perform a complete database cleanup by safely calling every + ``@api.autovacuum`` decorated method. + """ + if not self.env.is_admin(): + raise AccessDenied() + for model in self.env.values(): + cls = type(model) + for attr, func in inspect.getmembers(cls, is_autovacuum): + _logger.debug('Calling %s.%s()', model, attr) + try: + func(model) + self.env.cr.commit() + except Exception: + _logger.exception("Failed %s.%s()", model, attr) + self.env.cr.rollback() + + # Ensure backward compatibility with the previous autovacuum API + try: + self.power_on() + self.env.cr.commit() + except Exception: + _logger.exception("Failed power_on") + self.env.cr.rollback() + + # Deprecated API @api.model def power_on(self, *args, **kwargs): - if not self.env.is_admin(): - raise AccessDenied() - self.env['ir.attachment']._file_gc() - self._gc_transient_models() - self._gc_user_logs() - return True + tb = traceback.extract_stack(limit=2) + if tb[-2].name == 'power_on': + warnings.warn( + "You are extending the 'power_on' ir.autovacuum method" + f"in {tb[-2].filename} around line {tb[-2].lineno}. " + "You should instead use the @api.autovacuum decorator " + "on your garbage collecting method.", DeprecationWarning, stacklevel=2) diff --git a/odoo/addons/base/models/res_users.py b/odoo/addons/base/models/res_users.py index 4d012ad0ee4..f0941d134fb 100644 --- a/odoo/addons/base/models/res_users.py +++ b/odoo/addons/base/models/res_users.py @@ -190,6 +190,17 @@ class ResUsersLog(models.Model): # Currenly only uses the magical fields: create_uid, create_date, # for recording logins. To be extended for other uses (chat presence, etc.) + @api.autovacuum + def _gc_user_logs(self): + self._cr.execute(""" + DELETE FROM res_users_log log1 WHERE EXISTS ( + SELECT 1 FROM res_users_log log2 + WHERE log1.create_uid = log2.create_uid + AND log1.create_date < log2.create_date + ) + """) + _logger.info("GC'd %d user log entries", self._cr.rowcount) + class Users(models.Model): """ User class. A res.users record models an OpenERP user and is different diff --git a/odoo/api.py b/odoo/api.py index 42405b3d5f0..9daf85f0beb 100644 --- a/odoo/api.py +++ b/odoo/api.py @@ -288,6 +288,17 @@ def split_context(method, args, kwargs): return kwargs.pop('context', None), args, kwargs +def autovacuum(method): + """ + Decorate a method so that it is called by the daily vacuum cron job (model + ``ir.autovacuum``). This is typically used for garbage-collection-like + tasks that do not deserve a specific cron job. + """ + assert method.__name__.startswith('_'), "%s: autovacuum methods must be private" % method.__name__ + method._autovacuum = True + return method + + def model(method): """ Decorate a record-style method where ``self`` is a recordset, but its contents is not relevant, only the model is. Such a method:: diff --git a/odoo/models.py b/odoo/models.py index 15758441afa..43ac7929843 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -332,7 +332,6 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): """ # default values for _transient_vacuum() - _transient_check_count = 0 _transient_max_count = lazy_classproperty(lambda _: config.get('osv_memory_count_limit')) _transient_max_hours = lazy_classproperty(lambda _: config.get('transient_age_limit')) @@ -4705,61 +4704,6 @@ Record ids: %(records)s """ return cls._transient - def _transient_clean_rows_older_than(self, seconds): - assert self._transient, "Model %s is not transient, it cannot be vacuumed!" % self._name - # Never delete rows used in last 5 minutes - seconds = max(seconds, 300) - query = ("SELECT id FROM " + self._table + " WHERE" - " COALESCE(write_date, create_date, (now() at time zone 'UTC'))::timestamp" - " < ((now() at time zone 'UTC') - interval %s)") - self._cr.execute(query, ("%s seconds" % seconds,)) - ids = [x[0] for x in self._cr.fetchall()] - self.sudo().browse(ids).unlink() - - def _transient_clean_old_rows(self, max_count): - # Check how many rows we have in the table - self._cr.execute("SELECT count(*) AS row_count FROM " + self._table) - res = self._cr.fetchall() - if res[0][0] <= max_count: - return # max not reached, nothing to do - self._transient_clean_rows_older_than(300) - - @api.model - def _transient_vacuum(self, force=False): - """Clean the transient records. - - This unlinks old records from the transient model tables whenever the - "_transient_max_count" or "_max_age" conditions (if any) are reached. - Actual cleaning will happen only once every "_transient_check_time" calls. - This means this method can be called frequently called (e.g. whenever - a new record is created). - Example with both max_hours and max_count active: - Suppose max_hours = 0.2 (e.g. 12 minutes), max_count = 20, there are 55 rows in the - table, 10 created/changed in the last 5 minutes, an additional 12 created/changed between - 5 and 10 minutes ago, the rest created/changed more then 12 minutes ago. - - age based vacuum will leave the 22 rows created/changed in the last 12 minutes - - count based vacuum will wipe out another 12 rows. Not just 2, otherwise each addition - would immediately cause the maximum to be reached again. - - the 10 rows that have been created/changed the last 5 minutes will NOT be deleted - """ - assert self._transient, "Model %s is not transient, it cannot be vacuumed!" % self._name - _transient_check_time = 20 # arbitrary limit on vacuum executions - cls = type(self) - cls._transient_check_count += 1 - if not force and (cls._transient_check_count < _transient_check_time): - return True # no vacuum cleaning this time - cls._transient_check_count = 0 - - # Age-based expiration - if self._transient_max_hours: - self._transient_clean_rows_older_than(self._transient_max_hours * 60 * 60) - - # Count-based expiration - if self._transient_max_count: - self._transient_clean_old_rows(self._transient_max_count) - - return True - @api.model def search_read(self, domain=None, fields=None, offset=0, limit=None, order=None): """Perform a :meth:`search` followed by a :meth:`read`. @@ -6298,6 +6242,53 @@ class TransientModel(Model): _abstract = False # not abstract _transient = True # transient + @api.autovacuum + def _transient_vacuum(self): + """Clean the transient records. + + This unlinks old records from the transient model tables whenever the + "_transient_max_count" or "_max_age" conditions (if any) are reached. + Actual cleaning will happen only once every "_transient_check_time" calls. + This means this method can be called frequently called (e.g. whenever + a new record is created). + Example with both max_hours and max_count active: + Suppose max_hours = 0.2 (e.g. 12 minutes), max_count = 20, there are 55 rows in the + table, 10 created/changed in the last 5 minutes, an additional 12 created/changed between + 5 and 10 minutes ago, the rest created/changed more then 12 minutes ago. + - age based vacuum will leave the 22 rows created/changed in the last 12 minutes + - count based vacuum will wipe out another 12 rows. Not just 2, otherwise each addition + would immediately cause the maximum to be reached again. + - the 10 rows that have been created/changed the last 5 minutes will NOT be deleted + """ + if self._transient_max_hours: + # Age-based expiration + self._transient_clean_rows_older_than(self._transient_max_hours * 60 * 60) + + if self._transient_max_count: + # Count-based expiration + self._transient_clean_old_rows(self._transient_max_count) + + def _transient_clean_old_rows(self, max_count): + # Check how many rows we have in the table + query = 'SELECT count(*) FROM "{}"'.format(self._table) + self._cr.execute(query) + [count] = self._cr.fetchone() + if count > max_count: + self._transient_clean_rows_older_than(300) + + def _transient_clean_rows_older_than(self, seconds): + # Never delete rows used in last 5 minutes + seconds = max(seconds, 300) + query = """ + SELECT id FROM "{}" + WHERE COALESCE(write_date, create_date, (now() AT TIME ZONE 'UTC'))::timestamp + < (now() AT TIME ZONE 'UTC') - interval %s + """.format(self._table) + self._cr.execute(query, ["%s seconds" % seconds]) + ids = [x[0] for x in self._cr.fetchall()] + self.sudo().browse(ids).unlink() + + def itemgetter_tuple(items): """ Fixes itemgetter inconsistency (useful in some cases) of not returning a tuple if len(items) == 1: always returns an n-tuple where n = len(items) |
