Repository navigation
Expand file tree
/
Copy pathpagecache_trampolines.go
More file actions
424 lines (396 loc) · 14.5 KB
/
Copy pathpagecache_trampolines.go
File metadata and controls
424 lines (396 loc) · 14.5 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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
// Copyright 2026 The Sqlite Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package sqlite
import (
"fmt"
"math/bits"
"sync"
"unsafe"
"modernc.org/libc"
"modernc.org/libc/sys/types"
sqlite3 "modernc.org/sqlite/lib"
)
// The pcache binding owns the sqlite3_pcache_page stub that SQLite sees
// and forwards user-facing calls to the registered PageCache and
// per-database Cache instances. The user-facing surface in
// pagecache.go is idiomatic Go; this file holds the package-internal
// trampolines that bridge to the cgo-free C ABI.
//
// Ownership model:
//
// - Each xCreate on the C side mints a fresh pcacheBinding and a
// uintptr handle from pcacheBindings.gen. SQLite stores the handle
// as its opaque sqlite3_pcache*; trampolines recover the binding
// via pcacheBindings.m under RLock.
// - Each xFetch on the C side maps a (binding, key) pair to a
// sqlite3_pcache_page stub allocated via libc.Xcalloc. The stub
// lives until Unpin(discard=true), a Truncate sweep covering the
// key, or Destroy on the owning cache. SQLite re-fetches for the
// same key return the same stub; this is the binding-side fix for
// the "stale pPgHdr->pPage" issue called out in MR #126's
// §2 design note.
// - The Page object the user returns from Cache.Fetch is held in
// the binding's byStub map so a Page-identity comparison on the
// next Fetch for the same key can short-circuit stub reallocation
// when the impl retains the entry.
//
// Threading invariant: all Cache callbacks for a single pcacheBinding
// are serialised by the SQLite engine, but not always on one goroutine.
// Without shared cache, a binding belongs to one connection, which is
// opened SQLITE_OPEN_FULLMUTEX and which database/sql never uses from
// two goroutines at once. With cache=shared in a "file:" DSN, every
// connection sharing the cache reaches the same binding, each from its
// own goroutine, and SQLite serialises them through the BtShared mutex.
// The mutex on pcacheBinding gives the race detector the happens-before
// edge that SQLite's own mutex provides invisibly, and guards the maps
// against the cross-cache traffic of the package-global pcacheBindings
// registry.
type pcacheBinding struct {
mu sync.Mutex
cache Cache
byKey map[uint32]uintptr // key -> sqlite3_pcache_page stub uintptr
byStub map[uintptr]pcacheEntry // stub uintptr -> {Page, key}
}
type pcacheEntry struct {
page Page
key uint32
// pinned is set by Fetch and cleared by Unpin; Truncate and Destroy
// drop the entry outright. While it is set SQLite holds the stub,
// so the binding must not free it: see pcacheContractViolation.
pinned bool
}
// pcacheContractViolation panics. It is called when Cache.Fetch returns nil,
// or a different Page, for a key whose stub SQLite still holds pinned. The
// Page contract forbids that: Buf and Extra must stay valid from the Fetch
// until the matching Unpin. Freeing the stub, which is what the binding does
// for an unpinned key, would leave SQLite reading and writing freed memory
// and corrupt the database without any error. Crashing is the better
// failure.
func pcacheContractViolation(key uint32, got string) {
panic(fmt.Sprintf("sqlite: page cache contract violation: Cache.Fetch returned %s for page %d while SQLite holds it pinned", got, key))
}
// pcacheBindings is the package-global registry mapping the opaque
// sqlite3_pcache* handles SQLite sees to the *pcacheBinding they
// represent. Handles are minted from a bitset-backed allocator so they
// can be reclaimed on Destroy and stay small across the process
// lifetime (otherwise a long-running process that opens and closes many
// caches would grow the map without bound).
var pcacheBindings = struct {
mu sync.RWMutex
m map[uintptr]*pcacheBinding
gen pcacheIDGen
}{
m: map[uintptr]*pcacheBinding{},
}
// lookupPcacheBinding returns the binding for the given C handle, or
// nil if the handle is unknown. It is called from every trampoline
// other than Init/Shutdown.
func lookupPcacheBinding(h uintptr) *pcacheBinding {
pcacheBindings.mu.RLock()
defer pcacheBindings.mu.RUnlock()
return pcacheBindings.m[h]
}
// registerPcacheBinding installs b into the registry and returns the
// opaque handle SQLite will see.
func registerPcacheBinding(b *pcacheBinding) uintptr {
pcacheBindings.mu.Lock()
defer pcacheBindings.mu.Unlock()
h := pcacheBindings.gen.next()
pcacheBindings.m[h] = b
return h
}
// unregisterPcacheBinding removes h from the registry and reclaims its
// ID slot. Called from pcacheTrampolineDestroy after the user's
// Destroy returns.
func unregisterPcacheBinding(h uintptr) {
pcacheBindings.mu.Lock()
defer pcacheBindings.mu.Unlock()
delete(pcacheBindings.m, h)
pcacheBindings.gen.reclaim(h)
}
// pcacheIDGen is a bitset-backed allocator over uintptr handles
// starting at 1 (0 is reserved as a sentinel for "not registered").
// Identical in shape to the vtab idGen at sqlite.go:808-824; reproduced
// here to keep the pcache subsystem self-contained.
type pcacheIDGen struct {
bitset []uint64
}
func (g *pcacheIDGen) next() uintptr {
base := uintptr(1)
for i := 0; i < len(g.bitset); i, base = i+1, base+64 {
b := g.bitset[i]
if b != 1<<64-1 {
n := uintptr(bits.TrailingZeros64(^b))
g.bitset[i] |= 1 << n
return base + n
}
}
g.bitset = append(g.bitset, 1)
return base
}
func (g *pcacheIDGen) reclaim(id uintptr) {
bit := id - 1
g.bitset[bit/64] &^= 1 << (bit % 64)
}
// Sentinel returned by the binding when SQLite asks for a stub but the
// user impl returned nil or the binding could not allocate. Translating
// to uintptr(0) tells SQLite to fall back to its stress path.
const pcacheNullStub uintptr = 0
// pcacheStubSize is the size of the sqlite3_pcache_page struct that
// SQLite reads: two uintptr fields (pBuf, pExtra). Computed at startup
// to keep the trampolines free of unsafe.Sizeof calls on the hot path.
var pcacheStubSize = types.Size_t(unsafe.Sizeof(uintptr(0)) * 2)
// pcacheTrampolineInit is wired to sqlite3_pcache_methods2.xInit. The
// binding has no per-process state to set up at sqlite3_initialize
// time; PageCache.Create is invoked per database, not per
// initialize, and the package-global pcacheBindings registry is ready
// from package init. Returns SQLITE_OK unconditionally.
func pcacheTrampolineInit(tls *libc.TLS, arg uintptr) int32 {
return sqlite3.SQLITE_OK
}
// pcacheTrampolineShutdown is wired to xShutdown. The binding does not
// call sqlite3_shutdown internally, so in normal operation this
// trampoline never fires; it exists so the C methods table never
// holds a null xShutdown slot when paired with custom user code that
// might trigger sqlite3_shutdown out of band.
func pcacheTrampolineShutdown(tls *libc.TLS, arg uintptr) {}
// pcacheTrampolineCreate is wired to xCreate. SQLite asks the binding
// to create a new per-database cache. The trampoline calls
// PageCache.Create on the registered module and registers the
// returned Cache in pcacheBindings, handing SQLite back the opaque
// handle.
//
// szPage and szExtra are the sizes SQLite wants for each page's Buf
// and Extra buffers; szExtra is NOT just the application-visible
// extraSize, it already includes SQLite's PgHdr overhead. The
// Cache implementation must honour it as opaque.
//
// bPurgeable is advisory: 0 for in-memory databases (Unpin will only
// ever be called with discard=true), non-zero otherwise.
//
// A return of 0 tells SQLite the cache could not be created (treated
// as SQLITE_NOMEM by the caller in _sqlite3PcacheSetPageSize).
func pcacheTrampolineCreate(tls *libc.TLS, szPage, szExtra, bPurgeable int32) uintptr {
module := pcacheState.registered
if module == nil {
// Defensive: should be impossible because SQLite only consults
// the methods table after _sqlite3Config.Fpcache2 is installed,
// which happens only after registered is set.
return pcacheNullStub
}
cache, err := module.Create(int(szPage), int(szExtra), bPurgeable != 0)
if err != nil || cache == nil {
return pcacheNullStub
}
b := &pcacheBinding{
cache: cache,
byKey: map[uint32]uintptr{},
byStub: map[uintptr]pcacheEntry{},
}
return registerPcacheBinding(b)
}
// pcacheTrampolineCachesize forwards SQLite's xCachesize to
// Cache.SetSize. The size is advisory.
func pcacheTrampolineCachesize(tls *libc.TLS, pCache uintptr, nCachesize int32) {
b := lookupPcacheBinding(pCache)
if b == nil {
return
}
b.cache.SetSize(int(nCachesize))
}
// pcacheTrampolinePagecount forwards xPagecount to Cache.PageCount.
func pcacheTrampolinePagecount(tls *libc.TLS, pCache uintptr) int32 {
b := lookupPcacheBinding(pCache)
if b == nil {
return 0
}
return int32(b.cache.PageCount())
}
// pcacheTrampolineFetch forwards xFetch to Cache.Fetch on every
// request. SQLite does not require that re-fetches for the same key
// return an identical sqlite3_pcache_page pointer:
// _sqlite3PcacheFetchFinish keys off whether pPage->pExtra is already
// initialised (PgHdr.pPage != 0); a fresh page (zeroed Extra) is just
// re-initialised. So the binding asks the impl on every call and only
// reuses the cached stub when the returned Page is the same value;
// when the impl evicts and replaces the entry the stale stub is
// retired and a fresh one minted.
//
// This makes a bounded, evicting purgeable cache (the normal way an
// impl honours cache_size) safe by construction: an impl that drops
// an entry on Unpin(discard=false) cannot leak a stale stub to
// SQLite because the next Fetch reaches the impl, observes the
// eviction (page == nil or a different Page), and the binding
// updates its bookkeeping accordingly.
func pcacheTrampolineFetch(tls *libc.TLS, pCache uintptr, key uint32, createFlag int32) uintptr {
b := lookupPcacheBinding(pCache)
if b == nil {
return pcacheNullStub
}
page := b.cache.Fetch(key, FetchMode(createFlag))
b.mu.Lock()
defer b.mu.Unlock()
old, had := b.byKey[key]
var held pcacheEntry
if had {
held = b.byStub[old]
}
if page == nil {
// Lookup miss, eviction, or OOM: retire any stale stub the
// binding still held for this key. The next Fetch with
// createFlag > 0 will re-allocate.
if had {
if held.pinned {
pcacheContractViolation(key, "nil")
}
delete(b.byKey, key)
delete(b.byStub, old)
libc.Xfree(tls, old)
}
return pcacheNullStub
}
if had && held.page == page {
// Same Page value retained across the unpin/refetch cycle;
// reuse the existing stub. Buf and Extra addresses are stable
// while the page is pinned, which is exactly the window during
// which SQLite holds the stub pointer.
held.pinned = true
b.byStub[old] = held
return old
}
if had {
// Page was replaced for this key (impl evicted and re-allocated
// fresh memory). Retire the stale stub before minting a new one;
// SQLite has already finished using the old stub by the time
// xUnpin returned -- unless it has not, which is a contract
// violation by the Cache.
if held.pinned {
pcacheContractViolation(key, "a different Page")
}
delete(b.byStub, old)
libc.Xfree(tls, old)
}
stub := libc.Xcalloc(tls, 1, pcacheStubSize)
if stub == 0 {
delete(b.byKey, key)
return pcacheNullStub
}
f := (*[2]uintptr)(unsafe.Pointer(stub))
f[0], f[1] = uintptr(page.Buf()), uintptr(page.Extra())
b.byKey[key] = stub
b.byStub[stub] = pcacheEntry{page: page, key: key, pinned: true}
return stub
}
// pcacheTrampolineUnpin forwards xUnpin to Cache.Unpin. When discard
// is non-zero the binding evicts: it frees the stub after the user's
// Unpin returns. When discard is zero the binding only forwards to
// the user; the cache MAY release or retain the page and its memory
// at its discretion. The next Fetch for the key consults the impl,
// so a release between unpin and the next fetch is observed and
// reflected in the binding's bookkeeping.
func pcacheTrampolineUnpin(tls *libc.TLS, pCache, pPage uintptr, discard int32) {
b := lookupPcacheBinding(pCache)
if b == nil {
return
}
b.mu.Lock()
entry, ok := b.byStub[pPage]
b.mu.Unlock()
if !ok {
return
}
b.cache.Unpin(entry.page, discard != 0)
if discard == 0 {
b.mu.Lock()
if e, ok := b.byStub[pPage]; ok {
e.pinned = false
b.byStub[pPage] = e
}
b.mu.Unlock()
return
}
b.mu.Lock()
delete(b.byKey, entry.key)
delete(b.byStub, pPage)
b.mu.Unlock()
libc.Xfree(tls, pPage)
}
// pcacheTrampolineRekey forwards xRekey to Cache.Rekey, then updates
// the binding's byKey map so a subsequent Fetch for newKey returns
// the same stub. If newKey already has a stub the spec guarantees
// the colliding entry is unpinned; the binding retires it.
func pcacheTrampolineRekey(tls *libc.TLS, pCache, pPage uintptr, oldKey, newKey uint32) {
b := lookupPcacheBinding(pCache)
if b == nil {
return
}
b.mu.Lock()
entry, ok := b.byStub[pPage]
b.mu.Unlock()
if !ok {
return
}
b.cache.Rekey(entry.page, oldKey, newKey)
b.mu.Lock()
defer b.mu.Unlock()
if collider, ok := b.byKey[newKey]; ok && collider != pPage {
delete(b.byKey, newKey)
delete(b.byStub, collider)
libc.Xfree(tls, collider)
}
delete(b.byKey, oldKey)
b.byKey[newKey] = pPage
entry.key = newKey
b.byStub[pPage] = entry
}
// pcacheTrampolineTruncate forwards xTruncate to Cache.Truncate
// and then sweeps every binding-owned stub whose key is greater than
// or equal to limit. SQLite documents Truncate as the only callback
// permitted to evict a pinned page, so the binding does the eviction
// unconditionally.
func pcacheTrampolineTruncate(tls *libc.TLS, pCache uintptr, iLimit uint32) {
b := lookupPcacheBinding(pCache)
if b == nil {
return
}
b.cache.Truncate(iLimit)
b.mu.Lock()
for key, stub := range b.byKey {
if key >= iLimit {
delete(b.byKey, key)
delete(b.byStub, stub)
libc.Xfree(tls, stub)
}
}
b.mu.Unlock()
}
// pcacheTrampolineDestroy forwards xDestroy to Cache.Destroy,
// frees every remaining stub, and removes the binding from the
// registry. After this trampoline returns the SQLite handle is no
// longer valid and the binding instance is unreachable.
func pcacheTrampolineDestroy(tls *libc.TLS, pCache uintptr) {
b := lookupPcacheBinding(pCache)
if b == nil {
return
}
b.cache.Destroy()
b.mu.Lock()
for _, stub := range b.byKey {
libc.Xfree(tls, stub)
}
b.byKey = nil
b.byStub = nil
b.mu.Unlock()
unregisterPcacheBinding(pCache)
}
// pcacheTrampolineShrink forwards xShrink to Cache.Shrink. SQLite
// calls this as a memory-pressure hint; the user impl is free to
// ignore it.
func pcacheTrampolineShrink(tls *libc.TLS, pCache uintptr) {
b := lookupPcacheBinding(pCache)
if b == nil {
return
}
b.cache.Shrink()
}