Add pluggable storage backends (SQLite/Redis/Postgres+Redis hybrid) (ar-r82f.3)#91
Merged
Merged
Conversation
Introduces a pluggable StorageBackend ABC plus a factory that selects SQLite (default), Redis, or a Postgres+Redis hybrid at startup, matching the seller-agent's storage architecture. The existing DealStore is left unchanged and remains the synchronous SQLite path used by CrewAI agents; callers may migrate to the pluggable backend incrementally. Why pluggable - SQLite's single-writer constraint blocks horizontal scaling. - Sessions, caches, rate limits, and locks want native TTL and pub/sub. - Hybrid mode routes durable business data (deals, campaigns, orders, conversions, optimization decisions, experiments, supply path scores, quotes, negotiations, model artifacts, pacing) to Postgres and ephemeral data (sessions, caches, locks, pubsub, rate limits) to Redis. What's added - src/ad_buyer/storage/base.py — StorageBackend ABC plus async domain helpers (deal/campaign/order/session/conversion/optimization/ experiment/supply_path/quote/negotiation/model/pacing operations). - src/ad_buyer/storage/factory.py — get_storage_backend() and a global get_storage() / close_storage() helper. - src/ad_buyer/storage/sqlite_backend.py — aiosqlite KV implementation. - src/ad_buyer/storage/redis_backend.py — redis.asyncio KV implementation with namespaced keys, pubsub, and incr helpers; gated by optional extra so the base install stays lean. - src/ad_buyer/storage/postgres_backend.py — asyncpg JSONB KV implementation with connection pooling; gated by optional extra. - src/ad_buyer/storage/hybrid_backend.py — prefix-based routing between Postgres and Redis backends. - src/ad_buyer/storage/__init__.py — merged exports: legacy DealStore symbols plus the new StorageBackend / get_storage_backend factory. - tests/unit/test_storage_backend.py — 27 tests covering SQLite KV, domain helpers, factory selection, Postgres/Redis init, hybrid routing (1 skipped when redis package not installed locally). Configuration - New settings: storage_type, postgres_pool_min, postgres_pool_max. All other settings preserved; defaults keep SQLite behavior unchanged. - .env.example expanded with STORAGE_TYPE / POSTGRES_POOL_* and hybrid examples; existing entries untouched. - pyproject.toml gains optional extras: [redis], [postgres], [production]. Base dependencies and the crewai==1.10.1 pin are preserved. Docs - New docs/architecture/storage-backends.md page describing the three backends, prefix routing, optional extras, configuration reference, and a migration sketch from SQLite to Hybrid. - Cross-links added from deal-store.md, overview.md (nav table), configuration.md, deployment.md, deployment-ops-guide.md, and mkdocs.yml so the new page is discoverable. Backwards compatibility - No upstream behavior changes. Existing DealStore callers are unaffected. The new factory is opt-in: setting STORAGE_TYPE=sqlite (or leaving it unset) preserves today's behavior. - The new redis / asyncpg drivers are gated behind optional extras and imported lazily by the factory, so they are not loaded unless the corresponding backend is selected. bead: ar-r82f.3 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The CI workflow sets REDIS_URL=redis://localhost:6379/0 in the Test job env. With that ambient value, the two negative-path factory tests fell through their `if not redis_url` guard and tried to construct a RedisBackend without the redis package installed, surfacing as ImportError instead of the expected ValueError. Monkeypatch settings.redis_url to None inside each test and pass redis_url=None explicitly, so the factory's missing-URL branch fires regardless of the ambient environment. bead: ar-r82f.3 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a pluggable
StorageBackendabstraction with four concrete implementations — SQLite (default), Redis, PostgreSQL, and a Hybrid mode that routes durable data to Postgres and ephemeral data to Redis. This mirrors the seller-agent's storage architecture and gives the buyer agent a path off the single-writer SQLite constraint that today caps deployments at one ECS task.The legacy
DealStoreis untouched and remains the synchronous SQLite path used by CrewAI agents. Both APIs coexist; callers can migrate to the pluggable backend incrementally.Why
DesiredCount: 1. Hybrid mode lifts that.connect/disconnect/get/set/delete/exists/keys+ domain helpers) and same factory pattern.What's added
Storage code (
src/ad_buyer/storage/):base.py—StorageBackendABC and async domain helpers (deal, campaign, order, session, conversion, optimization decision, experiment, supply path score, quote, negotiation, model artifact, pacing snapshot).factory.py—get_storage_backend()selects SQLite / Redis / Hybrid from settings or explicit args; lazy-imports Redis/asyncpg so the base install stays lean.sqlite_backend.py—aiosqliteKV implementation with optional TTL.redis_backend.py—redis.asyncioKV with namespaced keys, pluspublish/incr/get_statshelpers. Importable without Redis installed (raises a clearImportErroronly on construction).postgres_backend.py—asyncpgJSONB KV with connection pooling (pool_min/pool_max).hybrid_backend.py— Prefix-based routing:session:,session_index:,cache:,lock:,pubsub:,rate_limit:go to Redis; everything else to Postgres.__init__.py— Merged exports: legacyDealStore+ newStorageBackend/get_storage_backend.Tests (
tests/unit/test_storage_backend.py):Config:
storage_type,postgres_pool_min,postgres_pool_max. All existing settings preserved; defaults keep SQLite behavior unchanged..env.examplegainsSTORAGE_TYPE,POSTGRES_POOL_*, and hybrid usage examples.pyproject.tomladds optional extras[redis],[postgres],[production]. Base dependencies and thecrewai==1.10.1pin are preserved.Docs:
docs/architecture/storage-backends.mdwith backend selection table, three-backend reference, routing rules, optional-extras install commands, configuration reference, and migration sketch.deal-store.md,overview.md,configuration.md,deployment.md,deployment-ops-guide.md, andmkdocs.ymlso the page is discoverable.Optional extras pattern
The factory lazy-imports
redisandasyncpg, so they're never loaded unless the corresponding backend is selected.Backwards compatibility
DealStorecallers are unaffected.STORAGE_TYPEunset (or set it tosqlite) to keep today's behavior.What's left for follow-on
Callers still use
DealStoredirectly. Migrating other domain stores (campaigns, orders, sessions, conversions, optimization, experiments, pacing) to the pluggable backend is incremental and out of scope here. The ABC includes the helper methods those migrations will need.Context
ar-r82f.3(parent epicar-r82f).Test plan
test_storage_backend.py: 26 passed + 1 skipped locally; Redis test skips whenredispackage not installed).python -c "from ad_buyer.storage import StorageBackend, get_storage_backend, DealStore"works in a freshuv sync --extra devenv.settings.database_url,settings.redis_url, etc. unchanged; newsettings.storage_type = "sqlite"by default.pip install -e ".[redis]"makesRedisBackendinstantiable;pip install -e ".[postgres]"makesPostgresBackendconnectable.deal:X) land in Postgres; ephemeral keys (e.g.session:X) land in Redis (verified in test via dual-SQLite stand-ins).🤖 Generated with Claude Code