Valkey owned-state measurement

Verified against source commit 838a464a094b6fa5cc70507aeee5f3c3dc1c3dc9. Library tests and live local-estate evidence are recorded in the 2026-09-20 adoption receipt. Re-verification commands and endpoint paths are provided below.

StellaOps.Doctor.Valkey.ValkeyOwnedStateMeasurer implements the per-service IOwnedStateMeasurer contract. A host supplies its existing connection, its owned key pattern, a stable state ID and its database number. The host retains the connection lifetime. OSM-2 adopted the four web roles below in the local estate; registering a check does not set a capacity budget.

For host registration, AddValkeyOwnedStates accepts an owner identifier, a configuration factory and named key/prefix definitions. It opens one connection lazily for that owner and disposes it with the service provider. Configuration and connection failures occur inside the check, so they produce Warning/unhealthy instead of failing endpoint composition. Successful checks include state.keys.count and state.stream.entries beside state.size.bytes; all three come from the same completed observation. Concurrent requests retain separate observations. The configuration factory must use the owner’s existing settings.

Size means the sum of MEMORY USAGE <key> SAMPLES 0 for the distinct keys returned by a complete SCAN ... MATCH <pattern>. It includes each selected key’s Valkey memory overhead. It excludes unrelated keys, other databases and instance-wide allocator/process overhead. MeasureAsync additionally returns the distinct key count and summed XLEN for stream keys. Those counts are diagnostics; bytes are the quantity compared by OwnedStateSizeBudgetCheck.

services.AddOwnedStateSizeCheck(provider => new ValkeyOwnedStateMeasurer(
    provider.GetRequiredService<IConnectionMultiplexer>(),
    "valkey:my-service-cache", "my-service:cache:*", database: 0));

The host must already register its own connection and per-service Doctor checks. An exact key can be the pattern when a state is one stream. Avoid overlapping patterns when combining totals. Do not use * to represent one service’s share of a shared database. Redis glob metacharacters in literal prefixes must be escaped by the host when constructing a pattern.

The budget key in this example is Doctor:State:valkey:my-service-cache:BudgetBytes. Without a positive budget, the existing check reports measured bytes at Info. At 80 percent it reports Warning; at 100 percent it reports Critical/unhealthy. This observation is not a write reservation or an atomic snapshot: concurrent writes can change a store during SCAN. Write admission remains the owner’s responsibility.

Only one connected standalone primary is supported. Cluster, replica-only, disconnected or ambiguous topologies fail. SCAN errors, unavailable memory, disappearing keys, invalid responses, arithmetic overflow and a changed primary invalidate the entire result. The Doctor check reports Warning/unhealthy and error.type, with no partial state.size.bytes; exception messages and raw keys are not exposed. Caller cancellation propagates.

The constructor bounds work to 1,000,000 distinct keys and 100,000 SCAN pages by default. Operators can configure those measurement limits independently of the byte budget. Reaching either limit fails measurement; it never truncates to a healthy total. SAMPLES 0 measures all nested elements and can be costly for large collections, so schedule checks at a cadence appropriate to the owner.

Verification: dotnet test src/__Libraries/__Tests/StellaOps.Doctor.Valkey.Tests/StellaOps.Doctor.Valkey.Tests.csproj. The integration test starts an isolated Valkey container and checks database and pattern isolation against direct memory measurements, plus the real budget verdict.

Host adoption

The following web roles passed OSM-2 live verification on 2026-09-20. Use their scoped endpoints with ops.health to repeat the read; do not use the retired flat /doctor/checks path. The dated receipt records 15 unbudgeted observations and 401/403/200 access controls for every host.

Doctor endpointState IDs after valkey:Source of selection
/doctor/scanner-web/checksscanner-jobs, scanner-jobs-dead, scanner-job-idempotency; scanner-events when publishing is enabledActual Scanner queue options and Redis event settings.
/doctor/jobengine-web/checksjobengine-planner, jobengine-planner-dead, jobengine-planner-idempotency, and the corresponding jobengine-runner statesScheduler queue options, including configured stream names and database.
/doctor/notify-web/checksnotify-stream:<stream>, notify-idempotency:<stream>, notify-events-deadNotify queue options; each configured stream is measured. The existing binder retains the initialized default stream when appending configured entries.
/doctor/router-gateway/checksrouter-requests, router-cancels, router-responsesEnabled Valkey messaging transport; request and cancellation templates span their service/connection families.

The gateway’s own endpoint requires the canonical ops.health scope through gateway:health:read and bypasses route dispatch only for its exact path. Other hosts’ Doctor paths still route to their owners. Gateway adoption adds no database authority.

Missing queue configuration produces an explicit measurement warning. A legacy Scanner:Queue:Dsn value does not imply that scanner:queue:redis:connectionString has configured the current durable queue. The observer does not introduce an alias between those separate settings or activate queue/event publishing.

The retained stream:orchestrator:events key was written by retired VexLens, not by the current Scanner publisher. On 2026-09-20 the live host had publishing disabled and configured stella.events; orchestrator:events was absent and stream:orchestrator:events remained a stream. The current RedisPlatformEventPublisher uses its configured name directly. The matrix keeps the retired stream as a separate unresolved custody/disposal item; this adapter does not assign another service’s historical state to Scanner or delete it.