Skip to content

Commit 523b277

Browse files
committed
sqlite3: simplify caching the results of sqlite3_errstr
The number of valid error codes is fixed. This commit takes advantage of that to use a static array for caching instead of a sync.Map.
1 parent f9812ff commit 523b277

2 files changed

Lines changed: 69 additions & 47 deletions

File tree

error.go

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ package sqlite3
1414
*/
1515
import "C"
1616
import (
17-
"sync"
1817
"sync/atomic"
1918
"syscall"
2019
)
@@ -153,38 +152,45 @@ const (
153152
ErrWarningAutoIndex = ErrNoExtended(ErrWarning | 1<<8)
154153
)
155154

156-
var errStrCache struct {
157-
sync.Map // int => string
158-
size atomic.Uint64 // size of cache
159-
intern sync.Map // interned error messages: string => string
160-
}
155+
// Cached results of sqlite3_errstr for common errors.
156+
// NB: this needs to be large enough to hold the last
157+
// common error (currently SQLITE_WARNING).
158+
var errStrCache [ErrWarning + 1]atomic.Pointer[string]
161159

162-
// errorString returns the result of sqlite3_errstr for result code rv,
160+
// errorString returns the result of sqlite3_errstr for result code rc,
163161
// which may be cached.
164-
func errorString(rv int) string {
165-
if v, ok := errStrCache.Load(rv); ok {
166-
return v.(string)
162+
func errorString(rc int) string {
163+
// Common error cases.
164+
switch rc {
165+
case C.SQLITE_ABORT_ROLLBACK:
166+
return "abort due to ROLLBACK"
167+
case C.SQLITE_ROW:
168+
return "another row available"
169+
case C.SQLITE_DONE:
170+
return "no more rows available"
171+
}
172+
if uint(rc) < uint(len(errStrCache)) {
173+
if p := errStrCache[rc].Load(); p != nil {
174+
return *p
175+
}
167176
}
168-
s := C.GoString(C.sqlite3_errstr(C.int(rv)))
177+
s := sqlite3ErrStr(rc)
169178
if s == "unknown error" {
170179
// Prevent the cache from growing unbounded by ignoring invalid
171180
// error codes.
172181
return "unknown error"
173182
}
174-
if v, loaded := errStrCache.intern.LoadOrStore(s, s); loaded {
175-
s = v.(string)
176-
}
177-
// NB: This is limit is not precise and we may exceed
178-
// it if this function is called concurrently.
179-
//
180-
// TODO: make the limit less than a power of 2 to reduce
181-
// the chance that we resize the map if it is exceeded.
182-
if errStrCache.size.Load() < 1024 {
183-
if v, loaded := errStrCache.LoadOrStore(rv, s); loaded {
184-
s = v.(string)
185-
} else {
186-
errStrCache.size.Add(1)
187-
}
183+
if uint(rc) < uint(len(errStrCache)) {
184+
// NB: There is a race here that could result in this store
185+
// overwriting a valid value, but it is unlikely and fixing
186+
// it is not worth the cost of the CAS operation, which can
187+
// be expensive on some platforms.
188+
errStrCache[rc].Store(&s)
188189
}
189190
return s
190191
}
192+
193+
// This function is declared separately so that we can call it from test code.
194+
func sqlite3ErrStr(rc int) string {
195+
return C.GoString(C.sqlite3_errstr(C.int(rc)))
196+
}

error_test.go

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"io/ioutil"
1414
"os"
1515
"path"
16+
"sort"
1617
"testing"
1718
)
1819

@@ -80,7 +81,6 @@ func TestSqlLogicErrors(t *testing.T) {
8081
if err.Error() != expectedErr {
8182
t.Errorf("Unexpected error: %s, expected %s", err.Error(), expectedErr)
8283
}
83-
8484
}
8585

8686
func TestExtendedErrorCodes_ForeignKey(t *testing.T) {
@@ -281,26 +281,42 @@ func TestError_SystemErrno(t *testing.T) {
281281
}
282282
}
283283

284-
func TestErrorStringCacheSize(t *testing.T) {
285-
for i := 0; i < 50_000; i++ {
286-
_ = errorString(i)
287-
}
288-
n := 0
289-
o := 0
290-
errStrCache.Range(func(_, v any) bool {
291-
n++
292-
return true
293-
})
294-
errStrCache.intern.Range(func(_, v any) bool {
295-
o++
296-
return true
297-
})
298-
if n > 1024 {
299-
t.Fatalf("len(errStrCache) should be capped at %d got: %d", 1024, n)
300-
}
301-
if o > 128 {
302-
// If sqlite3 adds a lot of new error messages this value
303-
// will need to be increased.
304-
t.Errorf("expected errStrMsgCache to be below %d got: %d", 128, o)
284+
func errorStringTestCodes() []int {
285+
// Special cases
286+
codes := []int{
287+
100, // SQLITE_ROW
288+
101, // SQLITE_DONE
289+
int(ErrAbortRollback), // SQLITE_ABORT_ROLLBACK
290+
}
291+
for code := 0; code < int(ErrWarning)+10; code++ {
292+
codes = append(codes, code)
293+
}
294+
sort.Ints(codes)
295+
return codes
296+
}
297+
298+
func TestErrorString(t *testing.T) {
299+
for _, code := range errorStringTestCodes() {
300+
got := errorString(code)
301+
want := sqlite3ErrStr(code)
302+
if got != want {
303+
t.Errorf("errorString(%d) = %q; want: %q", code, got, want)
304+
}
305+
}
306+
}
307+
308+
// Test that errorString caches valid results.
309+
func TestErrorStringCaching(t *testing.T) {
310+
for _, code := range errorStringTestCodes() {
311+
s1 := errorString(code)
312+
s2 := errorString(code)
313+
if p1, p2 := stringData(s1), stringData(s2); p1 != p2 {
314+
kind := "valid"
315+
if code > int(ErrWarning) {
316+
kind = "invalid"
317+
}
318+
t.Errorf("Failed to cache %s sqlite3 error code %d (%q): got: %p want: %p",
319+
kind, code, s1, p1, p2)
320+
}
305321
}
306322
}

0 commit comments

Comments
 (0)