Skip to content

feat(api): serve field lists from the resource-state read model - #333

Open
AlexAxthelm wants to merge 3 commits into
feat/stit-766-writesfrom
feat/stit-766-read-path
Open

AlexAxthelm wants to merge 3 commits into
feat/stit-766-writesfrom
feat/stit-766-read-path

Conversation

@AlexAxthelm

@AlexAxthelm AlexAxthelm commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Fourth PR of splitting #324 into smaller PRs for ease of review. Builds on #332: this is the one where we actually see faster results, because we're wiring it into the read paths.

The list and filter-options endpoints start reading from the precomputed table (when it's ready, and the caller's permissions match one of the stored profiles), falling back to the existing live path otherwise.

It also adds an operator's "off switch" (disabled) and a short per-process readiness cache, so the safety switch ships alongside the fast path. These are useful for comparison to the live coalescer for validity or performance testing, and setting disabled in the DB should start returning results from the live path after a ~5s timeout (part of machinery to prevent needing to check read-state readiness on every request)


Summary

Fourth PR in the STIT-766 read-model stack (builds on #332). List and filter-options now read precomputed rows when they can, taking typical list requests at ~50K resources from ~2–3 s to under 10 ms. Answers are identical either way.

When the fast path is used

All three must hold; otherwise the request is answered live, exactly as before:

  1. The caller's licensed sources exactly match one of the four permission profiles. Partial or unusual grants, and internal unscoped calls, go live.
  2. The table is ready and was built by the same commit the API is running. Every deploy of new code reads live until that code has rebuilt the table.
  3. An operator hasn't switched it off (disabled, below).

The detail endpoint and per-field source listings are unchanged; they always compute live.

Read path (og_field_resource_actions.py, queries.py)

  • The live code is unchanged, just moved into _query_live / _filter_options_live, which query and filter_options call when the fast path doesn't apply.
  • List page: picked and read in one statement (an inner, index-only id page, joined to the rows), so a write landing mid-request can't remove a row between choosing the page and loading it.
    • The count runs without ORDER BY.
    • Rows are turned into list items the same way the live path builds them, including provenance in field order (JSONB doesn't keep key order).
  • Filter-options: one DISTINCT per field, each answered from its (permission_mask, field) index.
  • Query labels:
    • cached list: resources.count + resources.list_page;
    • live list: still resources.count + resources.list_ids + resources.list_hydrate;
    • filter-options: resources.filter_options either way.
    • The readiness lookup is deliberately unlabeled.

Readiness cache (state.py)

  • Each API process checks readiness at most every 5 s, so most requests skip that lookup.
  • A briefly stale answer only delays switching between the read model and live. It never serves a broken page, because the table is never emptied and each process only trusts rows built by its own commit.
  • A rebuild clears this process's cache when it starts and when it finishes, so reads in the rebuilding process switch immediately.

Operator switch-off (new migration 2f2fb644b180)

  • Turning it off: UPDATE og_field_resource_state_status SET disabled = true; sends reads back to live within ~5 s.
  • It stays off: rebuilds (and later the deploy-time rebuild in feat(auth): add stitch-auth package (STIT-333, STIT-335) #5) still keep the rows current, but never clear the flag; they log a warning that it's off. ready = false alone would be undone by the next rebuild.
  • Turning it back on: SET disabled = false.
  • --ids repairs are allowed on a switched-off table, since nothing is served from it.

Docs

  • READ_MODEL.md:
    • who gets the fast path;
    • readiness, including the cache and the switch-off;
    • how to roll it out (manual for now: after the previous API revision stops serving, run --if-stale from the deployed image; feat(auth): add stitch-auth package (STIT-333, STIT-335) #5 automates this);
    • Verifying, with SQL checks and what the query labels mean;
    • when a full rebuild is needed;
    • performance at 50K;
    • the remaining known limitations.
  • HACKING.md: "Fast list reads locally".
  • deployments/api/README.md: a pointer to the runbook.

The stack

  1. Schema (feat(api): add the resource-state read model tables #330)
  2. Build and rebuild (feat(api): build/rebuild the resource-state read model tables #331)
  3. Writes keep the table current (feat(api): keep the resource-state read model current on writes #332)
  4. Read path, readiness cache and switch-off (this PR)
  5. CI: rebuild after each deploy, and wait for the old API revision to stop serving first

Until #5 lands, a deployed environment reads live until someone runs the rebuild by hand. That's safe, just slower.

Related issues

Part of: STIT-766
Replaces: #324 (split for review)

Testing

  • New tests:
    • tests/db/read_model/test_read_path.py:
      • cached list and filter-options match live for every profile, sort, filter, page and q= search;
      • fallback to live when not ready, built by other code, switched off, or the grants don't match a profile;
      • the readiness cache: one lookup for back-to-back requests, changes seen once the window passes, and a rebuild in this process switches reads immediately, including after a read during the rebuild;
      • pages served from stored rows;
      • HTTP responses unchanged.
    • tests/db/read_model/test_read_path_postgres.py (Postgres only): parity on real Postgres, including JSONB provenance key order, and that a page is picked and read in one statement.
    • tests/db/read_model/test_state.py:
      • the switch-off: not ready while set; rebuilds don't re-enable it; --if-stale doesn't rebuild a current, switched-off table; the CLI warns; --ids is allowed;
      • a value today's model rejects fails the list only where live fails.
    • The rebuild scenario now also checks that a reader who passed the readiness check just before a rebuild started still gets the full, correct answer mid-rebuild.
    • tests/observability/test_query_name_actions.py: the read-model list and filter-options labels.
  • Results: the API suite passes on SQLite and against Postgres; make check passes.
  • Migrations: apply cleanly on fresh SQLite and Postgres databases (single head); on Postgres, alembic reports no differences from the models.
  • Deliberate breakage check: I broke each of these by hand and confirmed a test fails:
    • readers ignoring the switch-off;
    • the read path ignoring readiness;
    • a cache that never expires;
    • a rebuild that doesn't clear the cache when it finishes. This one was a gap on Feat/stit 766 resource state #324; the new test above covers it.

Checklist

  • PR is focused on a single concern
  • Tests pass locally and in CI
  • Docs updated for user-visible changes
  • AI-assisted portions declared (see CONTRIBUTING.md)

AI use: Claude Code split this out of #324 and wrote most of the code, tests and docs. I reviewed it before committing. Verified by the tests, migration checks and deliberate breakage check above.

🤖 Generated with Claude Code

…-766)

List and filter-options now read precomputed rows when the caller's
licensed sources exactly match a permission profile and the read model
is ready (built by this commit); otherwise they answer live, unchanged.
At 50K resources list requests drop from ~2-3 s to under 10 ms.

Each process caches readiness for up to 5 s, so most requests skip the
lookup. Operators can switch the fast path off with a new `disabled`
flag that rebuilds never clear, so a deploy's rebuild can't undo it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

CD summary a220742

Frontend: https://witty-mushroom-017a3dc1e-333.westus2.1.azurestaticapps.net

Deployments (3)
service url fqdn
api open pr-0333-api.purplegrass-c07d0a94.westus2.azurecontainerapps.io
frontend https://witty-mushroom-017a3dc1e-333.westus2.1.azurestaticapps.net
stitch-llm open pr-0333-llm.purplegrass-c07d0a94.westus2.azurecontainerapps.io
Database (1)
db_name postgres_host postgres_port postgres_db
pr_0333 stitch-dev.postgres.database.azure.com 5432 pr_0333
Jobs (2)
job image postgres_db api_url auth_mode
db-migrations ghcr.io/rmi/stitch-api:pr-0333@sha256:bace2c260dd0f56725148d0f001d91da340414dc729324414c0d7c4906551354 pr_0333
seed ghcr.io/rmi/stitch-seed:pr-0333@sha256:c78a579670b8320a6699326c1d2a676e72cba2cf43713b3a7ea4c9ae1eb25d43 https://pr-0333-api.purplegrass-c07d0a94.westus2.azurecontainerapps.io/api/v1 stitch-client-bearer-token
Images (3)
build_time commit_time git_sha image image_digest
2026-10-15T15:58:50Z 2026-10-15T15:58:30Z 8984d7a ghcr.io/rmi/stitch-api:pr-0333 ghcr.io/rmi/stitch-api:pr-0333@sha256:bace2c260dd0f56725148d0f001d91da340414dc729324414c0d7c4906551354
2026-10-15T15:58:47Z 2026-10-15T15:58:30Z 8984d7a ghcr.io/rmi/stitch-seed:pr-0333 ghcr.io/rmi/stitch-seed:pr-0333@sha256:c78a579670b8320a6699326c1d2a676e72cba2cf43713b3a7ea4c9ae1eb25d43
2026-10-15T15:58:50Z 2026-10-15T15:58:30Z 8984d7a ghcr.io/rmi/stitch-stitch-llm:pr-0333 ghcr.io/rmi/stitch-stitch-llm:pr-0333@sha256:a196e5ec31933ca473730e0bb09ecb86446fd0be0bae9e4f78dcf5e771bbd9d3

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Cross-version repairs can leave mixed-version rows marked current, and cached readiness can expose incompatible replacements.

2 open findings
What changed in this PR

Builds on #332 by routing field lists and filter options through the precomputed read model when permissions and readiness allow, with live fallback.

Changes:

  • Adds cached list and filter-options queries.
  • Introduces a five-second readiness cache and persistent operator off switch.
  • Adds parity, fallback, concurrency, and observability tests plus rollout documentation.
File Description
HACKING.md Documents local rebuilds.
deployments/​api/​tests/​observability/​test_query_name_actions.py Checks fast-path query labels.
deployments/​api/​tests/​db/​read_model/​test_state.py Tests disabled-state and validation behavior.
deployments/​api/​tests/​db/​read_model/​test_read_path.py Tests parity, fallback, caching, and HTTP responses.
deployments/​api/​tests/​db/​read_model/​test_read_path_postgres.py Tests PostgreSQL parity and page consistency.
deployments/​api/​tests/​db/​read_model/​test_concurrency_postgres.py Extends rebuild concurrency coverage.
deployments/​api/​tests/​db/​read_model/​scenarios.py Checks reads during rebuilds.
deployments/​api/​tests/​conftest.py Resets readiness caching between tests.
deployments/​api/​src/​stitch/​api/​db/​read_model/​state.py Adds readiness caching and invalidation.
deployments/​api/​src/​stitch/​api/​db/​read_model/​rebuild.py Adds disabled-state handling for rebuilds and repairs.
deployments/​api/​src/​stitch/​api/​db/​queries.py Adds stored-state query builders.
deployments/​api/​src/​stitch/​api/​db/​og_field_resource_actions.py Routes eligible reads through stored state.
deployments/​api/​src/​stitch/​api/​db/​model/​og_field_resource_state.py Adds the disabled flag.
deployments/​api/​README.md Links the rollout runbook.
deployments/​api/​READ_MODEL.md Documents operation, verification, and performance.
deployments/​api/​alembic/​versions/​2f2fb644b180_resource_state_disabled_flag.py Migrates the disabled flag.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread deployments/api/src/stitch/api/db/read_model/rebuild.py Outdated
Comment thread deployments/api/src/stitch/api/db/read_model/state.py
This was referenced Oct 16, 2026
AlexAxthelm and others added 2 commits October 16, 2026 11:46
A switched-off table built by commit A accepted a repair from commit B,
leaving B's rows in a table still marked ready as built by A. --if-stale
from A then saw nothing to redo, and switching the table back on served
B's rows under A indefinitely. Apply the version guard whether or not
the table is switched off: repair it from the image that built it, or
rebuild it from the new one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An API process that cached "ready" in the last 5 seconds keeps reading
the table, so a rebuild by other code during that window can hand it
rows the new code just rewrote. Deploys already wait for the previous
revision to stop serving first; say so where rebuilds are documented
and where the cache is defined.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@AlexAxthelm
AlexAxthelm marked this pull request as ready for review October 16, 2026 09:51
@github-actions

Copy link
Copy Markdown

CD summary bdd51c6

Frontend: https://witty-mushroom-017a3dc1e-333.westus2.1.azurestaticapps.net

Deployments (3)
service url fqdn
api open pr-0333-api.purplegrass-c07d0a94.westus2.azurecontainerapps.io
frontend https://witty-mushroom-017a3dc1e-333.westus2.1.azurestaticapps.net
stitch-llm open pr-0333-llm.purplegrass-c07d0a94.westus2.azurecontainerapps.io
Database (1)
db_name postgres_host postgres_port postgres_db
pr_0333 stitch-dev.postgres.database.azure.com 5432 pr_0333
Jobs (1)
job image postgres_db
db-migrations ghcr.io/rmi/stitch-api:pr-0333@sha256:a15bb99b2363b15ce64955371a39c359164a6b79504f0c227a43624633dcba60 pr_0333
Images (3)
build_time commit_time git_sha image image_digest
2026-10-16T09:49:13Z 2026-10-16T09:48:57Z 2552d4b ghcr.io/rmi/stitch-api:pr-0333 ghcr.io/rmi/stitch-api:pr-0333@sha256:a15bb99b2363b15ce64955371a39c359164a6b79504f0c227a43624633dcba60
2026-10-16T09:49:11Z 2026-10-16T09:48:57Z 2552d4b ghcr.io/rmi/stitch-seed:pr-0333 ghcr.io/rmi/stitch-seed:pr-0333@sha256:c03e1bf56f6817b03956b4c3333cbce24b732bf979c92e316eb2a33291639445
2026-10-16T09:49:15Z 2026-10-16T09:48:57Z 2552d4b ghcr.io/rmi/stitch-stitch-llm:pr-0333 ghcr.io/rmi/stitch-stitch-llm:pr-0333@sha256:88aecc360245898097242584378cfa823e2494b39b46b9150638f3c331262d8d

This was referenced Oct 16, 2026

This branch was successfully deployed

1 active deployment
development — bdd51c62 Deployed Oct 16, 2026 by AlexAxthelm via Delete API container app / delete-container-app #353
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants