Skip to content

Commit 08be1f4

Browse files
committed
Do not reuse *sql.DB across the goroutine
1 parent 08a2913 commit 08be1f4

File tree

1 file changed

+33
-31
lines changed

1 file changed

+33
-31
lines changed

lib/sqlrunner.go

Lines changed: 33 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,19 @@ import (
99
"database/sql/driver"
1010
"encoding/hex"
1111
"fmt"
12+
"log/slog"
1213
"os"
1314
"path/filepath"
14-
"sync"
1515
"time"
1616

1717
lru "github.com/hashicorp/golang-lru/v2"
18+
"golang.org/x/sync/singleflight"
1819
"modernc.org/sqlite"
1920
_ "modernc.org/sqlite"
2021
)
2122

23+
var sf singleflight.Group
24+
2225
func init() {
2326
// MySQL-compatible functions
2427
sqlite.MustRegisterFunction("YEAR", &sqlite.FunctionImpl{
@@ -117,8 +120,7 @@ type SQLRunner struct {
117120
cache *lru.Cache[string, *QueryResult]
118121

119122
// cache
120-
db *sql.DB
121-
mu sync.Mutex
123+
filename string
122124
}
123125

124126
func NewSQLRunner(schema string) (*SQLRunner, error) {
@@ -144,21 +146,6 @@ func NewSQLRunner(schema string) (*SQLRunner, error) {
144146
return runner, nil
145147
}
146148

147-
// Close closes the SQLite instance.
148-
func (r *SQLRunner) Close() error {
149-
r.mu.Lock()
150-
defer r.mu.Unlock()
151-
152-
if r.db == nil {
153-
return nil
154-
}
155-
156-
err := r.db.Close()
157-
r.db = nil
158-
159-
return err
160-
}
161-
162149
// Query executes a query and returns the result.
163150
func (r *SQLRunner) Query(ctx context.Context, query string) (*QueryResult, error) {
164151
// Check the cache first
@@ -170,13 +157,20 @@ func (r *SQLRunner) Query(ctx context.Context, query string) (*QueryResult, erro
170157
if err != nil {
171158
return nil, fmt.Errorf("get schema: %w", err)
172159
}
160+
defer func() {
161+
if err := db.Close(); err != nil {
162+
slog.WarnContext(ctx, "close schema database", slog.Any("error", err))
163+
}
164+
}()
173165

174166
result, err := db.QueryContext(ctx, query)
175167
if err != nil {
176168
return nil, NewQueryError(err)
177169
}
178170
defer func() {
179-
_ = result.Close()
171+
if err := result.Close(); err != nil {
172+
slog.WarnContext(ctx, "close result", slog.Any("error", err))
173+
}
180174
}()
181175

182176
cols, err := result.Columns()
@@ -216,28 +210,36 @@ func (r *SQLRunner) Query(ctx context.Context, query string) (*QueryResult, erro
216210

217211
// getSqliteInstance gets the initialized SQLite instance.
218212
//
219-
// You don't need to close it after using this instance.
213+
// You should close the database after using it.
220214
func (r *SQLRunner) getSqliteInstance() (*sql.DB, error) {
221-
r.mu.Lock()
222-
defer r.mu.Unlock()
215+
if r.filename != "" {
216+
filename, err := initializeThreadSafe(r.schema)
217+
if err != nil {
218+
return nil, NewSchemaError(err)
219+
}
223220

224-
if r.db != nil {
225-
return r.db, nil
221+
r.filename = filename
226222
}
227223

228-
filename, err := initialize(r.schema)
224+
db, err := sql.Open("sqlite", fmt.Sprintf("file:%s?mode=ro", r.filename))
229225
if err != nil {
230-
return nil, NewSchemaError(err)
226+
return nil, fmt.Errorf("open schema database (r/o): %w", err)
231227
}
232228

233-
db, err := sql.Open("sqlite", fmt.Sprintf("file:%s?mode=ro", filename))
229+
return db, nil
230+
}
231+
232+
// initializeThreadSafe creates a new SQLite database and sets up the schema.
233+
// It is thread safe which ensures that the schema is only initialized once.
234+
func initializeThreadSafe(schema string) (filename string, err error) {
235+
filenameAny, err, _ := sf.Do(schema, func() (interface{}, error) {
236+
return initialize(schema)
237+
})
234238
if err != nil {
235-
return nil, fmt.Errorf("open schema database (r/o): %w", err)
239+
return "", err
236240
}
237241

238-
r.db = db
239-
240-
return db, nil
242+
return filenameAny.(string), nil
241243
}
242244

243245
// initialize creates a new SQLite database and sets up the schema.

0 commit comments

Comments
 (0)