[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
Closed
[FIX] base_ux, mail_ux: stop the PDF render from calling the instance back over HTTP#446jcadhoc wants to merge 2 commits into
jcadhoc wants to merge 2 commits into
Conversation
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.
Contributor
jcadhoc
force-pushed
the
19.0-h-126274-jc
branch
from
September 1, 2026 14:52
69278c9 to
7ab9fca
Compare
Contributor
Author
|
Closing in favour of ingadhoc/purchase#367. This PR post-processed the report html to inline the asset bundles and overrode |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

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 callsaccept(), the backlog fills up and the port stops accepting connections. Everything stays blocked untillimit_time_real(300s), but the liveness probe (tcpSocket8069, 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_idsis a compute withstore=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_htmlpost-processes the html handed to wkhtmltopdf:<style>tag;@font-facerules pointing back at the instance are dropped, keeping thefont-familydeclarations. wkhtmltopdf resolves those families from the system fonts of the image that runs it;/report/barcode/images are embedded as data URIs, resolved in process throughir.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_idsreuses the attachment already generated for the same report, the same record and the samewrite_dateof 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 parametermail_ux.report_attachment_cache_ttl, 600 seconds by default, 0 disables the reuse.Why this approach
workersdoes 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.limit_time_realbelow 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.Measurements
Taken from the access logs of a production 19.0 instance, over a 9 hour window:
/web/assets/*report_assets*, all of them coming from the loopback address — exactly 2 self-requests per render, which is what this PR removes./web/static/fonts/from that same loop, which is what makes dropping the local@font-facerules safe: nothing asks for those files today.Test plan
The PDF report has been generatedper opening, andir.attachmentmust not grow by 2 to 4 orphan rows each time.<link>to/web/assets/and nosrc="/report/barcode/". On the instance side, no request from the loopback address to/web/assets/*report_assets*during the render.base_ux.report_self_containedto0andmail_ux.report_attachment_cache_ttlto0: 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