Недетерминированное поведение в библиотеке PDF при промежуточном доступе к модели Django
Приложение Django 4.2 on Python 3.10
иногда ведет себя неправильно в том месте, которое должно быть без изменений. Если я обращаюсь к базе данных (postgresql 14
через pcygopg 3.1.18
) во время работы с pypdf 3.17.4
, то выходной документ ломается... примерно в 1 из 8 попыток . Как определить причину такого нестабильного поведения?
В полученном 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 не может генерировать действительно уникальные имена для (после слияния) дублирующихся идентификаторов, а доступ к БД просто дает возможность (задержку), чтобы это стало заметным. Я искал любые
time
илиrandom
импорты в соответствующих библиотеках, никаких очевидных ошибок там нет. Также ничто, похожее наtime.sleep(0.02)
, не могло заставить мой репродуктор работать, только доступ к базе данных. - Это CPython со вставленными упорядоченными кубиками, большинство кода должно вести себя одинаково на идентичном вводе. Также называл python с
-W module::ResourceWarning -W module::DeprecationWarning -W module::PendingDeprecationWarning
, чтобы с большей вероятностью быть уведомленным о любых ошибках с моей стороны, ничего. - Воспроизвел поведение в однопоточной команде управления, поэтому оно не связано с проблемой http/web-сервера.
Выявлена первопричина: При сбросе и повторном создании PdfReader в цикле объектов существует низкая вероятность pypdf.PdfWriter.merge
переработать в уже используемые отображения идентификаторов.
Триггером, по-видимому, является сборка мусора в моем цикле, причем psycopg3 действительно просто предоставляет возможность для этого, а не вызывает это.
В соответствующей документации по сбросу ассоциации между исходным и конечным 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