fix(print): copies-Replikation erzeugt UNIQUE job_ids (Round 2 #hangar-109)#120
Conversation
…r-109) User-Bericht 2026-06-14 nach PR #115: SMA-022-003 mit Menge 3 druckt weiterhin nur 1 Etikett. Live-Logs zeigen: Batch 7d8470f4: job 711db9bc skipped — state already printing (cancelled?) Root Cause: PR #115 hat das Image N-mal mit DERSELBEN job_id eingereiht. Der BatchWorker macht für jeden batch.job_ids einen `JobStateMachine.transition(job, PRINTING)` — der zweite Aufruf scheitert mit InvalidStateTransitionError (Job ist bereits PRINTING), das Replikat wird übersprungen. Resultat: nur 1 active_payload an print_multi → 1 Etikett trotz copies=3. Fix: pro Request UND pro Copy eine eigene UUID erzeugen, jeder Copy landet als separater DB-Job. BatchWorker kann sie unabhängig transitionen. API-Vertrag bleibt für Hangar erhalten: - `submit_batch_job` gibt weiterhin 1 master_id pro Request zurück (1:1 mit Hangar-Bestellung), nicht 1:N. - Intern werden N Hub-Job-Records mit copy_index/copy_total/copy_master_id Payload-Metadata persistiert (für Audit + zukünftige UI). Tests: - TestSubmitBatchJobCopies wurde korrigiert — der vorherige Assert "same job_id replicated 3x" war FALSCH (er hatte den Bug bestätigt statt verhindert). Neu: `len(set(kwargs["job_ids"])) == 3`. - master_ids bleibt 1 pro Request, master_ids[0] ist eine der enqueued IDs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! Dieser Pull Request behebt einen kritischen Fehler beim Drucken von Etiketten mit einer Kopienanzahl größer als eins. Zuvor wurden alle Kopien mit derselben Job-ID eingereiht, was dazu führte, dass der BatchWorker die nachfolgenden Kopien als bereits in Bearbeitung befindlich markierte und übersprang. Durch die Zuweisung eindeutiger UUIDs pro Kopie wird sichergestellt, dass der Worker jeden Druckvorgang korrekt verarbeiten kann, ohne die externe API-Schnittstelle zu verändern. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request resolves an issue where enqueuing multiple copies of a print job with the same job ID caused state transition failures in the batch worker. The fix generates a unique UUID for each copy while retaining a master ID reference for the API, and persists each copy independently in the database. The unit tests have been updated to verify this behavior. Feedback on the changes suggests optimizing database persistence by saving the job records concurrently using asyncio.gather instead of sequentially inside the nested loop.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| 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) |
There was a problem hiding this comment.
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)
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #120 +/- ##
==========================================
+ Coverage 89.26% 89.33% +0.07%
==========================================
Files 91 91
Lines 4257 4257
Branches 369 368 -1
==========================================
+ Hits 3800 3803 +3
+ Misses 359 355 -4
- Partials 98 99 +1
... and 3 files with indirect coverage changes
Flags with carried forward coverage won't be shown. Click here to find out more. Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
## <small>0.10.2 (2026-06-15)</small> * refactor(main): _resolve_model_id_from_config nimmt PrinterYAMLConfig statt Any (#67) (#123) ([2ff51d2](2ff51d2)), closes [#67](#67) [#123](#123) [#67](#67) [#59](#59) [#67](#67) [#59](#59) * refactor(snipeit): typisiere response payload als TypedDict statt dict[str, Any] (#67) (#122) ([e88601b](e88601b)), closes [#67](#67) [#122](#122) [#67](#67) [#67](#67) [#51](#51) * perf(lifespan): plugin-discovery in Thread auslagern (Audit #67) (#121) ([48215b4](48215b4)), closes [#67](#67) [#121](#121) [#67](#67) [#59](#59) * fix(config): stricter validation for log_level + webhook_api_key (#46) (#116) ([6b8ac30](6b8ac30)), closes [#46](#46) [#116](#116) [#46](#46) [#46](#46) [#46](#46) [#46](#46) [#116](#116) * fix(print): copies-replication erzeugt UNIQUE job_ids (Round 2 #hangar-109) (#120) ([bd781ae](bd781ae)), closes [#hangar-109](https://github.com/strausmann/label-printer-hub/issues/hangar-109) [#120](#120) [#115](#115) * fix(print): PrintOptions.copies replicates images for hardware print (#hangar-109) (#115) ([d27bbcc](d27bbcc)), closes [#hangar-109](https://github.com/strausmann/label-printer-hub/issues/hangar-109) [#115](#115) [strausmann/hangar#109](https://github.com/strausmann/hangar/issues/109) * docs(metrics): präzisiere sse_events_published_total Beschreibung (Audit #67) (#119) ([ab8c4c4](ab8c4c4)), closes [#67](#67) [#119](#119) [#67](#67) [#67](#67) [#65](#65) * docs(ptouch): korrigiere Tape-Klassen-Namen in ptouch-integration.md (Audit #67) (#118) ([0faab99](0faab99)), closes [#67](#67) [#118](#118) [#67](#67) [#59](#59) * chore(readme): codecov badge ohne embedded Token (Audit #67) (#117) ([4bb5423](4bb5423)), closes [#67](#67) [#117](#117) [#67](#67) [#59](#59) [skip ci]
Akuter User-Bug-Report 2026-06-14
User druckte nach PR #115-Deploy SMA-022-003 mit Menge 3 — erneut nur 1 Etikett.
Root Cause
PR #115 hat das Image N-mal mit DERSELBEN
job_ideingereiht. Live-Logs vom Hub:PrintQueue.BatchWorker(Z.858-880) macht für jedenbatch.job_idseinJobStateMachine.transition(job, PRINTING). Der zweite Aufruf mit demselbenjob_idscheitert mitInvalidStateTransitionError(Job bereits PRINTING), das Image wird übersprungen.Resultat: 1
active_payload→ 1 Etikett. Mein PR #115 Test war ebenfalls falsch — er asserierte explizitlen(set(job_ids)) == 1und hat den Bug damit zementiert.Fix
print_service.py:submit_batch_joberzeugt jetzt:copy_index/copy_total/copy_master_idals Payload-Metadata (Audit)API-Vertrag bleibt für Hangar
submit_batch_jobreturniert weiterhin 1 master_id pro Request (1:1 zur Hangar-Bestellung), nicht 1:N. Hangar muss nichts ändern.Tests
TestSubmitBatchJobCopieskorrigiert:assert len(set(job_ids)) == 1assert len(set(job_ids)) == 3assert len(set(job_ids)) == 2(mixed)assert len(set(job_ids)) == 3Zusätzlich Sicherstellung dass
master_idsweiterhin 1 pro Request bleibt.mypy ✓ ruff check ✓ ruff format ✓
Folge-Action
User soll nach Deploy SMA-022-003 mit Menge 3 erneut drucken → erwarte 3 Etiketten via Half-Cut.
Refs Hangar Issue #109, korrigiert PR #115