Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 41 additions & 37 deletions backend/app/services/print_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,51 +164,55 @@ async def _prepare_one(
prepared = await asyncio.gather(*[_prepare_one(r) for r in requests])

# 3. Pre-allocate job UUIDs + persist in JobStore
# Pro Request 1 Job persistieren (DB-Tracking bleibt 1:1 zur Hangar-Bestellung).
# Image-Replication für copies > 1 kommt in Schritt 4 (Batch-Enqueue).
job_ids: list[UUID] = []
for request, (_img, ld_dump) in zip(requests, prepared, strict=True):
job_id = uuid4()
db_job = Job(
id=job_id,
printer_id=self._printer_id,
template_key=None,
payload={
"tape_mm": tape_mm,
"content_type": str(request.content_type),
"rendered_tape_mm": tape_mm,
"options": request.options.model_dump(),
"label_data": ld_dump,
},
api_key_id=None,
source_ip=None,
)
await self._store.save_queued(db_job)
job_ids.append(job_id)

# 3b. Image-Liste für die Hardware aufbauen — Bug 2026-06-14:
# `PrintOptions.copies > 1` ist im DB-Schema vorgesehen, wurde aber
# bisher nicht in die Hardware-Druck-Liste übersetzt. Effekt: ein Request
# mit copies=3 schickte nur 1 Image an print_multi() → 1 Etikett statt 3.
# Fix: pro Request das Image so oft replizieren wie options.copies sagt.
# job_ids werden parallel repliziert damit die Längen-Validierung in
# enqueue_batch passt; duplizierte UUIDs landen einmal im self._jobs-Dict
# (Dict-Set überschreibt). Der Batch-Worker markiert je Job-ID einmal
# done — Hangar-Bestellung bleibt 1:N mit den replizierten Etiketten.
# Bug 2026-06-14 Round 2: PR #115 hatte das Image N-mal mit DERSELBEN
# job_id eingereiht — der BatchWorker scheitert dann beim zweiten
# `JobStateMachine.transition(job, PRINTING)` mit
# InvalidStateTransitionError (Job ist bereits PRINTING) und skippt
# die Replikate. Live-Log:
# "Batch ...: job 711db9bc skipped — state already printing"
# Resultat: 1 Etikett trotz copies=3.
#
# Fix: pro Request UND pro Copy einen eigenen DB-Job-Record mit
# eindeutiger UUID anlegen, damit der Worker jeden Druck unabhängig
# transitionen kann. Hangar-Bestellung bleibt 1:1 zur Hangar-Sicht —
# wir geben nur den ersten ("master") Job pro Request zurück, alle
# Replikate landen aber in der Hub-DB für Audit/Logging.
job_ids: list[UUID] = [] # master IDs, 1 pro request — Hangar-API
all_job_ids: list[UUID] = [] # alle inkl. Copies — für enqueue_batch
images: list[Image.Image] = []
expanded_job_ids: list[UUID] = []
for req, (img, _ld), jid in zip(requests, prepared, job_ids, strict=True):
n = max(1, int(req.options.copies))
for _ in range(n):
for request, (img, ld_dump) in zip(requests, prepared, strict=True):
n = max(1, int(request.options.copies))
master_id = uuid4()
job_ids.append(master_id)
for copy_idx in range(n):
this_id = master_id if copy_idx == 0 else uuid4()
db_job = Job(
id=this_id,
printer_id=self._printer_id,
template_key=None,
payload={
"tape_mm": tape_mm,
"content_type": str(request.content_type),
"rendered_tape_mm": tape_mm,
"options": request.options.model_dump(),
"label_data": ld_dump,
"copy_index": copy_idx,
"copy_total": n,
"copy_master_id": str(master_id),
},
api_key_id=None,
source_ip=None,
)
await self._store.save_queued(db_job)
images.append(img)
expanded_job_ids.append(jid)
all_job_ids.append(this_id)
Comment on lines +180 to +208

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Awaiting self._store.save_queued(db_job) sequentially inside a nested loop can introduce significant latency, especially when printing multiple copies or processing large batches. Since the JobStore protocol guarantees that implementations are safe for concurrent calls from multiple asyncio tasks, we can collect all the save coroutines and execute them concurrently using asyncio.gather to improve performance.

        job_ids: list[UUID] = []  # master IDs, 1 pro request — Hangar-API
        all_job_ids: list[UUID] = []  # alle inkl. Copies — für enqueue_batch
        images: list[Image.Image] = []
        save_tasks = []
        for request, (img, ld_dump) in zip(requests, prepared, strict=True):
            n = max(1, int(request.options.copies))
            master_id = uuid4()
            job_ids.append(master_id)
            for copy_idx in range(n):
                this_id = master_id if copy_idx == 0 else uuid4()
                db_job = Job(
                    id=this_id,
                    printer_id=self._printer_id,
                    template_key=None,
                    payload={
                        "tape_mm": tape_mm,
                        "content_type": str(request.content_type),
                        "rendered_tape_mm": tape_mm,
                        "options": request.options.model_dump(),
                        "label_data": ld_dump,
                        "copy_index": copy_idx,
                        "copy_total": n,
                        "copy_master_id": str(master_id),
                    },
                    api_key_id=None,
                    source_ip=None,
                )
                save_tasks.append(self._store.save_queued(db_job))
                images.append(img)
                all_job_ids.append(this_id)

        await asyncio.gather(*save_tasks)


# 4. Enqueue as BatchJob
first_options = requests[0].options
await self._queue.enqueue_batch(
printer_id=self._printer_id,
images=images,
job_ids=expanded_job_ids,
job_ids=all_job_ids,
tape_mm=tape_mm,
options={
"auto_cut": first_options.auto_cut,
Expand Down
18 changes: 13 additions & 5 deletions backend/tests/unit/services/test_print_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,13 +182,18 @@ async def test_copies_3_yields_three_images(self, make_service) -> None:
),
options=PrintOptions(copies=3),
)
await svc.submit_batch_job([request], half_cut=True)
master_ids = await svc.submit_batch_job([request], half_cut=True)
queue.enqueue_batch.assert_awaited_once()
kwargs = queue.enqueue_batch.await_args.kwargs
assert len(kwargs["images"]) == 3, "copies=3 must yield 3 images"
assert len(kwargs["job_ids"]) == 3, "job_ids must match images length"
# Alle drei job_ids zeigen auf den gleichen Hangar-Request-Job
assert len(set(kwargs["job_ids"])) == 1, "same job_id replicated 3x"
# Bug Round 2: jeder Copy braucht eine eigene job_id, sonst skippt
# der BatchWorker die 2. und 3. Etiketten als "already printing".
assert len(set(kwargs["job_ids"])) == 3, "each copy needs unique job_id"
# Hangar-API: trotzdem nur 1 master-ID pro Request zurueck (1:1 mit
# Hangar-Bestellung), der erste der drei Hub-internen job_ids.
assert len(master_ids) == 1
assert master_ids[0] in kwargs["job_ids"], "master id must be one of the enqueued job_ids"

@pytest.mark.asyncio
async def test_copies_1_yields_single_image(self, make_service) -> None:
Expand Down Expand Up @@ -219,11 +224,14 @@ async def test_mixed_copies_per_request(self, make_service) -> None:
data=RawLabelData(qr_payload="https://example.com/b"),
options=PrintOptions(copies=1),
)
await svc.submit_batch_job([req1, req2], half_cut=False)
master_ids = await svc.submit_batch_job([req1, req2], half_cut=False)
kwargs = queue.enqueue_batch.await_args.kwargs
assert len(kwargs["images"]) == 3, "copies=2 + copies=1 = 3 images"
assert len(kwargs["job_ids"]) == 3
assert len(set(kwargs["job_ids"])) == 2, "two distinct request-jobs"
# Bug Round 2: 3 unique job_ids — 2 fuer req1 (copies=2) + 1 fuer req2.
assert len(set(kwargs["job_ids"])) == 3, "all 3 copy jobs must be unique"
# 1 master_id pro Request (Hangar-API): 2 Requests → 2 master ids.
assert len(master_ids) == 2


# ---------------------------------------------------------------------------
Expand Down
Loading