Skip to content

External convert timeout can hang forever (weasyprint on large CSV) #3359

Open

Description

@tiborrr

What happens

Large CSV/text files converted with weasyprint can pin a process-item job for hours. Progress sits around 20%. Last job log lines look like:

Running external command: weasyprint …
Waiting for command to terminate…

docspell.joex.convert.weasyprint.command.timeout is set (default is 2 minutes). It does not kill these jobs. Other conversions on the same host do log real timeouts (Timed out after: 120000 ms), so the setting is live. These hung weasyprint runs just never get there.

If you kill the weasyprint OS process yourself, Docspell wakes up, skips the PDF, and finishes the item. Addons run. So the failure path is fine. The timeout path never reaches it.

Setup where we saw it

  • joex around v0.46
  • text/csv through weasyprint, files in the 3–10 MB range
  • weasyprint stuck at ~100% CPU on those files for a long time

Why I think it hangs

SysExec.waitFor does call Process.waitFor(timeout). When that returns "still running", it joins the stdout/stderr log fibers before raising TimeoutException:

// modules/common/src/main/scala/docspell/common/exec/SysExec.scala
.blocking(proc.waitFor(to.millis, TimeUnit.MILLISECONDS))
.flatTap(_ => fibers.get.flatMap(_.traverse_(identity)))
.flatMap(terminated =>
  if (terminated) proc.exitValue().pure[F]
  else Sync[F].raiseError(new TimeoutException(...))
)

Those fibers come from logOutputs / consumeOutputs. They only finish when the child closes its pipes. On timeout the child is still alive, so the join never returns. TimeoutException never fires. Resource cleanup never calls destroy(). You get a permanent hang after "Waiting for command to terminate…".

ExternConv.toPDF uses that path for weasyprint.

Note: WeasyPrint's own -t / --timeout only limits HTTP fetches for remote assets. It does not bound conversion time, so wiring that into args would not fix this.

What I'd expect

When command.timeout elapses, destroy the process (forcibly if needed), fail the conversion with a timeout, and let process-item continue without a PDF.

Fix direction

On timeout, destroy the process first, then cancel/join the log fibers, then raise TimeoutException. Soft destroy() may not be enough if weasyprint ignores it under load.

Workaround

Kill stuck weasyprint processes in the joex container. Jobs then continue.

Related in spirit to #3345 (external converter blocking workers), but this one is specifically timeout vs log-drain ordering in SysExec.

Activity

  1. tiborrr commented on Oct 2, 2026

    @tiborrr
    ContributorAuthor

    Local joex + restserver verification

    Ran this branch locally with restserver + joex (0.44.0-SNAPSHOT) after replacing docspell-common with the fixed jar.

    Setup:

    • uploaded hang.csv
    • weasyprint replaced with a fake binary that reads stdin then hangs forever (open pipes)
    • docspell.joex.convert.weasyprint.command.timeout = 3 seconds

    Result: conversion fails with timeout and process-item continues (text extraction / analysis) instead of hanging on Waiting for command to terminate….

    Converting file Some(hang.csv) … into a PDF
    PDF conversion failed: Timed out after: 3000 ms. Go without PDF file
    Starting text extraction for 1 files
    Extracting text for attachment hang.csv finished in 66 ms
    Starting text analysis
    

    Screenshot from the local run:

    joex timeout proof

    Full snippet: https://github.com/tiborrr/docspell/blob/fix/sysexec-timeout-destroy/docs/pr-evidence/pr3360-joex-timeout-snippet.txt

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions