Summary
A client that creates a row and immediately references it by id in a second
request gets a deterministic 404. The canopy_atlas seed publishes
0/12 pages on its first pass (saved but publish failed (404)); a second,
identical pass publishes all 12. Same for news article attach.
This matters beyond the seed: the official k8s/job-seed runs the seed
once, so a fresh cluster can come up with every page in draft and an empty
public site.
Root cause
simple_module_db.get_db commits after yield (deps.py, ~L46):
async with factory() as session:
yield session
if has_writes:
await session.commit() # runs in the dependency's exit code
FastAPI runs a yield dependency's exit code after the response has been
delivered. Page/article create() deliberately only flush()es — get_db
owns the commit — so:
POST …/pages flushes (page gets an id), returns 201 {id}; the response
reaches the client before the create request's get_db commit runs.
- The client immediately
POST …/pages/{id}/publish.
- That request opens a fresh session; the page isn't committed yet →
get_page → 404.
- The create's commit finally lands, which is why a second pass (page now
present) succeeds.
Deterministic locally because the follow-up request reliably beats the
post-response commit.
Reproduce
POST /api/pagebuilder/pages {…} -> 201 {"id": N}
POST /api/pagebuilder/pages/N/publish -> 404 Page not found
or run python -m canopy_atlas.seed once against a fresh DB → Seeded 0/12.
Impact
Any fast create-then-use client: seeds, provisioning scripts, integration
tests, and the packaged k8s/job-seed.
Fix options (design call)
- Commit before the response is sent — e.g. a route/middleware that
commits the unit of work before Starlette delivers the response, rather than
in the get_db exit code. Fixes every caller.
- An atomic create-and-publish endpoint for the workflow the seed needs.
- Downstream-only mitigation: make the seed resilient (retry / two-pass). This
is what canopy_atlas's seed does today, but the single-run k8s/job-seed
doesn't get that for free.
Found upgrading a downstream app (Global Canopy Atlas); its seed already
tolerates this with a two-pass run, but the k8s job does not.
Summary
A client that creates a row and immediately references it by id in a second
request gets a deterministic
404. Thecanopy_atlasseed publishes0/12 pages on its first pass (
saved but publish failed (404)); a second,identical pass publishes all 12. Same for news article attach.
This matters beyond the seed: the official
k8s/job-seedruns the seedonce, so a fresh cluster can come up with every page in
draftand an emptypublic site.
Root cause
simple_module_db.get_dbcommits afteryield(deps.py, ~L46):FastAPI runs a
yielddependency's exit code after the response has beendelivered. Page/article
create()deliberately onlyflush()es —get_dbowns the commit — so:
POST …/pagesflushes (page gets an id), returns201 {id}; the responsereaches the client before the create request's
get_dbcommit runs.POST …/pages/{id}/publish.get_page→404.present) succeeds.
Deterministic locally because the follow-up request reliably beats the
post-response commit.
Reproduce
or run
python -m canopy_atlas.seedonce against a fresh DB →Seeded 0/12.Impact
Any fast create-then-use client: seeds, provisioning scripts, integration
tests, and the packaged
k8s/job-seed.Fix options (design call)
commits the unit of work before Starlette delivers the response, rather than
in the
get_dbexit code. Fixes every caller.is what
canopy_atlas's seed does today, but the single-runk8s/job-seeddoesn't get that for free.
Found upgrading a downstream app (Global Canopy Atlas); its seed already
tolerates this with a two-pass run, but the k8s job does not.