Skip to content

[FIX] base_ux, mail_ux: stop the PDF render from calling the instance back over HTTP - #446

Closed
jcadhoc wants to merge 2 commits into
ingadhoc:19.0from
adhoc-dev:19.0-h-126274-jc
Closed

[FIX] base_ux, mail_ux: stop the PDF render from calling the instance back over HTTP#446
jcadhoc wants to merge 2 commits into
ingadhoc:19.0from
adhoc-dev:19.0-h-126274-jc

Conversation

@jcadhoc

@jcadhoc jcadhoc commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Rendering a PDF report is circular today: wkhtmltopdf resolves the report asset links (and the barcode images, when the report has any) against base_url, that is, against the very same Odoo instance. So a render occupies one HTTP worker and needs another free one to answer itself.

On an instance with workers = 3, three concurrent renders exhaust the three workers, nobody calls accept(), the backlog fills up and the port stops accepting connections. Everything stays blocked until limit_time_real (300s), but the liveness probe (tcpSocket 8069, 15s × 8) kills the container at 120s — so the pod never gets to recover by itself and the whole service goes down for 2 to 4 minutes.

The composer of a mail template amplifies it: attachment_ids is a compute with store=True, readonly=False, so the web client recomputes it on every round trip while the composer is opening, and each recompute renders the same PDF again.

Two independent changes, one per module.

base_ux: the report html no longer calls the instance back

_prepare_html post-processes the html handed to wkhtmltopdf:

  • the report asset bundles are inlined as a <style> tag;
  • the @font-face rules pointing back at the instance are dropped, keeping the font-family declarations. wkhtmltopdf resolves those families from the system fonts of the image that runs it;
  • the /report/barcode/ images are embedded as data URIs, resolved in process through ir.actions.report.barcode() — the same code the controller uses.

Anything that cannot be resolved is left exactly as it was, so a report never fails because of this. It can be turned off with the system parameter base_ux.report_self_contained.

mail_ux: one render per composer, not 2 to 6

_compute_attachment_ids reuses the attachment already generated for the same report, the same record and the same write_date of that record, as long as it is still orphan (res_model = mail.compose.message, res_id = 0), was created by the same user and falls inside the reuse window. This also stops the orphan attachments from piling up in the database on every opening of the composer.

The reuse only kicks in for the case that causes the problem and that is unambiguous: comment mode over a single record with a single qweb report on the template. With more than one report there is no way to tell which created attachment belongs to which report, so core resolves it.

The window bounds how long a change that the report shows but that does not touch the record's own write_date (partner data, company data, a translation) could be seen stale. System parameter mail_ux.report_attachment_cache_ttl, 600 seconds by default, 0 disables the reuse.

Why this approach

  • Raising workers does not close the door, it only moves the threshold: an instance with 8 effective workers (workers = 4, replicas = 2) hit the same deadlock. And it would mean paying for capacity to render the same PDF several times.
  • Lowering limit_time_real below the probe's 120s would turn the outage into a recycled worker, but it leaves the deadlock in place and it is a platform side setting, not a fix.
  • Adding a timeout and cancellation on the PDF service only changes how it fails ("the PDF failed" instead of "the instance is down for 5 minutes"). It does not make the PDF come out. Tracked separately on the platform side.

Measurements

Taken from the access logs of a production 19.0 instance, over a 9 hour window:

  • 259 renders and 508 requests to /web/assets/*report_assets*, all of them coming from the loopback address — exactly 2 self-requests per render, which is what this PR removes.
  • 0 requests to /web/static/fonts/ from that same loop, which is what makes dropping the local @font-face rules safe: nothing asks for those files today.
  • Between 13% and 36% of the renders of any instance are the same document generated 2 to 6 times within seconds — the recompute storm this PR stops.

Test plan

  • Open the mail composer on a purchase order (or any record with a mail template carrying a report), close it and open it again. The log must show one The PDF report has been generated per opening, and ir.attachment must not grow by 2 to 4 orphan rows each time.
  • Edit the record and reopen the composer: the PDF must be generated again.
  • Print any report and check the html handed to wkhtmltopdf has no <link> to /web/assets/ and no src="/report/barcode/". On the instance side, no request from the loopback address to /web/assets/*report_assets* during the render.
  • Print a report that carries a barcode or a QR code (a delivery slip, an invoice) and check the image comes out.
  • Set base_ux.report_self_contained to 0 and mail_ux.report_attachment_cache_ttl to 0: both behaviours must fall back to core.

Verified end to end against the same PDF service used in production: the report renders with the expected styling and page layout, with the text fonts resolved from the system.

The html of each rendered document grows by around 400 KB, the size of the inlined bundles.

Internal reference: https://www.adhoc.inc/odoo/helpdesk.ticket/126274

Juan Ignacio Carreras added 2 commits September 1, 2026 14:48
wkhtmltopdf resolves the report asset links and the barcode images against
base_url, that is, against the very same Odoo instance. A render therefore
occupies one HTTP worker and needs another free one to answer itself. On an
instance with workers = 3, three concurrent renders exhaust the three workers,
nobody calls accept(), the backlog fills up and the port stops accepting
connections. Everything stays blocked until limit_time_real (300s), but the
liveness probe (tcpSocket 8069, 15s x 8) kills the container at 120s, so the
pod never gets to recover by itself.

_prepare_html now post-processes the html handed to wkhtmltopdf:

* the report asset bundles are inlined as a <style> tag;
* the @font-face rules pointing back at the instance are dropped, keeping the
  font-family declarations. Measured on the production logs, wkhtmltopdf never
  requests any of those font files: it resolves those families from the system
  fonts of the image that runs it;
* the /report/barcode/ images are embedded as data URIs.

Anything that cannot be resolved is left exactly as it was, so a report never
fails because of this.

Measured on a production 19.0 instance, over 9 hours of logs: 259 renders and 508 requests to
/web/assets/*report_assets*, all of them from 127.0.0.6 and zero requests to
/web/static/fonts. That is exactly 2 self-requests per render, which is what
this removes.

The html of each document grows by around 400 KB (the size of the inlined
bundles). The behaviour can be turned off with the system parameter
base_ux.report_self_contained.

Change note: los reportes PDF ya no dependen de que Odoo se conteste a sí mismo
por HTTP mientras los genera. El css del reporte y los códigos de barras van
embebidos en el html, así que un render no necesita más que su propio worker.
Antes, en instancias con pocos workers, varios renders simultáneos se trababan
entre sí y terminaban en un reinicio del servicio de 2 a 4 minutos. Se puede
desactivar con el parámetro de sistema base_ux.report_self_contained.
mail.compose.message.attachment_ids is a compute with store=True and
readonly=False, so the web client recomputes it on every round trip while the
composer is opening, and each recompute calls _render_qweb_pdf again on the
very same record. Measured in production, the same PDF is generated 2 to 6
times per opening of the composer, and every recompute leaves its orphan
ir.attachment behind (res_model = mail.compose.message, res_id = 0).

_compute_attachment_ids now reuses the attachment already generated for the
same report, the same record and the same write_date of that record, as long as
it is still orphan, was created by the same user and falls inside the reuse
window. The reuse only kicks in for the case that causes the problem and that
is unambiguous: comment mode over a single record with a single qweb report on
the template. With more than one report there is no way to tell which created
attachment belongs to which report, so core resolves it.

The window bounds how long a change that the report shows but that does not
touch the record's own write_date (partner data, company data, a translation)
could be seen stale. It is set through the system parameter
mail_ux.report_attachment_cache_ttl (600 seconds by default, 0 disables the
reuse).

Change note: al abrir el compositor de correo con una plantilla que adjunta un
reporte, el PDF se genera una sola vez en lugar de 2 a 6 veces. Además deja de
acumular los PDF basura que quedaban tirados en la base en cada apertura. Si el
registro cambia, el PDF se vuelve a generar. La ventana de reuso se configura
con el parámetro de sistema mail_ux.report_attachment_cache_ttl.
@roboadhoc

Copy link
Copy Markdown
Contributor

Pull request status dashboard

@jcadhoc

jcadhoc commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

Closing in favour of ingadhoc/purchase#367.

This PR post-processed the report html to inline the asset bundles and overrode _compute_attachment_ids to memoize the rendered report — around 290 lines across two modules. Core already persists and reuses report PDFs through ir.actions.report.attachment / attachment_use, so the same behaviour is two data fields on the report and no Python at all.

@jcadhoc jcadhoc closed this Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants