LEGLINK-948: Recover Redis used memory before falling back to ABS - #1809
Conversation
The Redis-vs-ABS decision was logged at Debug on every path except the catch-all. Deployed environments run at Information, so an ABS result could not be distinguished from a failed memory probe without redeploying at a different log level. - Log the selection decision at Information with used_memory, the configured MaxMemoryBytes, the computed usage percentage, the threshold and the resulting cache type, for both outcomes rather than only for the ABS fallback. - Raise the three probe-failure paths from Debug to Warning and include the endpoint, the raw used_memory value and the reported memory statistics. Add the endpoint to the exception path. - Report the configured endpoints and per-server IsConnected state when no connected server is found. - Dump the full INFO memory section once per process so the raw server output can be inspected without shell access to the Redis instance. Azure Managed Redis is not expected to report maxmemory, which is the assumption behind supplying the limit via configuration; logging it confirms whether that still holds. - Replace ToDictionary with a defensive lookup. It throws on duplicate keys, and the catch-all swallowed that into a silent ABS fallback. Azure Managed Redis proxies Redis Enterprise and is not guaranteed to return the unique-key INFO shape open-source Redis does. Adds unit tests covering the Information-level decision log, the no-connected-server warning, and duplicate INFO keys no longer forcing the ABS fallback.
📝 WalkthroughWalkthrough
ChangesHybrid resource cache diagnostics
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
DotNet/ServiceTests/UnitTests/Shared/ResourceCache/HybridResourceCacheTests.cs (1)
169-215: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAdd focused tests for the remaining probe branches.
Add one XUnit test where
InfoAsync("memory")returns no section and verify ABS is selected. Add one XUnit test whereused_memoryis non-numeric and verify Redis is selected with a warning. Keep the tests mock-only.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@DotNet/ServiceTests/UnitTests/Shared/ResourceCache/HybridResourceCacheTests.cs` around lines 169 - 215, Add two mock-only XUnit tests alongside the existing HybridResourceCache tests: configure InfoAsync("memory") to return no section and verify writes use _absCache, then configure a non-numeric used_memory value and verify writes use _redisCache while VerifyWarningLogged confirms a warning. Reuse CreateSut, Write, and the existing Redis setup helpers without changing production code.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@DotNet/ServiceTests/UnitTests/Shared/ResourceCache/HybridResourceCacheTests.cs`:
- Around line 175-183: Strengthen Selection_decision_is_logged_at_Information by
asserting the log message content, not only its Information level. Update
VerifyLogged to require the selection marker “=> selected” and the chosen cache
type “Redis”, while preserving the existing at-least-once expectation.
In `@DotNet/Shared/Application/Services/ResourceCache/HybridResourceCache.cs`:
- Line 49: The _infoSectionLogged guard in HybridResourceCache must be
process-wide rather than instance-specific. Change the field declaration to
static so all HybridResourceCache instances share the same logging state,
preserving the existing once-only INFO section logging behavior.
- Around line 276-280: Sanitize every dynamic logging argument in
HybridResourceCache before passing it to logger methods: at lines 276-280
sanitize the full Redis INFO section; at lines 157-161 sanitize the configured
endpoint and server-state descriptions; at lines 171-188 sanitize the endpoint,
raw used_memory, and formatted diagnostics; at lines 197-221 sanitize the
endpoint and diagnostic strings; and at lines 227-230 sanitize the endpoint. Use
the existing project sanitization utility or established pattern consistently
across these calls.
---
Nitpick comments:
In
`@DotNet/ServiceTests/UnitTests/Shared/ResourceCache/HybridResourceCacheTests.cs`:
- Around line 169-215: Add two mock-only XUnit tests alongside the existing
HybridResourceCache tests: configure InfoAsync("memory") to return no section
and verify writes use _absCache, then configure a non-numeric used_memory value
and verify writes use _redisCache while VerifyWarningLogged confirms a warning.
Reuse CreateSut, Write, and the existing Redis setup helpers without changing
production code.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: a3d76326-e686-4edd-85e7-572204983c44
📒 Files selected for processing (2)
DotNet/ServiceTests/UnitTests/Shared/ResourceCache/HybridResourceCacheTests.csDotNet/Shared/Application/Services/ResourceCache/HybridResourceCache.cs
The Hybrid cache abandoned the memory probe on the first miss. If INFO memory returned nothing it selected ABS, and if INFO threw the outer catch selected ABS, so a restricted command decided the cache rather than memory pressure. Azure Managed Redis proxies Redis Enterprise and already withholds maxmemory, which is why LEGLINK-770 moved the limit into configuration; the same restrictions can hide used_memory or the whole INFO memory section. Only the numerator is sourced from the server. It is now recovered through a chain, and the result is still measured against the configured ResourceCache:Redis:MaxMemoryBytes: - INFO memory used_memory, as before. - MEMORY STATS total.allocated when INFO memory is missing, empty or has no parsable used_memory. - A full INFO dump scanned for used_memory when MEMORY STATS is restricted or omits total.allocated. - ABS only when every source fails, logged as a warning. A rejected INFO no longer reaches the outer catch. ReadInfoAsync swallows non-cancellation exceptions and returns null so the chain continues; the outer catch still routes genuine connectivity failures to ABS. Behaviour changes: an absent or unparsable used_memory previously selected Redis immediately and now consults the remaining sources; an empty or rejected INFO previously selected ABS immediately and now does the same. No connected server still selects ABS, and an unset MaxMemoryBytes still selects Redis. The decision log now records which command produced the figure, so it is visible whether the instance answered INFO, MEMORY STATS or neither. Adds unit tests for MEMORY STATS recovery below and above the threshold, exhaustion of every source selecting ABS, and a rejected INFO still reaching MEMORY STATS.
🛠️ Description of Changes
Fixes and instruments the Hybrid resource cache selection for LEGLINK-948, where
ResourceCache:CacheImplementation = Hybridselects ABS on every correlation in TEST.The reporter's A/B/A reproduction rules out connectivity: forcing
CacheImplementation = Redisimmediately before and after the failing run writes to Redis successfully. That narrows the problem toHybridResourceCache.SelectCacheTypeAsync, which had two defects.1. The memory probe gave up on the first miss.
SelectCacheTypeAsyncsourcedused_memoryfromINFO memoryand nothing else. If that section came back empty it selected ABS; ifINFOthrew, the outer catch selected ABS. Either way a restricted command decided the cache rather than memory pressure. Azure Managed Redis proxies Redis Enterprise and already withholdsmaxmemory— that is precisely why LEGLINK-770 moved the limit into configuration — so the same restrictions can plausibly hideused_memoryor the whole INFO memory section. On such an instance the old code produces deterministic always-ABS, which is the reported symptom.The numerator is now recovered through a chain, and is still measured against the configured
ResourceCache:Redis:MaxMemoryBytes. Only the numerator ever comes from the server:A rejected
INFOno longer reaches the outer catch —ReadInfoAsyncswallows non-cancellation exceptions and returns null so the chain continues. The outer catch still routes genuine connectivity failures to ABS.Selection behaviour, before and after:
used_memorypresentused_memoryabsent or unparsableMaxMemoryBytesunset2. The decision was invisible in every deployed environment.
Four paths returned ABS and three logged at
Debug. Deployed environments run atInformation, so an ABS result could not be distinguished from a failed probe without redeploying at a different log level — the reason this bug needed a code change before it could even be diagnosed.Informationfor both outcomes with the used-memory figure, which command produced it, the configuredMaxMemoryBytes, the computed percentage and the threshold.Warningnaming what failed, with the endpoint and the reported memory statistics.IsConnectedstate are reported.INFO memorysection is dumped once per process, so the raw server output can be inspected without shell access to the Redis instance.maxmemoryis included deliberately: AMR not reporting it is the assumption behind LEGLINK-770, and this confirms whether that still holds per instance.SelectCacheTypeAsyncis memoized per correlationId, so it runs roughly once per patient-correlation rather than per resource.Informationhere is not a hot path.3.
ToDictionaryreplaced with a defensive lookup. It throws on duplicate keys and the catch-all swallowed that into a silent ABS fallback. AMR is not guaranteed to return the unique-key INFO shape open-source Redis does.Once deployed, the decision line distinguishes the remaining hypotheses.
via INFO memorywith a figure close to what Redis Insight reports means real memory pressure against a mis-sized denominator;via MEMORY STATSorvia INFO (full)means INFO memory is restricted on AMR; a figure far above what Redis Insight reports means the metric is node-level and the percentage approach is unsound there; and the exhausted-sources warning means no memory signal is available at all.Note this lives in
DotNet/Shared, so every .NET service picks it up, not only the three services in the reproduction steps.Related follow-up, not in this PR:
ResourceCache:Redis:MaxMemoryBytesis unprovisioned in dev, qa, qa2 and test, so all environments fall back to theappsettings.jsondefault of268435456(256 MB) whileapp-config.yamldocuments LCG Redis instances as 1 GB. Deliberately left out so the first run after this deploys reads the current denominator — changing both at once would blur which factor moved the result.🧪 Testing Performed
dotnet build DotNet/Shared/Shared.csproj— clean, 0 errors.dotnet test DotNet/ServiceTests/ServiceTests.csproj --filter FullyQualifiedName~HybridResourceCacheTests— 15/15 passing (9 pre-existing, 6 new).MinimumLevel.Default = Informationwith overrides only forMicrosoftandSystem, and the TEST App Configuration store sets the same three keys.LantanaGroup.Link.Shared.*is not filtered, so these reach Loki.Not yet exercised against a live Azure Managed Redis instance. That is the next step: deploy to TEST and re-run the reporter's reproduction steps. The
INFO memorydump and thevia <command>marker on the decision line are what confirm which commands that instance actually answers.🧑🔬 Unit Testing
📓 Documentation Updated
No documentation changes needed. No configuration keys are added, removed or renamed, and the meaning of
ResourceCache:Redis:MaxMemoryBytesandMemoryThresholdPercentis unchanged — the percentage comparison they drive is the same, only the recovery of the used-memory numerator and the observability around it have changed.