Store, rebuild, and operate views
The view store is the publication boundary for a derived answer. It saves the state, per-stream cursors, reducer identity, and processing metadata as one ViewSnapshot. Choose the store and operating model according to the answer's required lifetime and the deployment's trust boundary.
Snapshot fields
| Field | Meaning |
|---|---|
view_id | application-defined identifier; unique inside one engine |
state | reducer-owned, deep-copyable dictionary |
cursors | canonical stream key to opaque history end cursor |
updated_at_ms | time of the most recent non-empty batch represented; 0 for an empty initial or empty-history rebuilt snapshot |
reducer_name | reducer identity used to prevent accidental incremental reinterpretation |
version | reserved value carried in v1; not migration, fencing, or compare-and-swap control |
messages_processed | lifetime number of consumed messages represented by this snapshot |
Supplied stores
| Store | Lifetime and serialization | Appropriate use | Important limit |
|---|---|---|---|
InMemoryViewStore | process-local deep-copied Python values | development, tests, disposable local answers | disappears on restart |
RedisViewStore | JSON snapshot plus per-user index in a deployment namespace | restart-surviving answers in a trusted deployment | durability without writer election, quotas, or shared-hosting certification |
The Redis implementation is constructed from its implementation module:
from servercheetah.module_implementations.views.redis_view_store import RedisViewStore
from servercheetah.views import create_view_engine
view_store = RedisViewStore(
redis_client,
namespace=deployment_namespace,
)
engine = create_view_engine(
history=components.history,
view_store=view_store,
)
Redis keys include the deployment namespace, user ID, and view ID. save() writes the snapshot and updates the user's view index in one transaction. delete() removes both. list_views() is user-scoped and accepts an optional view-ID prefix.
Every IViewStore must own a saved value independently of its caller and return a detached value from load(). It must structurally scope load, save, delete, and list_views by user_id and save state plus cursors atomically.
Those are persistence contracts, not a complete tenant threat model. Reducer code is trusted host code, and client-originated history can still drive CPU and state growth. Derived Views is not in the audited shared-tenancy component family.
Rebuild publishes one complete replacement
Use rebuild after changing reducer logic or state shape, repairing corruption, or deliberately recomputing from retained evidence.
from servercheetah.types.views import ViewRebuildIncompleteError
try:
processed = await engine.rebuild(
view_id,
batch_size=500,
max_iterations=2_000,
)
except ViewRebuildIncompleteError as exc:
log.warning(
"rebuild incomplete after %s messages; prior snapshot retained",
exc.processed_count,
)
raise
rebuild() takes the same process-local per-view lock as process(). It starts from the reducer's initial state, queries retained history in pages, and assembles a private snapshot. Readers continue to see the previous committed snapshot during this work.
The engine publishes once only after an empty query proves that replay is exhausted. A history, reducer, store, or cancellation failure preserves the previous committed snapshot. A successful empty-history rebuild deliberately replaces it with the reducer's initial state.
If max_iterations is reached before an empty read, the engine raises ViewRebuildIncompleteError, publishes no callback or partial state, and preserves the prior snapshot—or leaves the view absent when there was none. The exception reports processed count, stream count, and per-stream and total query-result bounds.
An exactly full final page is conservative: v1 cannot know that it was the last page until it performs another query. If the limit is reached on that full page, retry with a higher explicit limit.
Atomic publication does not provide cross-process fencing. Another engine can still publish the same view independently. Quiesce all other writers or provide deployment-owned election before a rebuild.
Change reducer identity deliberately
Incremental process() compares the stored reducer_name with the current registration. A mismatch raises ViewReducerMismatchError instead of feeding an old state shape to new logic. rebuild() is the sanctioned replacement path and can publish a snapshot under the newly registered reducer name.
Changing implementation while keeping the same name is not detected. Treat reducer names as state contract identifiers—for example, device-status-v2—or version the application-owned view_id. Frozen v1 does not interpret the snapshot's numeric version field or migrate state.
For a high-impact migration, a separate versioned view ID gives the application a shadow-and- promote workflow: register and rebuild the new view, verify its state, switch readers, then remove the old registration and stored snapshot through an explicit retention decision.
Retention sets the rebuild horizon
A rebuild can replay only messages still available in every source stream. If history retention removed an earlier event, the replacement cannot reconstruct its effect. Encrypted payloads also remain encrypted unless application reducer code deliberately has and uses the necessary keys.
Set retention from the maximum acceptable processor outage, backlog time, repair window, and rebuild requirement—not merely the normal processing interval. If a complete all-history answer must survive beyond message retention, preserve it in product-owned durable storage rather than assuming a derived view can recreate it forever.
Operating checklist
Before depending on a view:
- bound its registration population, state bytes, keys, collections, and reducer execution cost;
- choose one processing owner across all application replicas;
- re-register definitions deterministically after restart;
- supervise each important view independently rather than relying on one fail-fast
process_all()call; - monitor last success, failures and skips, consumed counts, snapshot age, and useful lag;
- keep source history long enough for expected lag and repair;
- rehearse
ViewRebuildIncompleteError, reducer mismatch, cancellation, and store failure; and - authorize stored-view deletion separately from process-local unregistration.
Derived views returns to the feature boundary. For general retained-evidence operations, see Diagnostics and observability.