Skip to content

Commit 60c49c5

Browse files
committed
[FIX] orm: in recompute_fields, avoid memory error from postgresql
improves 13adede Queries run through client-side cursors will make postgresql materialze the whole of the result immediately (which is actually, why `cr.rowcount` is always available right after `execute` in this case). With server side cursors (named cursors) on the other hand, tuples are materialized when they are fetched. This is why running the `query` for ids through the client-side cursor just to be able to access `cr.rowcount`, can cause an out-of-memory exception from PostgreSQL. We fix this by wrapping the query in a `CREATE TABLE AS` statement that inserts returned ids into a temporary table. We then use a named_cursor to fetch ids from this table in chunks, server-side. Another approach would have been to just wrap the query in a `SELECT count(*)` query and run this once to get the `count`. The approach using `CREATE TABLE AS` has been chosen over that solution to support queries that include DML statements (e.g. `UPDATE ... RETURNING`) that affect the results of the compute, as it allows us to run the query on the main (client) cursor, while still using a named_cursor for fetching the ids memory-efficiently. closes #322 Related: odoo/upgrade#8487 Signed-off-by: Christophe Simonis (chs) <chs@odoo.com>
1 parent 693590a commit 60c49c5

File tree

1 file changed

+16
-13
lines changed

1 file changed

+16
-13
lines changed

src/util/orm.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
from .exceptions import MigrationError
4343
from .helpers import table_of_model
4444
from .misc import chunks, log_progress, version_between, version_gte
45-
from .pg import column_exists, format_query, get_columns, named_cursor
45+
from .pg import SQLStr, column_exists, format_query, get_columns, named_cursor
4646

4747
# python3 shims
4848
try:
@@ -278,7 +278,8 @@ def recompute_fields(cr, model, fields, ids=None, logger=_logger, chunk_size=256
278278
:param int chunk_size: number of records per chunk - used to split the processing
279279
:param str strategy: strategy used to process the re-computation
280280
:param str query: query to get the IDs of records to recompute, it is an error to set
281-
both `ids` and `query`
281+
both `ids` and `query`. Note that the processing will always happen
282+
in ascending order. If that is unwanted, you must use `ids` instead.
282283
"""
283284
if strategy not in {"flush", "commit", "auto"}:
284285
raise ValueError("Invalid strategy {!r}".format(strategy))
@@ -288,25 +289,26 @@ def recompute_fields(cr, model, fields, ids=None, logger=_logger, chunk_size=256
288289
model = Model._name
289290

290291
if ids is None:
291-
query = format_query(cr, "SELECT id FROM {} ORDER BY id", table_of_model(cr, model)) if query is None else query
292-
cr.execute(query)
292+
query = format_query(cr, "SELECT id FROM {}", table_of_model(cr, model)) if query is None else SQLStr(query)
293+
cr.execute(
294+
format_query(cr, "CREATE UNLOGGED TABLE _upgrade_rf(id) AS (WITH query AS ({}) SELECT * FROM query)", query)
295+
)
293296
count = cr.rowcount
294-
if count < 2**21: # avoid the overhead of a named cursor unless we have at least two chunks
295-
ids_ = (id_ for (id_,) in cr.fetchall())
296-
else:
297+
cr.execute("ALTER TABLE _upgrade_rf ADD CONSTRAINT pk_upgrade_rf_id PRIMARY KEY (id)")
297298

298-
def get_ids():
299-
with named_cursor(cr, itersize=2**20) as ncr:
300-
ncr.execute(query)
301-
for (id_,) in ncr:
302-
yield id_
299+
def get_ids():
300+
with named_cursor(cr, itersize=2**20) as ncr:
301+
ncr.execute("SELECT id FROM _upgrade_rf ORDER BY id")
302+
for (id_,) in ncr:
303+
yield id_
303304

304-
ids_ = get_ids()
305+
ids_ = get_ids()
305306
else:
306307
count = len(ids)
307308
ids_ = ids
308309

309310
if not count:
311+
cr.execute("DROP TABLE IF EXISTS _upgrade_rf")
310312
return
311313

312314
_logger.info("Computing fields %s of %r on %d records", fields, model, count)
@@ -336,6 +338,7 @@ def get_ids():
336338
else:
337339
flush(records)
338340
invalidate(records)
341+
cr.execute("DROP TABLE IF EXISTS _upgrade_rf")
339342

340343

341344
class iter_browse(object):

0 commit comments

Comments
 (0)