From 3d5b47aa8f6481fe63971e265a5a872e36a03585 Mon Sep 17 00:00:00 2001 From: maxlandon Date: Sat, 18 Jul 2026 19:10:02 +0200 Subject: [PATCH 1/3] commands: fix import config/CA completion (cross-dir search + coloring) The `import` completers (client configs and server user-CA) were supposed to look through other teamserver apps' directories and highlight config files, but several things were broken: - isConfigDir had an inverted condition: with noSelf=true (both import commands) it required the dir to equal the current app AND then rejected it, so the cross-application search always returned nothing. - ConfigsCompleter skipped every file that didn't parse as a teamclient config, so CA .pem files never showed up under `teamserver import`. - GetConfigStyle (was unexported getConfigStyle) returned "" for non-matching files. Chained onto ActionFiles(), that REPLACED carapace's default style.ForPath styling, so ordinary files - most visibly directories - lost their color. It now falls back to style.ForPath, so configs are highlighted while directories stay blue. - The server `import` CA file completion had no styling at all; it now shares the exported GetConfigStyle. Add unit tests for isConfigDir and GetConfigStyle. Co-Authored-By: Claude Opus 4.8 (1M context) --- client/commands/commands.go | 50 ++++++++------- client/commands/commands_test.go | 103 +++++++++++++++++++++++++++++++ server/commands/commands.go | 2 +- 3 files changed, 133 insertions(+), 22 deletions(-) create mode 100644 client/commands/commands_test.go diff --git a/client/commands/commands.go b/client/commands/commands.go index f04cc15..44b807b 100644 --- a/client/commands/commands.go +++ b/client/commands/commands.go @@ -156,7 +156,7 @@ have none yet.`, iComps.PositionalCompletion( carapace.Batch( carapace.ActionCallback(ConfigsCompleter(cli, "teamclient/configs", ".teamclient.cfg", "other teamserver apps", true)), - carapace.ActionFiles().Tag("server configuration").StyleF(getConfigStyle(".teamclient.cfg")), + carapace.ActionFiles().Tag("server configuration").StyleF(GetConfigStyle(".teamclient.cfg")), ).ToA(), ) @@ -206,7 +206,7 @@ func ConfigsAppCompleter(cli *client.Client, tag string) carapace.Action { results = append(results, fmt.Sprintf("[%s] %s:%d", cfg.User, cfg.Host, cfg.Port)) } - configsAction := carapace.ActionValuesDescribed(results...).StyleF(getConfigStyle(command.ClientConfigExt)) + configsAction := carapace.ActionValuesDescribed(results...).StyleF(GetConfigStyle(command.ClientConfigExt)) return carapace.Batch(append( compErrors, @@ -255,18 +255,20 @@ func ConfigsCompleter(cli *client.Client, filePath, ext, tag string, noSelf bool filePath := filepath.Join(configPath, file.Name()) - cfg, err := cli.ReadConfig(filePath) - if err != nil || cfg == nil { - continue + // Teamclient configs parse into something we can describe as + // [user] host:port. Other importable files matched by the + // extension (eg. CA .pem files) don't parse as a config but + // should still be listed, just without that description. + if cfg, err := cli.ReadConfig(filePath); err == nil && cfg != nil { + results = append(results, filePath, fmt.Sprintf("[%s] %s:%d", cfg.User, cfg.Host, cfg.Port)) + } else { + results = append(results, filePath, "") } - - results = append(results, filePath) - results = append(results, fmt.Sprintf("[%s] %s:%d", cfg.User, cfg.Host, cfg.Port)) } } } - configsAction := carapace.ActionValuesDescribed(results...).StyleF(getConfigStyle(ext)) + configsAction := carapace.ActionValuesDescribed(results...).StyleF(GetConfigStyle(ext)) if len(compErrors) > 0 { return carapace.Batch(append(compErrors, configsAction)...).ToA() @@ -277,31 +279,37 @@ func ConfigsCompleter(cli *client.Client, filePath, ext, tag string, noSelf bool } func isConfigDir(cli *client.Client, dir fs.DirEntry, noSelf bool) bool { - if !strings.HasPrefix(dir.Name(), ".") { - return false - } - - if !dir.IsDir() { + // We look for hidden per-application directories (~/./...). The caller + // then probes a specific subpath inside, which filters out any hidden dir + // that is not actually a teamserver application directory. + if !dir.IsDir() || !strings.HasPrefix(dir.Name(), ".") { return false } - if strings.TrimPrefix(dir.Name(), ".") != cli.Name() { - return false - } - - if noSelf { + // When noSelf is set, exclude the current application's own directory so we + // only surface configs belonging to OTHER teamserver applications. + if noSelf && strings.TrimPrefix(dir.Name(), ".") == cli.Name() { return false } return true } -func getConfigStyle(ext string) func(s string, sc style.Context) string { +// GetConfigStyle returns a carapace style function that highlights files with +// the given extension (typically a teamserver/teamclient config or CA file), so +// completion visually distinguishes them from ordinary files. It is exported so +// that server-side command completers can share the same styling. +// +// For files that do not match the extension it falls back to carapace's default +// path styling (style.ForPath, LS_COLORS): chaining a StyleF onto ActionFiles() +// replaces that default, so without this fallback ordinary files (directories, +// executables, ...) would lose all of their coloring. +func GetConfigStyle(ext string) func(s string, sc style.Context) string { return func(s string, sc style.Context) string { if strings.HasSuffix(s, ext) { return style.Red } - return s + return style.ForPath(s, sc) } } diff --git a/client/commands/commands_test.go b/client/commands/commands_test.go new file mode 100644 index 0000000..bd7e690 --- /dev/null +++ b/client/commands/commands_test.go @@ -0,0 +1,103 @@ +package commands + +/* + team - Embedded teamserver for Go programs and CLI applications + Copyright (C) 2023 Reeflective + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +import ( + "io" + "io/fs" + "log/slog" + "testing" + + "github.com/carapace-sh/carapace" + "github.com/carapace-sh/carapace/pkg/style" + + "github.com/reeflective/team/client" +) + +// fakeDirEntry is a minimal fs.DirEntry for exercising isConfigDir without +// touching the filesystem. +type fakeDirEntry struct { + name string + isDir bool +} + +func (f fakeDirEntry) Name() string { return f.name } +func (f fakeDirEntry) IsDir() bool { return f.isDir } +func (f fakeDirEntry) Type() fs.FileMode { + if f.isDir { + return fs.ModeDir + } + return 0 +} +func (f fakeDirEntry) Info() (fs.FileInfo, error) { return nil, nil } + +// TestIsConfigDir guards the config-directory filter used by the import +// completers. The regression it protects against: the condition was inverted so +// that, with noSelf=true, no directory ever matched and cross-application config +// completion silently returned nothing. +func TestIsConfigDir(t *testing.T) { + cli, err := client.New("myapp", + client.WithInMemory(), + client.WithLogger(slog.NewTextHandler(io.Discard, nil)), + ) + if err != nil { + t.Fatalf("client.New: %v", err) + } + + cases := []struct { + name string + entry fakeDirEntry + noSelf bool + want bool + }{ + {"other app is included (noSelf)", fakeDirEntry{".otherapp", true}, true, true}, + {"own app is excluded (noSelf)", fakeDirEntry{".myapp", true}, true, false}, + {"own app is included (self allowed)", fakeDirEntry{".myapp", true}, false, true}, + {"non-hidden dir is ignored", fakeDirEntry{"otherapp", true}, true, false}, + {"hidden non-dir is ignored", fakeDirEntry{".otherapp", false}, true, false}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := isConfigDir(cli, tc.entry, tc.noSelf); got != tc.want { + t.Fatalf("isConfigDir(%q, noSelf=%v) = %v, want %v", tc.entry.name, tc.noSelf, got, tc.want) + } + }) + } +} + +// TestGetConfigStyle checks that config/CA files are highlighted while every +// other path keeps carapace's default per-type styling (so directories stay +// blue, etc.) rather than being stripped of color. +func TestGetConfigStyle(t *testing.T) { + styleFn := GetConfigStyle(".teamclient.cfg") + ctx := carapace.Context{} + + if got := styleFn("alice.teamclient.cfg", ctx); got != style.Red { + t.Fatalf("config file style = %q, want %q", got, style.Red) + } + + // Non-config paths must defer to carapace's default path styling, not "" and + // not the value itself. + for _, path := range []string{"notes.txt", "somedir/", "/usr/bin/env"} { + if got, want := styleFn(path, ctx), style.ForPath(path, ctx); got != want { + t.Fatalf("non-config %q style = %q, want ForPath %q", path, got, want) + } + } +} diff --git a/server/commands/commands.go b/server/commands/commands.go index 4ecb298..6217092 100644 --- a/server/commands/commands.go +++ b/server/commands/commands.go @@ -312,7 +312,7 @@ users to this one. The file is JSON of the form {"certificate":"...","private_ke iComps.PositionalCompletion( carapace.Batch( carapace.ActionCallback(cli.ConfigsCompleter(client, "teamserver/certs", ".teamserver.pem", "other teamservers user CAs", true)), - carapace.ActionFiles().Tag("teamserver user CAs"), + carapace.ActionFiles().Tag("teamserver user CAs").StyleF(cli.GetConfigStyle(".teamserver.pem")), ).ToA(), ) From 28b7e68581c3edc884590cbe66bc161188ba597a Mon Sep 17 00:00:00 2001 From: maxlandon Date: Sat, 18 Jul 2026 19:29:12 +0200 Subject: [PATCH 2/3] db: use the wazero interpreter under -race to stop flaky wasm traps CI intermittently failed on the unix (-race) jobs with "wasm error: out of bounds memory access" deep inside ncruces SQLite query execution (never on the windows job, which runs without -race). The optimizing wazero compiler emits native code the Go race detector cannot instrument, which occasionally traps under -race. Split the runtime configuration by build tag: normal builds keep the optimizing compiler with the persistent on-disk compilation cache (fast startup, ~19ms warm); the `race` build selects wazero's pure-Go interpreter, which is race-clean. Startup caching is irrelevant to test binaries, so nothing is lost there. Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/db/sqlite_runtime.go | 58 ++++++++------------------- internal/db/sqlite_runtime_cache.go | 61 +++++++++++++++++++++++++++++ internal/db/sqlite_runtime_race.go | 37 +++++++++++++++++ 3 files changed, 115 insertions(+), 41 deletions(-) create mode 100644 internal/db/sqlite_runtime_cache.go create mode 100644 internal/db/sqlite_runtime_race.go diff --git a/internal/db/sqlite_runtime.go b/internal/db/sqlite_runtime.go index fbdcad7..ad9b29b 100644 --- a/internal/db/sqlite_runtime.go +++ b/internal/db/sqlite_runtime.go @@ -22,64 +22,40 @@ package db import ( "math/bits" - "os" - "path/filepath" "sync" "github.com/ncruces/go-sqlite3" - "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/api" ) // sqliteRuntimeOnce guards the one-time global wazero runtime configuration. var sqliteRuntimeOnce sync.Once -// configureSQLiteRuntime makes the pure-Go (wazero) SQLite engine reuse a -// persistent, on-disk cache of the compiled WASM module. +// configureSQLiteRuntime installs a global wazero runtime configuration for the +// pure-Go SQLite engine, once, before the first connection is opened. // -// Without it, wazero recompiles the ~1.5MB SQLite module on every process -// start (~2s on a typical machine), which makes short-lived invocations — -// shell completion in particular — painfully slow. With the cache, only the -// first run per wazero version pays that cost; subsequent runs load the -// precompiled module in tens of milliseconds. -// -// It is best-effort and never fatal: if the cache directory is unavailable, we -// leave ncruces' default runtime configuration in place (correct, just not -// cached). We also defer to any RuntimeConfig an embedding application may have -// set itself. +// The actual configuration (an optimizing compiler with a persistent on-disk +// module cache in normal builds, or the interpreter under the race detector) is +// provided by buildRuntimeConfig in a build-tagged file. It is best-effort: when +// that returns nil we leave ncruces' own default in place. func configureSQLiteRuntime() { sqliteRuntimeOnce.Do(func() { if sqlite3.RuntimeConfig != nil { return } - cacheRoot, err := os.UserCacheDir() - if err != nil { - return - } - - cacheDir := filepath.Join(cacheRoot, "reeflective-team", "sqlite-wasm") - if err := os.MkdirAll(cacheDir, 0o700); err != nil { - return - } - - cache, err := wazero.NewCompilationCacheWithDir(cacheDir) - if err != nil { - return + if cfg := buildRuntimeConfig(); cfg != nil { + sqlite3.RuntimeConfig = cfg.WithCoreFeatures(api.CoreFeaturesV2) } + }) +} - // Match ncruces' default memory limit, which is otherwise skipped when a - // custom RuntimeConfig is supplied: 256MB on 64-bit, 32MB on 32-bit. - pages := uint32(4096) - if bits.UintSize < 64 { - pages = 512 - } +// memoryLimitPages mirrors ncruces' default WASM memory limit, which is skipped +// when we supply our own RuntimeConfig: 256MB on 64-bit, 32MB on 32-bit. +func memoryLimitPages() uint32 { + if bits.UintSize < 64 { + return 512 + } - // NewRuntimeConfig() selects the optimizing compiler where supported and - // the interpreter otherwise; the cache is simply ignored by the latter. - sqlite3.RuntimeConfig = wazero.NewRuntimeConfig(). - WithCompilationCache(cache). - WithMemoryLimitPages(pages). - WithCoreFeatures(api.CoreFeaturesV2) - }) + return 4096 } diff --git a/internal/db/sqlite_runtime_cache.go b/internal/db/sqlite_runtime_cache.go new file mode 100644 index 0000000..4fd7772 --- /dev/null +++ b/internal/db/sqlite_runtime_cache.go @@ -0,0 +1,61 @@ +//go:build !cgo_sqlite && !race + +package db + +/* + team - Embedded teamserver for Go programs and CLI applications + Copyright (C) 2023 Reeflective + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +import ( + "os" + "path/filepath" + + "github.com/tetratelabs/wazero" +) + +// buildRuntimeConfig returns an optimizing-compiler runtime backed by a +// persistent, on-disk cache of the compiled SQLite WASM module. +// +// Without it, wazero recompiles the ~1.5MB module on every process start +// (~2s), which makes short-lived invocations — shell completion in particular — +// painfully slow. With the cache, only the first run per wazero version pays +// that cost; later runs load the precompiled module in tens of milliseconds. +// +// It returns nil (leaving ncruces' default in place) when the cache directory +// is unavailable, so this is never fatal. +func buildRuntimeConfig() wazero.RuntimeConfig { + cacheRoot, err := os.UserCacheDir() + if err != nil { + return nil + } + + cacheDir := filepath.Join(cacheRoot, "reeflective-team", "sqlite-wasm") + if err := os.MkdirAll(cacheDir, 0o700); err != nil { + return nil + } + + cache, err := wazero.NewCompilationCacheWithDir(cacheDir) + if err != nil { + return nil + } + + // NewRuntimeConfig() selects the optimizing compiler where supported and the + // interpreter otherwise; the cache is simply ignored by the latter. + return wazero.NewRuntimeConfig(). + WithCompilationCache(cache). + WithMemoryLimitPages(memoryLimitPages()) +} diff --git a/internal/db/sqlite_runtime_race.go b/internal/db/sqlite_runtime_race.go new file mode 100644 index 0000000..0b85798 --- /dev/null +++ b/internal/db/sqlite_runtime_race.go @@ -0,0 +1,37 @@ +//go:build !cgo_sqlite && race + +package db + +/* + team - Embedded teamserver for Go programs and CLI applications + Copyright (C) 2023 Reeflective + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +import ( + "github.com/tetratelabs/wazero" +) + +// buildRuntimeConfig uses wazero's pure-Go interpreter under the race detector. +// +// The optimizing compiler generates native machine code that the Go race +// detector cannot instrument; under `-race` this occasionally traps with a +// spurious "wasm error: out of bounds memory access" during query execution. +// The interpreter is race-clean. Startup caching is irrelevant here (this build +// is only produced by `go test -race`), so no compilation cache is configured. +func buildRuntimeConfig() wazero.RuntimeConfig { + return wazero.NewRuntimeConfigInterpreter(). + WithMemoryLimitPages(memoryLimitPages()) +} From c1756e709f165b9caf4cb049160ad7dff5ac1dd8 Mon Sep 17 00:00:00 2001 From: maxlandon Date: Sat, 18 Jul 2026 19:44:29 +0200 Subject: [PATCH 3/3] db: serialize cold SQLite cache compilation across processes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The persistent wazero compilation cache populated itself with a temp-file-then-rename. On Windows, that rename fails ("Access is denied") when another process holds the destination, and ncruces turns it into a fatal connection error — so concurrent cold starts (parallel `go test` packages, or a user launching several teamserver processes at once) broke. It surfaced as flaky Windows CI. Compile the module once while holding a cross-process advisory lock (O_CREATE|O_EXCL lock file, with stale-lock stealing), so only one process writes the cache; the rest wait and then read it warm. Reads never collide, so warm startup stays ~20ms and query speed is unchanged. The race build keeps the interpreter (no cache, no lock) from the previous commit. Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/db/sqlite_runtime.go | 24 +++------ internal/db/sqlite_runtime_cache.go | 82 +++++++++++++++++++++++++---- internal/db/sqlite_runtime_race.go | 22 +++++--- 3 files changed, 91 insertions(+), 37 deletions(-) diff --git a/internal/db/sqlite_runtime.go b/internal/db/sqlite_runtime.go index ad9b29b..7e7a9dc 100644 --- a/internal/db/sqlite_runtime.go +++ b/internal/db/sqlite_runtime.go @@ -23,31 +23,19 @@ package db import ( "math/bits" "sync" - - "github.com/ncruces/go-sqlite3" - "github.com/tetratelabs/wazero/api" ) // sqliteRuntimeOnce guards the one-time global wazero runtime configuration. var sqliteRuntimeOnce sync.Once // configureSQLiteRuntime installs a global wazero runtime configuration for the -// pure-Go SQLite engine, once, before the first connection is opened. -// -// The actual configuration (an optimizing compiler with a persistent on-disk -// module cache in normal builds, or the interpreter under the race detector) is -// provided by buildRuntimeConfig in a build-tagged file. It is best-effort: when -// that returns nil we leave ncruces' own default in place. +// pure-Go SQLite engine, once, before the first connection is opened. The actual +// setup (optimizing compiler + persistent module cache in normal builds, or the +// interpreter under the race detector) is provided by setupSQLiteRuntime in a +// build-tagged file. It is best-effort: on any failure ncruces' own default is +// left in place. func configureSQLiteRuntime() { - sqliteRuntimeOnce.Do(func() { - if sqlite3.RuntimeConfig != nil { - return - } - - if cfg := buildRuntimeConfig(); cfg != nil { - sqlite3.RuntimeConfig = cfg.WithCoreFeatures(api.CoreFeaturesV2) - } - }) + sqliteRuntimeOnce.Do(setupSQLiteRuntime) } // memoryLimitPages mirrors ncruces' default WASM memory limit, which is skipped diff --git a/internal/db/sqlite_runtime_cache.go b/internal/db/sqlite_runtime_cache.go index 4fd7772..acc654c 100644 --- a/internal/db/sqlite_runtime_cache.go +++ b/internal/db/sqlite_runtime_cache.go @@ -23,39 +23,99 @@ package db import ( "os" "path/filepath" + "time" + "github.com/ncruces/go-sqlite3" "github.com/tetratelabs/wazero" ) -// buildRuntimeConfig returns an optimizing-compiler runtime backed by a -// persistent, on-disk cache of the compiled SQLite WASM module. +const ( + // compileLockTimeout bounds how long we wait for another process to finish + // the first (cold) compilation before proceeding anyway. Compilation itself + // takes ~2s, so this is generously above it. + compileLockTimeout = 20 * time.Second + + // compileLockStale is the age past which a lock file is assumed to have been + // abandoned by a crashed process and may be stolen. + compileLockStale = 30 * time.Second +) + +// setupSQLiteRuntime configures the pure-Go SQLite engine to reuse a persistent, +// on-disk cache of the compiled WASM module. // -// Without it, wazero recompiles the ~1.5MB module on every process start -// (~2s), which makes short-lived invocations — shell completion in particular — +// Without it, wazero recompiles the ~1.5MB module on every process start (~2s), +// which makes short-lived invocations — shell completion in particular — // painfully slow. With the cache, only the first run per wazero version pays // that cost; later runs load the precompiled module in tens of milliseconds. // -// It returns nil (leaving ncruces' default in place) when the cache directory -// is unavailable, so this is never fatal. -func buildRuntimeConfig() wazero.RuntimeConfig { +// It is best-effort: if the cache directory is unavailable we leave ncruces' +// default in place, and we also defer to any RuntimeConfig an embedding +// application set itself. +func setupSQLiteRuntime() { + if sqlite3.RuntimeConfig != nil { + return + } + cacheRoot, err := os.UserCacheDir() if err != nil { - return nil + return } cacheDir := filepath.Join(cacheRoot, "reeflective-team", "sqlite-wasm") if err := os.MkdirAll(cacheDir, 0o700); err != nil { - return nil + return } cache, err := wazero.NewCompilationCacheWithDir(cacheDir) if err != nil { - return nil + return } // NewRuntimeConfig() selects the optimizing compiler where supported and the // interpreter otherwise; the cache is simply ignored by the latter. - return wazero.NewRuntimeConfig(). + sqlite3.RuntimeConfig = wazero.NewRuntimeConfig(). WithCompilationCache(cache). WithMemoryLimitPages(memoryLimitPages()) + + warmCompilationCache(cacheDir) +} + +// warmCompilationCache triggers the one-time module compilation while holding a +// cross-process advisory lock, so that concurrent cold starts don't race to +// write the shared cache. wazero populates the cache with a temp-file-then- +// rename, and on Windows that rename fails ("Access is denied") when another +// process holds the destination — which ncruces then turns into a fatal +// connection error. Serializing the cold compile avoids the collision; once the +// cache is warm, reads never conflict. +func warmCompilationCache(cacheDir string) { + lockPath := filepath.Join(cacheDir, ".compile.lock") + + var lock *os.File + + deadline := time.Now().Add(compileLockTimeout) + for time.Now().Before(deadline) { + if f, err := os.OpenFile(lockPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600); err == nil { + lock = f + break + } + + // Steal a lock left behind by a crashed process. + if info, statErr := os.Stat(lockPath); statErr == nil && time.Since(info.ModTime()) > compileLockStale { + os.Remove(lockPath) + continue + } + + time.Sleep(75 * time.Millisecond) + } + + if lock != nil { + defer func() { + lock.Close() + os.Remove(lockPath) + }() + } + + // As the lock holder we are the sole writer; if we timed out waiting, the + // holder has by now populated the cache and this only reads it. + _ = sqlite3.Initialize() } diff --git a/internal/db/sqlite_runtime_race.go b/internal/db/sqlite_runtime_race.go index 0b85798..834bc32 100644 --- a/internal/db/sqlite_runtime_race.go +++ b/internal/db/sqlite_runtime_race.go @@ -21,17 +21,23 @@ package db */ import ( + "github.com/ncruces/go-sqlite3" "github.com/tetratelabs/wazero" ) -// buildRuntimeConfig uses wazero's pure-Go interpreter under the race detector. +// setupSQLiteRuntime uses wazero's pure-Go interpreter under the race detector. // -// The optimizing compiler generates native machine code that the Go race -// detector cannot instrument; under `-race` this occasionally traps with a -// spurious "wasm error: out of bounds memory access" during query execution. -// The interpreter is race-clean. Startup caching is irrelevant here (this build -// is only produced by `go test -race`), so no compilation cache is configured. -func buildRuntimeConfig() wazero.RuntimeConfig { - return wazero.NewRuntimeConfigInterpreter(). +// The optimizing compiler generates native machine code the Go race detector +// cannot instrument; under `-race` this occasionally traps with a spurious +// "wasm error: out of bounds memory access" during query execution. The +// interpreter is race-clean. Startup caching (and thus the cross-process cache +// lock) is irrelevant here, since this build is only produced by `go test +// -race`. +func setupSQLiteRuntime() { + if sqlite3.RuntimeConfig != nil { + return + } + + sqlite3.RuntimeConfig = wazero.NewRuntimeConfigInterpreter(). WithMemoryLimitPages(memoryLimitPages()) }