Remove the in-memory message store backend (#3159) - #3170
Conversation
Redis Streams (RedisMessageStore) is now the only message-store backend. Follow-up to #2662 / PR #3153, which pinned every deployment to explicit redis mode and made the in-memory backend reachable only by accident — re-introducing the #3076 mid-phase-restart message-loss risk. Production: - message_store.py shrinks to the shared types (Message, MessageType, GetMessagesMeta, HEARTBEAT_STATES, coerce_deprecated_message_type) and the singleton accessor. The ~370-line in-memory MessageStore (incl. the blocking-Condition machinery) is gone. - Backend selection collapses: EGG_MESSAGE_STORE_BACKEND unset or "redis" selects Redis; the removed multi-backend-era values ("memory"/"auto") and unknown values raise at creation, as does an unreachable Redis — no fallback. - The #3077 slice-6 fail-loud scaffolding that existed only because the auto→memory fallback was possible is removed: MEMORY_FALLBACK_MARKER, is_memory_fallback_degraded(), _reset_memory_fallback_state_for_test(), and the degraded components.message_store entry in /api/v1/health. Tests: - orchestrator/tests/conftest.py installs a session-scoped fakeredis-backed creator for get_message_store(), replacing the silent auto→memory fallback unit tests used to land on. - test_message_store.py is rewritten around the type surface and the redis-only fail-loud creation semantics; the in-memory behavioral suite is dropped where test_redis_message_store.py already pins the same contracts, and the plural from_roles / slice filter matrix — previously pinned only against the in-memory store — is ported to the redis unit file. - Direct MessageStore() constructions migrate to fakeredis-backed RedisMessageStore; MagicMock(spec=MessageStore) sweeps to spec=RedisMessageStore; the dual-backend parametrizations in test_pipelines_status_wait_route.py and the #2640 regression tier drop their in-memory arm. - integration_tests/test_message_store_backend.py now pins that /api/v1/health carries NO message_store component. Docs/manifests: concurrent-execution.md, coordination-state.md, STRUCTURE.md, and the k8s manifest comments describe the redis-only world; the explicit EGG_MESSAGE_STORE_BACKEND=redis pin stays as deployed documentation. Closes #3159
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
No agent-mode design concerns.
This is an infrastructure refactor (removing the in-memory MessageStore backend in favor of Redis-only) touching the message store, health routes, k8s manifests, tests, and docs. It doesn't touch prompt construction, model selection, LLM invocation paths, or any agent input/output surface — so none of the agent-mode design guidelines apply. No direct Anthropic API calls, pinned model identifiers, human-facing structured-output requirements, pre-fetching, or post-processing pipelines were introduced.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Review: Remove the in-memory message store backend (#3159)
I traced the full changeset — production selection logic, the deleted in-memory class and its consumers, the fakeredis test seam, and every mechanical sweep — and verified the cross-module wiring rather than reviewing files in isolation. This is a clean, well-scoped deletion. No blocking issues. Details below, then two minor non-blocking notes.
What I verified
No dangling references to deleted symbols. Grepped all production code for MessageStore (the class), is_memory_fallback_degraded, MEMORY_FALLBACK_MARKER, _reset_memory_fallback_state_for_test, and MESSAGE_STORE_AUTO_FALLBACK_TO_MEMORY — zero hits outside docstrings and .egg-state/ artifacts. Every production importer of message_store pulls only retained symbols (Message, MessageType, GetMessagesMeta, coerce_deprecated_message_type, get_message_store). The _reset_message_store_fallback autouse fixtures and their local import message_store were removed together, so no orphaned F401 imports remain (consistent with the clean make lint claim, and tests/+orchestrator/tests/ show no residual message_store references in the three health/wedge files).
Fail-loud selection is real, not just documented. _create_message_store raises RuntimeError (with the #3159 pointer) on any backend value other than redis/unset, before importing the redis module. get_redis_message_store issues client.ping() and raises ConnectionError on an unreachable Redis — so the "no fallback" claim holds end-to-end. The k8s comment's "failed creation leaves the singleton unset → lazy retry" is also accurate: the exception propagates out of _create_message_store() before _message_store/_redis_store are assigned, so the next get_message_store() retries cleanly.
No deployed behavior change. k8s/base/orchestrator-deployment.yaml already pinned EGG_MESSAGE_STORE_BACKEND=redis (#2662), and the new integration test pins both the manifest value and the absence of the components.message_store health entry. The only behavioral change is for an unset env var (now redis/fail-loud instead of auto/memory-fallback) — exactly the #3076 risk surface the issue targets.
Tests exercise the production path. The session-scoped conftest fixture rebinds _create_message_store to build a real RedisMessageStore over fakeredis.FakeRedis(), so the unit tier runs the actual store class (XADD/XREAD/XRANGE), not a stub. The TestBackendCreation tests capture the genuine pre-patch _create_message_store at import time and mock only get_redis_message_store, correctly covering unset→redis, explicit redis, removed memory/auto→raise, unknown→raise, and connection-failure-propagates. The ported from_roles/slice filter matrix in test_redis_message_store.py and the _resolve_tip_stream_id handshake in test_host_wait_integration.py both drive real RedisMessageStore methods (confirmed both exist). The MagicMock(spec=MessageStore) → spec=RedisMessageStore sweep is valid — RedisMessageStore exposes the full interface (add_message, get_messages, get_messages_with_meta, get_latest_id, get_status, clear). No self-seeding goldens; the one renamed test (test_in_memory_fallback_without_wait → test_wait_returns_immediately_when_messages_exist) has a body that matches its new name. The pre-existing _FakeMessageStore doubles for reconstruct_tracker_from_messages retain real-store coverage via test_brc_gap_audit.py (now fakeredis-backed).
Non-blocking suggestions
-
test_unset_env_selects_redisisn't hermetic w.r.t.EGG_MESSAGE_STORE_BACKEND. It setsREDIS_HOST/PORT/DBbut never clears the backend var, relying on the ambient pytest env having it unset. If a runner ever exportsEGG_MESSAGE_STORE_BACKEND=memory/auto, the test errors on the unmocked raise rather than asserting the unset→redis contract it's named for. Addmonkeypatch.delenv("EGG_MESSAGE_STORE_BACKEND", raising=False)to make the "unset" precondition explicit. -
(trivia, not introduced here)
redis_message_store.py:102uses the parenthesis-freeexcept json.JSONDecodeError, TypeError:(PEP 758, valid only on the repo's requiredpython>=3.14). It's correct and catches both types — I verified — but it reads like the Python-2except E, name:form to anyone scanning quickly. Since the PR is already rewriting this module's header, wrapping it asexcept (json.JSONDecodeError, TypeError):would remove the double-take. Entirely optional.
Nice cleanup — deleting ~1,500 lines of a second behavioral contract that could only drift is the right call, and the migration kept the real coverage.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
| egg is addressing review feedback... |
Review feedback dispositionsBoth reviews ( 1. 2. Parenthesize — Authored by egg |
|
egg feedback addressed. View run logs 4 previous review(s) hidden. |
Closes #3159.
Problem
#2662 / PR #3153 pinned every deployment to
EGG_MESSAGE_STORE_BACKEND=redis, so the in-memoryMessageStoreno longer ran anywhere — but the backend, theauto/memoryselection modes, the #3077 slice-6 auto→memory fail-loud scaffolding, and ~1,500 lines of in-memory unit tests all still existed. Two store implementations means every behavioral contract (blocking reads, full-history cursor fallback, filters) maintained twice and free to drift, plus the #3076 restart-loss risk if the in-memory store were ever selected again by accident.Changes
Redis-only store.
orchestrator/message_store.pyshrinks to the shared types (Message,MessageType,GetMessagesMeta,HEARTBEAT_STATES,coerce_deprecated_message_type) and theget_message_store()singleton accessor — the typing seam stays where every consumer already imports it;RedisMessageStorekeeps its own module. The ~370-line in-memory class (including the blocking-Conditionmachinery added to mirror XREAD BLOCK) is deleted.Fail-loud selection. This deliberately supersedes the #3077 HITL Q3 freeze, per the issue:
EGG_MESSAGE_STORE_BACKENDunset or"redis"selects Redis (there is nothing else the variable could mean); the removed multi-backend-era values"memory"/"auto"and any unknown value raise at creation with a pointer to #3159, and an unreachable Redis raises instead of falling back. The k8s manifest keeps the explicitredispin as deployed documentation (still pinned by the integration test).Fallback machinery removed.
MEMORY_FALLBACK_MARKER,is_memory_fallback_degraded(),_reset_memory_fallback_state_for_test(), and the degradedcomponents.message_storeentry in/api/v1/healthexist only because auto→memory was possible — all gone, along with their per-test reset fixtures. The TASK-4-3 probe-isolation regression lock (health must not call into the store) is retained.Tests migrate to fakeredis.
orchestrator/tests/conftest.pyinstalls a session-scoped fakeredis-backed creator behindget_message_store(), replacing the silent auto→memory fallback the unit tier used to land on (reset_message_store()mid-test re-creates against fakeredis too).test_message_store.pyis rewritten around the type surface + redis-only creation semantics; in-memory behavioral tests are dropped wheretest_redis_message_store.pyalready pins the same contract, and the plural-from_roles/slice filter matrix — previously unit-pinned only against the in-memory store — is ported to the redis unit file. DirectMessageStore()constructions become fakeredis-backedRedisMessageStore;MagicMock(spec=MessageStore)sweeps tospec=RedisMessageStore; the dual-backend parametrizations (test_pipelines_status_wait_route.py, the #2640 regression tier) drop their in-memory arm. The/status/waitblocking-read handshake that used to pokestore._condre-lands as an instrumented_resolve_tip_stream_idsnapshot event.Docs/manifests.
concurrent-execution.md,coordination-state.md(wipe-semantics table row retired, slice-6 history preserved),STRUCTURE.md, and the k8s manifest comments now describe the redis-only world;integration_tests/test_message_store_backend.pypins that/api/v1/healthcarries nomessage_storecomponent.Testing
orchestrator/tests: 6,674 passed, 1 failed —test_state_store_wedge_propagation.py::test_probe_skipped_when_request_context_missing, verified failing identically on pristine main on this host (environmental, pre-existing).tests/: 7,257 passed (2 pre-existing reap-script host failures, known from #3115).integration_tests/regressionmessage-bus + BRC gap-audit tiers: 39 passed.make lintclean.Out of scope
Message-routing test coverage on top of the store (#2640/#2661) and the third-party-image supply-chain decision (#3154), per the issue.