Commit 4ddd785
Fix sorted-set Memory leaks and BITOP epoch tracking (#1752)
## Summary
Two correctness bugs in the sorted-set object store and the BITOP read
loop.
## 1. Sorted-set `IMemoryOwner` leaks in `GEO*STORE` / `ZUNIONSTORE` / `ZINTERSTORE`
Three pooled-buffer leaks where backends (`GeoSearch`, `SortedSetRange`,
internal `ZADD`'s `SortedSetAdd`) wrote replies via `RespMemoryWriter`,
which (with a default `SpanByte`) rents a `MemoryPool<byte>` buffer
(≥512 bytes) and assigns it to `.SpanByteAndMemory.Memory`:
- **`SortedSetGeoOps.cs` (`GEO*STORE`)** — the `searchOutMem.Memory`
from `GeoSearch` was leaked (only `searchOutHandler` — the
`MemoryHandle` from `Pin()` — was disposed). The internal `ZADD`
invocation (`zAddOutput`) was discarded entirely.
- **`SortedSetOps.cs` (`ZUNIONSTORE` / `ZINTERSTORE`)** — same pattern:
`rangeOutputMem.Memory` from `SortedSetRange` was leaked, and the
internal `ZADD`'s `zAddOutput.SpanByteAndMemory.Memory` was leaked.
Under heavy `GEO*STORE` / `ZUNIONSTORE` / `ZINTERSTORE` traffic this was
real `MemoryPool` churn and GC pressure.
### Fix
- For each `*STORE`-style internal `ZADD`, wrap the
`RMWObjectStoreOperationWithOutput` call in a `try` / `finally` that
disposes `zAddOutput.SpanByteAndMemory.Memory` if `!IsSpanByte`.
- Extend the existing `finally` blocks that dispose `*Handler` (the
`MemoryHandle`) to also dispose the underlying `*Mem.Memory`
(the `IMemoryOwner<byte>`).
## 2. BITOP: pending-completion epoch tracking was broken
`StorageSession.HeadAddress` was a `readonly long` field captured at
session-construction time and never updated.
`MainStoreOps.ReadWithUnsafeContext` compared it against itself
(`HeadAddress == localHeadAddress`) to decide whether to set
`epochChanged = true` after pending completion. Two bugs:
1. The field is frozen, so the check was meaningless — the live store
`HeadAddress` was never consulted.
2. The condition was also **inverted**: it set `epochChanged = true`
when the addresses were equal (i.e., head did NOT move), the
opposite of what the comment said.
In addition, `Read` can return synchronously with a pointer into the
**read cache** (a separate log with its own `HeadAddress` that can be
evicted independently of the main log). The original check would not
detect read-cache eviction.
### Fix
- Removed the stale `StorageSession.HeadAddress` field.
- Added `ClientSession.HeadAddress` and `ClientSession.ReadCacheHeadAddress`
accessors that read the live values from `store.Log.HeadAddress` /
`store.ReadCache?.HeadAddress`.
- `ReadWithUnsafeContext` now captures both addresses at the start of
the BITOP loop and, after pending completion, sets `epochChanged = true`
if **either** has advanced — correctly invalidating any pointers
captured into either log.
## Files changed
- `libs/storage/Tsavorite/cs/src/core/ClientSession/ClientSession.cs` —
new `HeadAddress` and `ReadCacheHeadAddress` live accessors
- `libs/server/Storage/Session/StorageSession.cs` — removed stale
`HeadAddress` field
- `libs/server/Storage/Session/MainStore/MainStoreOps.cs` —
`ReadWithUnsafeContext` uses live addresses with the corrected
comparison; signature now also takes `localReadCacheHeadAddress`
- `libs/server/Storage/Session/MainStore/BitmapOps.cs` — captures both
live head addresses; passes them to `ReadWithUnsafeContext`
- `libs/server/Storage/Session/ObjectStore/SortedSetGeoOps.cs` — leak
fixes (`searchOutMem` + `zAddOutput`)
- `libs/server/Storage/Session/ObjectStore/SortedSetOps.cs` — leak
fixes (`rangeOutputMem` + `zAddOutput`)
## Validation
- All 660 sorted-set + geo + bitmap tests pass on `main`
- `dotnet format --verify-no-changes` clean
## Note on related fixes on `dev`
This is a subset of the fixes on the companion `dev` branch
(`badrishc/memory-fixes`). Other fixes from that branch were
intentionally **not** ported here because they do not apply to `main`:
- The BITOP overflow-pointer fix relies on the `ISourceLogRecord` /
`LogRecord` / `OverflowByteArray` model that exists only on `dev`. On
`main` the BITOP backend operates on `SpanByte` values that are always
pinned in log memory, so the use-after-fixed bug fixed on `dev` does
not exist on `main`.
- The PFCOUNT/PFMERGE bounds-check tightening from `dev` is unnecessary
on `main` because the `main` backend already validates
`value.Length <= dst.Length` *before* the `Buffer.MemoryCopy`, so the
wrong-capacity argument is gated.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>1 parent d746a55 commit 4ddd785
6 files changed
Lines changed: 59 additions & 11 deletions
File tree
- libs
- server/Storage/Session
- MainStore
- ObjectStore
- storage/Tsavorite/cs/src/core/ClientSession
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
105 | 105 | | |
106 | 106 | | |
107 | 107 | | |
108 | | - | |
| 108 | + | |
| 109 | + | |
109 | 110 | | |
110 | 111 | | |
111 | 112 | | |
112 | 113 | | |
113 | 114 | | |
114 | 115 | | |
115 | 116 | | |
116 | | - | |
| 117 | + | |
117 | 118 | | |
118 | 119 | | |
119 | 120 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
50 | | - | |
| 50 | + | |
51 | 51 | | |
52 | 52 | | |
53 | 53 | | |
| |||
63 | 63 | | |
64 | 64 | | |
65 | 65 | | |
66 | | - | |
67 | | - | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
68 | 72 | | |
69 | 73 | | |
70 | 74 | | |
| |||
Lines changed: 16 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
207 | 207 | | |
208 | 208 | | |
209 | 209 | | |
210 | | - | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
211 | 213 | | |
212 | | - | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
213 | 224 | | |
214 | 225 | | |
215 | 226 | | |
216 | 227 | | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
217 | 231 | | |
218 | 232 | | |
219 | 233 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
785 | 785 | | |
786 | 786 | | |
787 | 787 | | |
788 | | - | |
789 | | - | |
| 788 | + | |
| 789 | + | |
| 790 | + | |
| 791 | + | |
| 792 | + | |
| 793 | + | |
| 794 | + | |
| 795 | + | |
| 796 | + | |
| 797 | + | |
| 798 | + | |
| 799 | + | |
| 800 | + | |
790 | 801 | | |
791 | 802 | | |
792 | 803 | | |
793 | 804 | | |
794 | 805 | | |
| 806 | + | |
| 807 | + | |
| 808 | + | |
795 | 809 | | |
796 | 810 | | |
797 | 811 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
25 | | - | |
26 | 25 | | |
27 | 26 | | |
28 | 27 | | |
| |||
107 | 106 | | |
108 | 107 | | |
109 | 108 | | |
110 | | - | |
111 | 109 | | |
112 | 110 | | |
113 | 111 | | |
| |||
Lines changed: 17 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
108 | 108 | | |
109 | 109 | | |
110 | 110 | | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
111 | 128 | | |
112 | 129 | | |
113 | 130 | | |
| |||
0 commit comments