Skip to content

Commit d0ced5b

Browse files
committed
[IMP] jinja_to_qweb: store validation data in the DB
upg-2994884 Avoid `MemoryError` and/or killed process because of `malloc()` failing within `lxml` / `libxml2`. Debugging this by determining the size of involved datastructures through means of `sys.getsizeof()` showed that the overly large memory consumption has 3 different sources: 1. During conversion: The global variable `templates_to_check` grows to hundreds of MiB after the various calls to `upgrade_jinja_fields()` by upgrade scripts. 2. During conversion: The call to `cr.dictfetchall()` to gather all templates(fields) that are to be converted, already consumes hundreds of MiB. 3. At the start of function `verify_upgraded_jinja_fields()`, the process is at ~1.5GiB because of (1) and (2). While iterating over all the templates in `templates_to_check`, no significant amount of memory is allocated on top of this *by python datastructures*. But, with each call to `is_converted_template_valid()`, the size of the process increases until it hits the RLIMIT. This function calls into `lxml` multiple times, suggesting that the memory is allocated in `malloc()` calls in the `lxml` and/or `libxml2` C library/ies, evading python's memory accounting and garbage collection. Internet research shows that `lxml` has a long history of different memory leaks in C code, plus some caching mechanism *across documents* that could be responsible[^1]. More recent versions of the module seem to have been improved, but still we're stuck with old versions. This patch takes care of (1) by, instead of keeping the converted data in memory for the validation step, creating a "temporary" table and inserting the data there. After validation, the table is dropped. [^1]: https://benbernardblog.com/tracking-down-a-freaky-python-memory-leak-part-2/ Part-of: #288 Signed-off-by: Christophe Simonis (chs) <[email protected]>
1 parent 1f58187 commit d0ced5b

File tree

1 file changed

+110
-57
lines changed

1 file changed

+110
-57
lines changed

src/util/jinja_to_qweb.py

Lines changed: 110 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,6 @@
5757
re.X | re.DOTALL,
5858
)
5959

60-
templates_to_check = {}
61-
6260

6361
def _remove_safe(expression):
6462
return re.sub(REMOVE_SAFE_REGEX, " ", expression).strip()
@@ -226,6 +224,48 @@ def _replace_endif(string):
226224
return reg.sub(r"\1</t>", string)
227225

228226

227+
def setup_templates_to_check(cr):
228+
if table_exists(cr, "_upgrade_jinja_to_qweb"):
229+
return
230+
cr.execute(
231+
"""
232+
CREATE UNLOGGED TABLE _upgrade_jinja_to_qweb(
233+
table_name varchar,
234+
template_type varchar,
235+
table_id int4,
236+
template_field varchar,
237+
model_name varchar,
238+
template_name varchar,
239+
template_name_field varchar,
240+
template_converted varchar,
241+
PRIMARY KEY(table_name, template_type, table_id, template_field)
242+
)
243+
"""
244+
)
245+
246+
247+
def insert_templates_to_check(cr, *args):
248+
cr.execute(
249+
"""
250+
INSERT INTO _upgrade_jinja_to_qweb (
251+
table_name,
252+
template_type,
253+
table_id,
254+
template_field,
255+
model_name,
256+
template_name,
257+
template_name_field,
258+
template_converted
259+
) VALUES(%s, %s, %s, %s, %s, %s, %s, %s)
260+
""",
261+
args,
262+
)
263+
264+
265+
def cleanup_templates_to_check(cr):
266+
cr.execute("DROP TABLE _upgrade_jinja_to_qweb")
267+
268+
229269
def upgrade_jinja_fields(
230270
cr,
231271
table_name,
@@ -246,7 +286,7 @@ def upgrade_jinja_fields(
246286
sql_where_qweb_fields = [field + r"~ '(\$\{|%\s*(if|for))'" for field in qweb_fields]
247287
sql_where_fields = " OR ".join(sql_where_inline_fields + sql_where_qweb_fields)
248288

249-
templates_to_check[table_name] = []
289+
setup_templates_to_check(cr)
250290
model = model_of_table(cr, table_name)
251291

252292
cr.commit() # ease the processing for PG
@@ -260,18 +300,44 @@ def upgrade_jinja_fields(
260300
for data in cr.dictfetchall():
261301
_logger.info("process %s(%s) %s", table_name, data["id"], data[name_field])
262302

303+
# only for mailing.mailing
304+
if fetch_model_name:
305+
cr.execute(
306+
"""
307+
SELECT model FROM ir_model WHERE id=%s
308+
""",
309+
[data[table_model_name]],
310+
)
311+
model_name = cr.fetchone()[0]
312+
else:
313+
model_name = model_name or data[table_model_name]
314+
263315
# convert the fields
264316
templates_converted = {}
265-
266317
for field in inline_template_fields:
267318
_logger.info(" `- convert inline field %s", field)
268-
template = data[field]
269-
templates_converted[field] = convert_jinja_to_inline(template) if template else ""
319+
converted = convert_jinja_to_inline(data[field]) if data[field] else ""
320+
templates_converted[field] = converted
321+
if data[field]:
322+
insert_templates_to_check(
323+
cr,
324+
table_name,
325+
"inline_template",
326+
data["id"],
327+
field,
328+
model_name,
329+
data[name_field],
330+
name_field,
331+
converted,
332+
)
270333

271334
for field in qweb_fields:
272335
_logger.info(" `- convert qweb field %s", field)
273-
template = data[field]
274-
templates_converted[field] = convert_jinja_to_qweb(template) if template else ""
336+
converted = convert_jinja_to_qweb(data[field]) if data[field] else ""
337+
templates_converted[field] = converted
338+
insert_templates_to_check(
339+
cr, table_name, "qweb", data["id"], field, model_name, data[name_field], name_field, converted
340+
)
275341

276342
fields = [f for f in (inline_template_fields + qweb_fields) if data[f] != templates_converted[f]]
277343
if fields:
@@ -286,30 +352,6 @@ def upgrade_jinja_fields(
286352
""",
287353
field_values + [data["id"]],
288354
)
289-
# prepare data to check later
290-
291-
# only for mailing.mailing
292-
if fetch_model_name:
293-
cr.execute(
294-
"""
295-
SELECT model FROM ir_model WHERE id=%s
296-
""",
297-
[data[table_model_name]],
298-
)
299-
model_name = cr.fetchone()[0]
300-
else:
301-
model_name = model_name or data[table_model_name]
302-
303-
templates_to_check[table_name].append(
304-
(
305-
data,
306-
name_field,
307-
model_name,
308-
inline_template_fields,
309-
qweb_fields,
310-
templates_converted,
311-
)
312-
)
313355

314356
if not table_exists(cr, "ir_translation"):
315357
return
@@ -402,49 +444,58 @@ def upgrade_jinja_fields(
402444

403445
def verify_upgraded_jinja_fields(cr):
404446
env = get_env(cr)
405-
for table_name, template_data in templates_to_check.items():
447+
cr.execute("SELECT DISTINCT(table_name) FROM _upgrade_jinja_to_qweb")
448+
for (table_name,) in cr.fetchall():
406449
field_errors = {}
407450
missing_records = []
451+
ncr = named_cursor(cr, 100)
452+
ncr.execute(
453+
"""
454+
SELECT template_type,
455+
table_id,
456+
template_field,
457+
model_name,
458+
template_name,
459+
template_name_field,
460+
template_converted
461+
FROM _upgrade_jinja_to_qweb
462+
WHERE table_name = %s
463+
ORDER BY template_type,
464+
template_field
465+
""",
466+
[table_name],
467+
)
408468
for (
409-
data,
410-
name_field,
469+
template_type,
470+
table_id,
471+
template_field,
411472
model_name,
412-
inline_template_fields,
413-
qweb_fields,
414-
templates_converted,
415-
) in template_data:
473+
template_name,
474+
template_name_field,
475+
template_converted,
476+
) in ncr:
416477
if model_name not in env:
417478
# custom model not loaded yet. Ignore
418479
continue
419480
model = env[model_name]
420481
record = model.with_context({"active_test": False}).search([], limit=1, order="id")
421482

422-
key = (data["id"], data[name_field])
483+
key = (table_id, template_name, template_name_field)
423484
field_errors[key] = []
424485

425486
if not record:
426487
missing_records.append(key)
427488

428-
for field in inline_template_fields:
429-
if not data[field]:
430-
continue
431-
is_valid = is_converted_template_valid(
432-
env, data[field], templates_converted[field], model_name, record.id, engine="inline_template"
433-
)
434-
if not is_valid:
435-
field_errors[key].append(field)
436-
437-
for field in qweb_fields:
438-
is_valid = is_converted_template_valid(
439-
env, data[field], templates_converted[field], model_name, record.id, engine="qweb"
440-
)
441-
if not is_valid:
442-
field_errors[key].append(field)
489+
is_valid = is_converted_template_valid(
490+
env, template_field, template_converted, model_name, record.id, engine=template_type
491+
)
492+
if not is_valid:
493+
field_errors[key].append(template_field)
443494

444495
if missing_records:
445496
list_items = "\n".join(
446497
f'<li>id: "{id}", {html_escape(name_field)}: "{html_escape(name)}" </li>'
447-
for id, name in missing_records
498+
for id, name, name_field in missing_records
448499
)
449500
add_to_migration_reports(
450501
f"""
@@ -464,7 +515,7 @@ def verify_upgraded_jinja_fields(cr):
464515

465516
if field_errors:
466517
string = []
467-
for (id, name), fields in field_errors.items():
518+
for (id, name, name_field), fields in field_errors.items():
468519
fields_string = "\n".join(f"<li>{html_escape(field)}</li>" for field in fields)
469520
string.append(
470521
f"""<li>id: {id}, {html_escape(name_field)}: {html_escape(name)},
@@ -486,6 +537,8 @@ def verify_upgraded_jinja_fields(cr):
486537
"Jinja upgrade",
487538
format="html",
488539
)
540+
ncr.close()
541+
cleanup_templates_to_check(cr)
489542

490543

491544
def is_converted_template_valid(env, template_before, template_after, model_name, record_id, engine="inline_template"):

0 commit comments

Comments
 (0)