feat(states): add storage-phase config and task-state repository selector - #380
Conversation
c7ec547 to
d28a45c
Compare
| PROD = "production" | ||
|
|
||
|
|
||
| class StoragePhase(str, Enum): |
There was a problem hiding this comment.
why not just enum? Why is string needed
There was a problem hiding this comment.
Matches the other enums in this file (Environment, EnvVarKeys). Existing code compares raw env strings to those members directly, which only works when members are strings. Also keeps it log/JSON-friendly without .value everywhere. Pydantic would parse either way, so it's more for consistency
| # Resolved through the shared selector (not constructed by hand) so the | ||
| # worker honors TASK_STATE_STORAGE_PHASE the same way the API does. | ||
| task_state_repository = get_task_state_repository() | ||
| task_message_service = TaskMessageService( |
There was a problem hiding this comment.
why not do the same for messages too since youre introducing that env var in this pr?
There was a problem hiding this comment.
The env var and the selector are separate pieces here. The selector only exists where there are two backends to select between. Messages get theirs in M2 (Milestone 2), alongside the Postgres message repo, same as state got its selector in this PR.
The env var can't wait for M2 though: mongodb_required is derived from both phases, so both settings need to exist for it to have its final shape. Until M2 consumes it, startup validation keeps it locked to mongodb.
| """ | ||
| phase = EnvironmentVariables.refresh().TASK_STATE_STORAGE_PHASE | ||
| if phase == StoragePhase.MONGODB: | ||
| return TaskStateRepository(GlobalDependencies().mongodb_database) |
There was a problem hiding this comment.
mongo db class should implement TaskStateRepositoryProtocol no? To keep the two coupled and hold the repository up to protocol standard (no extra methods are added that diverge from mongo vs postgres repository)
There was a problem hiding this comment.
You're right, I just added it. The repo now declares the Protocol as a base, and a test enforces that every protocol method has a real implementation, so any drift fails CI at the class instead of at some call site. The follow-up's Postgres repo gets the same base and test, plus the shared test suite running against both backends, which is what keeps the two aligned.
05a801e to
7aa6ffe
Compare
…ctor Introduce TASK_STATE_STORAGE_PHASE and TASK_MESSAGE_STORAGE_PHASE settings (mongodb|postgres, default mongodb) with a derived mongodb_required property, a TaskStateRepositoryProtocol capturing the contract callers rely on through the DTaskStateRepository seam, and a get_task_state_repository() selector that resolves the backend by phase. The DI seam and both Temporal factories (task retention, scheduled agent runs) now construct the repository through the selector, so a phase switch applies to API handlers and workers alike. The postgres phase raises NotImplementedError until the Postgres repository lands, and the mongodb default keeps existing deployments unchanged.
An accepted-but-unimplemented phase previously surfaced only on first use: request-time 500s from the API and a crash inside Temporal worker wiring. Fail at process startup instead, with the misconfigured variable named in the error. The selector keeps its NotImplementedError as defense in depth for configs constructed outside refresh().
…Protocol Declares TaskStateRepositoryProtocol as a base of TaskStateRepository so conformance is visible at the class and checkable by IDEs. Because an explicit subclass silently inherits the protocol's placeholder methods, a test pins that every required method is a real implementation.
7aa6ffe to
07433d0
Compare
Summary
First PR in a short series adding an optional PostgreSQL backend for task state storage, selected per deployment by a config flag. This PR lands the configuration and the selection seam only — no behavior change: the default phase is
mongodb, and existing deployments continue to run exactly as before. Data-migration phases are out of scope for this series; if that effort happens later, it adds its own phase values (additive, not breaking).What's in this PR
1. Storage-phase settings (
src/config/environment_variables.py)TASK_STATE_STORAGE_PHASEandTASK_MESSAGE_STORAGE_PHASE:mongodb | postgres, defaultmongodb.StoragePhaseenum — a typo fails at startup instead of silently routing to a backend the operator didn't choose.postgres, until it lands in a follow-up) are rejected at config refresh — a misconfigured deployment fails at process startup with the variable named, instead of request-time 500s or a Temporal worker crash mid-wiring.mongodb_requiredproperty (true unless every store's phase ispostgres) for later use in conditional Mongo startup/readiness.2.
TaskStateRepositoryProtocol(src/domain/repositories/task_state_repository.py)DTaskStateRepositoryseam — the states use case and authorization shortcuts (create/get/update/delete/list), the retention service (find_by_field/delete_by_field/batch_create), andget_by_task_and_agent.ItemDoesNotExist/DuplicateItemError, the Mongo-shaped$inlist filter). The MongoDB repository satisfies it unchanged.3. Phase selector wired into the seam
get_task_state_repository()resolves the repository for the configured phase;DTaskStateRepositorynow points at it, so every DI consumer follows automatically.mongodbbranch touches a Mongo handle, which is what will allow running without a MongoDB connection once no store needs one.NotImplementedErrorbranch remains as defense in depth; startup validation (above) makes it unreachable through env config.4. Temporal factories routed through the selector (
src/temporal/task_retention_factory.py,src/temporal/scheduled_agent_run_factory.py)TaskStateRepository(mongodb_database)by hand, bypassing DI. Without this change, apostgres-phase deployment's workers would silently keep using MongoDB while the API used Postgres.Plus docker-compose pass-throughs for both variables (defaulting to
mongodb) on the API and worker services.Review focus
find_by_field/delete_by_field/batch_create— these are part of the contract the future Postgres repository must implement.CRUDRepositoryABC): the ABC'slist()takes no filter/pagination arguments and lacks the task-state-specific methods, and a Protocol requires no change to the MongoDB repository's class hierarchy.Testing
mongodb_requiredmatrix; selector resolution for all phases, including a test pinning that non-mongodb branches never touch Mongo wiring; both Temporal factories resolving through the selector.openapi.yamlregenerated with no diff; ruff clean.Follow-ups (separate PRs)
TaskStateORM+ a schema change creating the new (empty)task_statestable — no data is moved.TaskStatePostgresRepository+ repository tests parametrized over both backends; selector'spostgresbranch goes live.Greptile Summary
The PR adds validated storage-phase configuration and routes task-state repository construction through a shared selector.
Confidence Score: 5/5
The PR appears safe to merge.
No blocking failure remains; the unsupported PostgreSQL phase is now rejected during API and Temporal process startup before repository wiring or request handling.
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD E[Process startup] --> R[EnvironmentVariables.refresh] R --> V{Storage phase supported?} V -->|No| F[Fail startup with named configuration error] V -->|Yes: mongodb| S[get_task_state_repository] S --> M[TaskStateRepository using MongoDB] M --> A[FastAPI DI consumers] M --> T[Temporal factories]Reviews (6): Last reviewed commit: "refactor(states): make the Mongo reposit..." | Re-trigger Greptile