-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbuffer_read.go
More file actions
180 lines (164 loc) · 5.63 KB
/
Copy pathbuffer_read.go
File metadata and controls
180 lines (164 loc) · 5.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
// Read paths on *scopeBuffer:
//
// - tailOffset — window at the newest end, oldest-first within (drives /tail)
// - sinceSeq — window after a seq cursor, oldest-first (drives /head)
// - getByID — single-item lookup by id (drives /get?id=)
// - getBySeq — single-item lookup by seq (drives /get?seq=)
//
// All four take b.mu.RLock so multiple readers run concurrently. None
// check b.detached: reading from a detached buffer returns the state
// the buffer had at detach time, which is fine for reads — no
// orphan-write hazard, only an eventually-stale snapshot. The
// read-bookkeeping (recordRead) runs separately and is lock-free.
package scopecache
import "sort"
// tailOffset returns the window of newest `limit` items after
// skipping `offset` from the newest end, appended into `out`. out
// MAY be nil or empty (a pool-borrowed scratch slice is fine); the
// method always returns out[:0] sliced to the actual result length.
// The window is preserved in its native seq-ascending (oldest-first)
// order; clients sort by seq if they want newest-first.
//
// hasMore is true when older items exist before the window (i.e.
// start > 0), signalling to the caller that the response is clipped
// at the oldest end. It does NOT signal truncation at the newest end
// (that is what offset already describes to the client).
//
// Scratch-slice contract: out may be a pool-borrowed slice with
// cap ≥ limit to avoid the per-call alloc that dominates GC pressure
// on the /head + /tail hot path. Caller is responsible for releasing
// out back to its pool after consuming the result.
func (b *scopeBuffer) tailOffset(out []Item, limit, offset int) ([]Item, bool) {
b.mu.RLock()
defer b.mu.RUnlock()
if limit <= 0 || offset < 0 {
return out[:0], false
}
if offset >= len(b.items) {
return out[:0], false
}
end := len(b.items) - offset
start := end - limit
hasMore := start > 0
if start < 0 {
start = 0
}
if start >= end {
return out[:0], false
}
// items is []*Item; deref-copy the window into out so the read path
// never hands back the live pointers.
window := b.items[start:end]
out = out[:0]
for _, p := range window {
out = append(out, *p)
}
return out, hasMore
}
// sinceSeq returns items with seq > afterSeq, oldest-first, up to
// limit, appended into `out`. Same scratch-slice contract as
// tailOffset. limit ≤ 0 returns out[:0] — matches every other
// multi-item read on the public surface. HTTP rejects 0/negative
// with 400 via normalizeLimit; the guard here exists for direct
// Gateway callers.
//
// The bool is true when more matching items exist beyond the
// returned slice, so the handler surfaces truncated=true without
// the client guessing from count == limit.
func (b *scopeBuffer) sinceSeq(out []Item, afterSeq uint64, limit int) ([]Item, bool) {
if limit <= 0 {
return out[:0], false
}
b.mu.RLock()
defer b.mu.RUnlock()
if len(b.items) == 0 {
return out[:0], false
}
idx := sort.Search(len(b.items), func(i int) bool {
return b.items[i].Seq > afterSeq
})
if idx >= len(b.items) {
return out[:0], false
}
available := len(b.items) - idx
take := available
hasMore := false
if available > limit {
take = limit
hasMore = true
}
out = out[:0]
for j := 0; j < take; j++ {
out = append(out, *b.items[idx+j])
}
return out, hasMore
}
// getByID inlines the RUnlock instead of deferring it: on a map lookup
// this small the defer overhead is a measurable fraction of the call.
// Pattern: RLock → lookup → unlock → branch on ok. Hot read paths only —
// keep `defer` on slower paths where it's noise.
func (b *scopeBuffer) getByID(id string) (Item, bool) {
b.mu.RLock()
item, ok := b.byID[id]
b.mu.RUnlock()
if !ok {
return Item{}, false
}
return *item, true
}
// getBySeq does a binary search on b.items (monotonic ascending by Seq)
// and deref-copies under the lock — value Item out, no *Item leak. For
// realistic scope sizes (≤ 14 comparisons for 10k items) this matches
// a hashmap's amortised constant cost in absolute ns, without the
// mapassign + map-grow tax on the corresponding write paths.
func (b *scopeBuffer) getBySeq(seq uint64) (Item, bool) {
b.mu.RLock()
i, ok := b.indexBySeqLocked(seq)
if !ok {
b.mu.RUnlock()
return Item{}, false
}
item := *b.items[i]
b.mu.RUnlock()
return item, true
}
// rawBytesByID / rawBytesBySeq return the raw /get output bytes —
// rawBytes for JSON-string payloads, else Payload — WITHOUT copying
// the surrounding 96-byte Item (which getByID/getBySeq do). The returned
// slice aliases scopeBuffer storage; payload backing arrays are immutable
// once stored (writes replace the slice header wholesale, never mutate in
// place — see buffer_write.go), so the view stays valid for the GC
// lifetime of the slice. Read-only: callers must not mutate the bytes.
func (b *scopeBuffer) rawBytesByID(id string) ([]byte, bool) {
b.mu.RLock()
item, ok := b.byID[id]
b.mu.RUnlock()
if !ok {
return nil, false
}
if item.rawBytes != nil {
return item.rawBytes, true
}
return item.Payload, true
}
func (b *scopeBuffer) rawBytesBySeq(seq uint64) ([]byte, bool) {
b.mu.RLock()
i, ok := b.indexBySeqLocked(seq)
if !ok {
b.mu.RUnlock()
return nil, false
}
item := b.items[i]
// rawBytes / Payload are slice headers that alias scopeBuffer
// storage; payload backing arrays are immutable once stored
// (wholesale-replace semantics in buffer_write.go), so the alias
// stays valid for the GC lifetime of the slice. Capture before
// unlocking to keep the *Item itself off the public surface.
raw := item.rawBytes
payload := item.Payload
b.mu.RUnlock()
if raw != nil {
return raw, true
}
return payload, true
}