在访问Django模型时,PDF库中的非确定性行为

2 投票
1 回答
60 浏览
提问于 2025-04-13 18:41

我有一个在 Django 4.2Python 3.10 上运行的应用,有时候它的表现不太正常,尤其是在应该是 无状态 的地方。如果我在使用 pypdf 3.17.4 的时候访问数据库(postgresql 14,通过 pcygopg 3.1.18),生成的输出文档就会出问题……大约每8次尝试中就会有1次出现这种情况。我该如何找出这个不稳定行为的原因呢?

生成的PDF文件有内容缺失,所以只要比较输出的大小就能判断出这个错误是怎么触发的:

# call as: python3 manage.py minrepro input.pdf
import argparse, io
from django.core.management.base import BaseCommand
from pypdf import PdfReader, PdfWriter
from djangoapp.models import DjangoModel


def main(fin):
    pdfout = PdfWriter()
    pageout = pdfout.add_blank_page(width=200, height=200)

    for i in range(8):
        # Note: accessing the database *during* PDF merging is relevant!
        # without the next line, the problem cannot be reproduced
        for c in range(31): a = DjangoModel.objects.first()

        fin.seek(0)
        for pagein in PdfReader(fin, strict=True).pages:
            pageout.merge_page(pagein)

    with io.BytesIO() as fout:
        pdfout.write(fout)
        return fout.tell()

class Command(BaseCommand):
    def add_arguments(self, parser):
        parser.add_argument(dest="pdf", type=argparse.FileType("rb") )
    def handle(self, *args, **options):
        for i in range(30):
            if i == 0: first_size = main(options["pdf"])
            current_size = main(options["pdf"])
            if not first_size == current_size:
                print(f"presumed stateless call was not {i=}, {first_size=} != {current_size=}")

  • 我最初的猜测是 pypdf 没有为(合并后)重复的标识符生成真正唯一的名称,而数据库的访问只是给了这个问题一个显现的机会(延迟)。我查看了相关库中是否有 timerandom 的导入,但没有发现明显的错误。而且,像 time.sleep(0.02) 这样的代码也无法让我重现这个问题,只有访问数据库才会导致。
  • 这是使用 CPython 的,字典是按插入顺序的,大部分代码在相同输入下应该表现一致。我还用 -W module::ResourceWarning -W module::DeprecationWarning -W module::PendingDeprecationWarning 运行了 Python,以便更容易发现我可能犯的错误,但也没有发现任何问题。
  • 我在一个单线程的管理命令中重现了这个行为,因此这 任何 http/web 服务器的问题无关。

1 个回答

2

根本原因已经找到:在循环中丢弃并重新创建PdfReader对象,导致pypdf.PdfWriter.merge有很小的概率会重复使用已经用过的标识符映射。

看起来触发这个问题的原因是我循环中的垃圾回收,psycopg3只是提供了这个发生的机会,并不是直接导致它的原因。

关于重置源PDF和目标PDF对象之间关联的相关文档让我发现了之前忽略的细节:pypdf假设但从不检查在合并过程中使用的PdfReader对象不会重复使用Python的id()

两个生命周期不重叠的对象可能会有相同的id()值。 -- Python id(object)

在我的代码中确实发生了这种情况,可以通过打印id(page.pdf)轻松看到。

在最后一个输入合并之前,保持对PdfReader对象的引用(或者简单地在输出写入之前)也会阻止我的复现代码正常工作,像这样:

store_for_pypdf = []
for loop:
    ...
    reader = PdfReader(fin, strict=True)
    store_for_pypdf.append(reader)
    ... # use reader
...
pdfout.write(fout)
del store_for_pypdf

撰写回答