1010
1111_logger = logging .getLogger (__name__ )
1212
13+ # Number of records deleted per unlink() call. Bounds the id list, the SQL
14+ # DELETE size and the per-commit work, independently of how many records the
15+ # rule retrieves (rule.batch_size).
16+ UNLINK_BATCH_SIZE = 5000
17+
1318
1419class AutovacuumMixin (models .AbstractModel ):
1520 _name = "autovacuum.mixin"
1621 _description = "Mixin used to delete messages or attachments"
1722
18- def batch_unlink (self ):
23+ def batch_unlink (self , batch_size = 0 ):
24+ # batch_size <= 0 => delete everything in a single unlink (no chunking).
1925 with Registry (self .env .cr .dbname ).cursor () as new_cr :
26+ if batch_size == - 1 :
27+ batch_size = len (self )
28+ if not batch_size or batch_size < 0 :
29+ batch_size = new_cr .IN_MAX
30+ elif batch_size > new_cr .IN_MAX :
31+ # Adapt cursor IN_MAX to batch_size if needed:
32+ # unlink silently chunk using IN_MAX (1000-rows)
33+ # Assigning on the instance shadows the class attribute
34+ # for this cursor only, not the class
35+ # (Cursor.IN_MAX stays 1000)
36+ new_cr .IN_MAX = batch_size
2037 new_env = api .Environment (new_cr , self .env .uid , self .env .context )
2138 try :
22- while self :
23- batch_delete = self [0 :1000 ]
24- self -= batch_delete
39+ remaining = self
40+ while remaining :
41+ batch_delete = remaining [0 :batch_size ]
42+ remaining -= batch_delete
2543 # do not attach new env to self because it may be
2644 # huge, and the cache is cleaned after each unlink
2745 # so we do not want to much record is the env in
@@ -38,7 +56,13 @@ def autovacuum(self, ttype="message"):
3856 rules = self .env ["vacuum.rule" ].search ([("ttype" , "=" , ttype )])
3957 for rule in rules :
4058 records = rule ._search_autovacuum_records ()
41- records .batch_unlink ()
59+ _logger .info (
60+ "Autovacuum rule %s: %s %s record(s) matched, deleting..." ,
61+ rule .name ,
62+ len (records ),
63+ self ._name ,
64+ )
65+ records .batch_unlink (UNLINK_BATCH_SIZE )
4266
4367 def _get_autovacuum_domain (self , rule ):
4468 return []
@@ -95,21 +119,28 @@ def _get_autovacuum_records_model(self, rule):
95119 self ._prefix_domain_fields (autovacuum_relation , mixin_domain ),
96120 ]
97121 )
98- records = self .env [rule .model_id .model ].search (
99- record_domain ,
100- # Thanks to the optimization with _prefix_domain_fields,
101- # We now have at least 1 mixin-record per related-record
102- # => We can also limit this search here
103- # and this won't affect the final search
104- limit = limit ,
105- )
122+ related_model = self .env [rule .model_id .model ]
123+ # Optimizations
124+ # 1. We have at least 1 mixin-record per related-record
125+ # (see _prefix_domain_fields)
126+ # => we can limit this search and get a free optimization.
127+ # Reason: len(models) <= len(models mails)
128+
129+ # 2. The Query is injected directly into the domain as a SQL subquery
130+ # (res_id IN (SELECT ...)) instead of materialising ids in Python.
131+ # _search:
132+ # "No default order is applied when
133+ # the method is invoked without parameter ``order``."
134+ related_query = related_model ._search (record_domain , limit = limit )
106135 mixin_domain = expression .AND (
107- [mixin_domain , [("res_id" , "in" , records . ids )]]
136+ [mixin_domain , [("res_id" , "in" , related_query )]]
108137 )
109- return self .search (
110- mixin_domain ,
111- limit = limit ,
112- )
138+ # Use _search (not search): deletion order is irrelevant and _search
139+ # applies no ORDER BY. The model default (mail.message._order =
140+ # 'id desc') combined with LIMIT would force Postgres to sort the whole
141+ # matching set on every batch (~18s for the 15M stock.picking backlog);
142+ # without it the LIMIT short-circuits the index scan (~70ms).
143+ return self .browse (self ._search (mixin_domain , limit = limit ))
113144
114145 # Retro-compatibility
115146 _get_autovacuum_records = _get_autovacuum_records_model
0 commit comments