Skip to content

fix(api): Phase 7b.1 — upsert name-collision + /readiness proxy gap#77

Merged
strausmann merged 3 commits into
mainfrom
fix/upsert-runtime-printer-name-collision
May 17, 2026
Merged

fix(api): Phase 7b.1 — upsert name-collision + /readiness proxy gap#77
strausmann merged 3 commits into
mainfrom
fix/upsert-runtime-printer-name-collision

Conversation

@strausmann
Copy link
Copy Markdown
Owner

Summary

Two Phase 7b foundation gaps surfaced by the first hhdocker02 production deploy of PR #75 (784decc).

1. upsert_runtime_printer crashed on upgrade path

When the lifespan ran against the existing Phase 7a DB (one Printer row with a random UUID4, same name as what the new deterministic UUIDv5 wants to write), the INSERT hit UNIQUE constraint failed: printers.name. Crash-loop until the DB was wiped.

Fix: name-fallback lookup when the id-lookup misses. If the same name exists under a different id, delete that row before the new INSERT.

existing = await session.get(Printer, printer_id)
if existing is None:
    result = await session.execute(select(Printer).where(col(Printer.name) == name))
    existing_by_name = result.scalar_one_or_none()
    if existing_by_name is not None:
        await session.delete(existing_by_name)
        await session.flush()
# ... falls through to existing INSERT

New regression test test_upsert_handles_existing_row_with_same_name_different_id seeds a Phase-7a-shaped row and asserts the upsert succeeds with the deterministic id.

2. /readiness returned 404 from the public edge

PR #75 Cluster 3 (H1) proxied /docs, /openapi.json, /redoc through the Go frontend — but missed /readiness. The deep-check endpoint was reachable only on the backend port 8000, not via labels.strausmann.cloud.

Fix: one extra r.Handle(\"/readiness\", prx) next to the existing 3. Test extended to assert all 4 paths preserve through the proxy.

Test plan

  • uv run pytest -q — 677 passed, 3 skipped, 0 failed (was 676 — added one regression test)
  • uv run ruff check . && uv run ruff format --check . && uv run mypy app — clean
  • go test ./...TestProxyMountsBackendDocRoutes now exercises 4 sub-paths, all green
  • go vet ./... — clean
  • Production smoke against labels.strausmann.cloud/readiness after merge + deploy — should return HTTP 200 with status in {ready, degraded}

Deploy notes

  • Production was hot-fixed by wiping the SQLite DB on hhdocker02 (user authorized — "noch nichts live in Verwendung"). The current Phase 7b deploy is working with the deterministic UUIDv5 from a fresh DB.
  • This PR is the proper fix so the NEXT upgrade scenario (if Phase 7c migrates anything to a non-empty DB) doesn't repeat the crash.

Closes #76
Refs #22

Phase 7b's upsert_runtime_printer keyed only on Printer.id (the new
deterministic UUIDv5 from C1). An existing Phase 7a row with a different
id but the same name violated the UNIQUE(name) constraint on the INSERT,
crashing the backend during the first hhdocker02 deploy.

Fix: when the id-lookup misses, fall back to a name-lookup. If a row with
the same name exists under a different id, delete it before the new
INSERT. New regression test mimics the production state.

Refs #76
Refs #22
Phase 7b H1 added /docs, /openapi.json, /redoc proxying but missed
/readiness. Production smoke against labels.strausmann.cloud showed
the readiness deep-check returning 404 from the frontend edge. One
extra r.Handle keeps the path intact and reaches the backend's
existing handler.

Refs #76
Refs #22
Copilot AI review requested due to automatic review settings May 17, 2026 18:47
@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, 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!

This pull request addresses two critical issues identified during production deployment. It resolves a database crash occurring during upgrades due to name collisions in the printer table and fixes a routing gap where the /readiness health check endpoint was inaccessible via the public-facing proxy.

Highlights

  • Database Upsert Fix: Updated the printer upsert logic to handle name collisions during upgrades by deleting existing records with the same name before inserting new ones, preventing unique constraint violations.
  • Proxy Configuration: Exposed the /readiness endpoint through the Go frontend proxy to ensure deep-check health status is accessible from the public edge.
  • Regression Testing: Added a new integration test to verify the upsert behavior with name collisions and updated existing proxy tests to include the /readiness path.
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 17, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.98%. Comparing base (784decc) to head (43386c2).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #77      +/-   ##
==========================================
+ Coverage   91.93%   91.98%   +0.04%     
==========================================
  Files          70       70              
  Lines        3038     3055      +17     
  Branches      259      261       +2     
==========================================
+ Hits         2793     2810      +17     
  Misses        181      181              
  Partials       64       64              
Components Coverage Δ
Printer Backends (transport) 87.50% <ø> (ø)
Printer Models (drivers) 91.42% <ø> (ø)
Services 92.09% <ø> (ø)
REST API 91.30% <ø> (ø)
Pydantic Schemas 100.00% <ø> (ø)
Integration Plugins 100.00% <ø> (ø)
Files with missing lines Coverage Δ
backend/app/db/lifespan.py 90.00% <100.00%> (+2.04%) ⬆️
Flag Coverage Δ
backend 91.98% <100.00%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 784decc...43386c2. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses two production-deploy gaps found after Phase 7b went live: (1) startup failure when upsert_runtime_printer encounters a legacy printers row with a conflicting name, and (2) missing /readiness routing through the Go frontend reverse proxy.

Changes:

  • Add a name-based fallback in upsert_runtime_printer to delete a legacy printer row when the deterministic UUIDv5 lookup misses but name collides.
  • Add /readiness to the Go frontend proxy allowlist alongside /docs, /openapi.json, and /redoc.
  • Add/extend regression tests for both the DB upgrade-path collision and the proxy route coverage.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
frontend/cmd/server/main.go Proxies /readiness to the backend alongside other FastAPI utility routes.
frontend/cmd/server/main_test.go Extends proxy-mount test coverage to include /readiness.
backend/tests/integration/db/test_lifespan_printer_upsert.py Adds regression test seeding a legacy printer row with same name but different id.
backend/app/db/lifespan.py Implements name-collision fallback behavior during runtime-printer upsert.

Comment thread backend/app/db/lifespan.py Outdated
Comment thread backend/app/db/lifespan.py Outdated
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request addresses a database unique constraint conflict (issue #76) by implementing a cleanup step during the application lifespan to remove legacy printer records that share names with new deterministic UUIDv5 entries. It also introduces a /readiness endpoint to the frontend server and includes a regression test for the database fix. Feedback focuses on a potential IntegrityError during the deletion of printer records if cascading deletes are not configured, which could lead to startup crashes. Additionally, a suggestion was made to use standard SQLAlchemy imports instead of aliases to maintain consistency with the rest of the codebase.

Comment thread backend/app/db/lifespan.py
Comment thread backend/app/db/lifespan.py Outdated
Bot reviews on PR #77 flagged that session.delete(existing_by_name)
would crash with FOREIGN KEY violations on any real upgrade scenario
(jobs/printer_state/printer_status_cache referencing the old printer_id).
The previous fix only worked because the production smoke test ran on
a wiped DB with no dependent rows — Finding #1 (HIGH).

Replace the delete-then-insert path with non-destructive id migration:
cascade-rewrite FK columns in printer_status_cache, printer_state, and
jobs, then UPDATE the printer row's PK in-place. Historical print jobs
survive the upgrade.

SQLite stores UUIDs as 32-char hex strings without dashes; raw SQL
bind params use str(uuid).replace('-', '') to match the on-disk format.
After raw SQL UPDATEs the ORM identity map is expired so the migrated
row is visible on the next session.get() call.

Also incorporates two MEDIUM findings:
- Finding #2: use app.repositories.printers.get_by_name helper instead
  of re-implementing the select.
- Finding #3: drop the _select alias, use select directly like the rest
  of lifespan.py.

New regression test seeds an old printer row WITH dependent jobs,
printer_state, and printer_status_cache rows, then confirms the upsert
migrates all FK references to the new deterministic UUIDv5 without
losing data.

Refs #76
Refs #22
@strausmann strausmann merged commit 4e74a03 into main May 17, 2026
19 checks passed
@strausmann strausmann deleted the fix/upsert-runtime-printer-name-collision branch May 17, 2026 20:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Phase 7b.1: upsert_runtime_printer schlägt fehl bei Upgrade von bestehender DB (UNIQUE name)

2 participants