summaryrefslogtreecommitdiff
path: root/internal/db/bundb
diff options
context:
space:
mode:
Diffstat (limited to 'internal/db/bundb')
-rw-r--r--internal/db/bundb/admin_test.go1
-rw-r--r--internal/db/bundb/bundb.go25
-rw-r--r--internal/db/bundb/drivers.go346
-rw-r--r--internal/db/bundb/errors.go105
-rw-r--r--internal/db/bundb/tag_test.go13
5 files changed, 25 insertions, 465 deletions
diff --git a/internal/db/bundb/admin_test.go b/internal/db/bundb/admin_test.go
index da3370c4e..8018ef3fa 100644
--- a/internal/db/bundb/admin_test.go
+++ b/internal/db/bundb/admin_test.go
@@ -74,6 +74,7 @@ func (suite *AdminTestSuite) TestCreateInstanceAccount() {
// we need to take an empty db for this...
testrig.StandardDBTeardown(suite.db)
// ...with tables created but no data
+ suite.db = testrig.NewTestDB(&suite.state)
testrig.CreateTestTables(suite.db)
// make sure there's no instance account in the db yet
diff --git a/internal/db/bundb/bundb.go b/internal/db/bundb/bundb.go
index b0ce575e6..e7256c276 100644
--- a/internal/db/bundb/bundb.go
+++ b/internal/db/bundb/bundb.go
@@ -48,8 +48,6 @@ import (
"github.com/uptrace/bun/dialect/pgdialect"
"github.com/uptrace/bun/dialect/sqlitedialect"
"github.com/uptrace/bun/migrate"
-
- "modernc.org/sqlite"
)
// DBService satisfies the DB interface
@@ -133,12 +131,12 @@ func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) {
switch t {
case "postgres":
- db, err = pgConn(ctx, state)
+ db, err = pgConn(ctx)
if err != nil {
return nil, err
}
case "sqlite":
- db, err = sqliteConn(ctx, state)
+ db, err = sqliteConn(ctx)
if err != nil {
return nil, err
}
@@ -295,7 +293,7 @@ func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) {
return ps, nil
}
-func pgConn(ctx context.Context, state *state.State) (*bun.DB, error) {
+func pgConn(ctx context.Context) (*bun.DB, error) {
opts, err := deriveBunDBPGOptions() //nolint:contextcheck
if err != nil {
return nil, fmt.Errorf("could not create bundb postgres options: %w", err)
@@ -326,7 +324,7 @@ func pgConn(ctx context.Context, state *state.State) (*bun.DB, error) {
return db, nil
}
-func sqliteConn(ctx context.Context, state *state.State) (*bun.DB, error) {
+func sqliteConn(ctx context.Context) (*bun.DB, error) {
// validate db address has actually been set
address := config.GetDbAddress()
if address == "" {
@@ -339,9 +337,6 @@ func sqliteConn(ctx context.Context, state *state.State) (*bun.DB, error) {
// Open new DB instance
sqldb, err := sql.Open("sqlite-gts", address)
if err != nil {
- if errWithCode, ok := err.(*sqlite.Error); ok {
- err = errors.New(sqlite.ErrorCodeString[errWithCode.Code()])
- }
return nil, fmt.Errorf("could not open sqlite db with address %s: %w", address, err)
}
@@ -356,11 +351,9 @@ func sqliteConn(ctx context.Context, state *state.State) (*bun.DB, error) {
// ping to check the db is there and listening
if err := db.PingContext(ctx); err != nil {
- if errWithCode, ok := err.(*sqlite.Error); ok {
- err = errors.New(sqlite.ErrorCodeString[errWithCode.Code()])
- }
return nil, fmt.Errorf("sqlite ping: %w", err)
}
+
log.Infof(ctx, "connected to SQLITE database with address %s", address)
return db, nil
@@ -528,12 +521,8 @@ func buildSQLiteAddress(addr string) string {
// Use random name for in-memory instead of ':memory:', so
// multiple in-mem databases can be created without conflict.
- addr = uuid.NewString()
-
- // in-mem-specific preferences
- // (shared cache so that tests don't fail)
- prefs.Add("mode", "memory")
- prefs.Add("cache", "shared")
+ addr = "/" + uuid.NewString()
+ prefs.Add("vfs", "memdb")
}
if dur := config.GetDbSqliteBusyTimeout(); dur > 0 {
diff --git a/internal/db/bundb/drivers.go b/internal/db/bundb/drivers.go
index 1811ad533..f39189c9d 100644
--- a/internal/db/bundb/drivers.go
+++ b/internal/db/bundb/drivers.go
@@ -18,350 +18,14 @@
package bundb
import (
- "context"
"database/sql"
- "database/sql/driver"
- "time"
- _ "unsafe" // linkname shenanigans
- pgx "github.com/jackc/pgx/v5/stdlib"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "modernc.org/sqlite"
+ "github.com/superseriousbusiness/gotosocial/internal/db/postgres"
+ "github.com/superseriousbusiness/gotosocial/internal/db/sqlite"
)
-var (
- // global SQL driver instances.
- postgresDriver = pgx.GetDefaultDriver()
- sqliteDriver = getSQLiteDriver()
-
- // check the postgres connection
- // conforms to our conn{} interface.
- // (note SQLite doesn't export their
- // conn type, and gets checked in
- // tests very regularly anywho).
- _ conn = (*pgx.Conn)(nil)
-)
-
-//go:linkname getSQLiteDriver modernc.org/sqlite.newDriver
-func getSQLiteDriver() *sqlite.Driver
-
func init() {
- sql.Register("pgx-gts", &PostgreSQLDriver{})
- sql.Register("sqlite-gts", &SQLiteDriver{})
-}
-
-// PostgreSQLDriver is our own wrapper around the
-// pgx/stdlib.Driver{} type in order to wrap further
-// SQL driver types with our own err processing.
-type PostgreSQLDriver struct{}
-
-func (d *PostgreSQLDriver) Open(name string) (driver.Conn, error) {
- c, err := postgresDriver.Open(name)
- if err != nil {
- return nil, err
- }
- return &PostgreSQLConn{conn: c.(conn)}, nil
-}
-
-type PostgreSQLConn struct{ conn }
-
-func (c *PostgreSQLConn) Begin() (driver.Tx, error) {
- return c.BeginTx(context.Background(), driver.TxOptions{})
-}
-
-func (c *PostgreSQLConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
- tx, err := c.conn.BeginTx(ctx, opts)
- err = processPostgresError(err)
- if err != nil {
- return nil, err
- }
- return &PostgreSQLTx{tx}, nil
-}
-
-func (c *PostgreSQLConn) Prepare(query string) (driver.Stmt, error) {
- return c.PrepareContext(context.Background(), query)
-}
-
-func (c *PostgreSQLConn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) {
- st, err := c.conn.PrepareContext(ctx, query)
- err = processPostgresError(err)
- if err != nil {
- return nil, err
- }
- return &PostgreSQLStmt{stmt: st.(stmt)}, nil
-}
-
-func (c *PostgreSQLConn) Exec(query string, args []driver.Value) (driver.Result, error) {
- return c.ExecContext(context.Background(), query, toNamedValues(args))
-}
-
-func (c *PostgreSQLConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
- result, err := c.conn.ExecContext(ctx, query, args)
- err = processPostgresError(err)
- return result, err
-}
-
-func (c *PostgreSQLConn) Query(query string, args []driver.Value) (driver.Rows, error) {
- return c.QueryContext(context.Background(), query, toNamedValues(args))
-}
-
-func (c *PostgreSQLConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
- rows, err := c.conn.QueryContext(ctx, query, args)
- err = processPostgresError(err)
- return rows, err
-}
-
-func (c *PostgreSQLConn) Close() error {
- return c.conn.Close()
-}
-
-type PostgreSQLTx struct{ driver.Tx }
-
-func (tx *PostgreSQLTx) Commit() error {
- err := tx.Tx.Commit()
- return processPostgresError(err)
-}
-
-func (tx *PostgreSQLTx) Rollback() error {
- err := tx.Tx.Rollback()
- return processPostgresError(err)
-}
-
-type PostgreSQLStmt struct{ stmt }
-
-func (stmt *PostgreSQLStmt) Exec(args []driver.Value) (driver.Result, error) {
- return stmt.ExecContext(context.Background(), toNamedValues(args))
-}
-
-func (stmt *PostgreSQLStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) {
- res, err := stmt.stmt.ExecContext(ctx, args)
- err = processPostgresError(err)
- return res, err
-}
-
-func (stmt *PostgreSQLStmt) Query(args []driver.Value) (driver.Rows, error) {
- return stmt.QueryContext(context.Background(), toNamedValues(args))
-}
-
-func (stmt *PostgreSQLStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
- rows, err := stmt.stmt.QueryContext(ctx, args)
- err = processPostgresError(err)
- return rows, err
-}
-
-// SQLiteDriver is our own wrapper around the
-// sqlite.Driver{} type in order to wrap further
-// SQL driver types with our own functionality,
-// e.g. hooks, retries and err processing.
-type SQLiteDriver struct{}
-
-func (d *SQLiteDriver) Open(name string) (driver.Conn, error) {
- c, err := sqliteDriver.Open(name)
- if err != nil {
- return nil, err
- }
- return &SQLiteConn{conn: c.(conn)}, nil
-}
-
-type SQLiteConn struct{ conn }
-
-func (c *SQLiteConn) Begin() (driver.Tx, error) {
- return c.BeginTx(context.Background(), driver.TxOptions{})
-}
-
-func (c *SQLiteConn) BeginTx(ctx context.Context, opts driver.TxOptions) (tx driver.Tx, err error) {
- err = retryOnBusy(ctx, func() error {
- tx, err = c.conn.BeginTx(ctx, opts)
- err = processSQLiteError(err)
- return err
- })
- if err != nil {
- return nil, err
- }
- return &SQLiteTx{Context: ctx, Tx: tx}, nil
-}
-
-func (c *SQLiteConn) Prepare(query string) (driver.Stmt, error) {
- return c.PrepareContext(context.Background(), query)
-}
-
-func (c *SQLiteConn) PrepareContext(ctx context.Context, query string) (st driver.Stmt, err error) {
- err = retryOnBusy(ctx, func() error {
- st, err = c.conn.PrepareContext(ctx, query)
- err = processSQLiteError(err)
- return err
- })
- if err != nil {
- return nil, err
- }
- return &SQLiteStmt{st.(stmt)}, nil
-}
-
-func (c *SQLiteConn) Exec(query string, args []driver.Value) (driver.Result, error) {
- return c.ExecContext(context.Background(), query, toNamedValues(args))
-}
-
-func (c *SQLiteConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (result driver.Result, err error) {
- err = retryOnBusy(ctx, func() error {
- result, err = c.conn.ExecContext(ctx, query, args)
- err = processSQLiteError(err)
- return err
- })
- return
-}
-
-func (c *SQLiteConn) Query(query string, args []driver.Value) (driver.Rows, error) {
- return c.QueryContext(context.Background(), query, toNamedValues(args))
-}
-
-func (c *SQLiteConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (rows driver.Rows, err error) {
- err = retryOnBusy(ctx, func() error {
- rows, err = c.conn.QueryContext(ctx, query, args)
- err = processSQLiteError(err)
- return err
- })
- return
-}
-
-func (c *SQLiteConn) Close() error {
- // see: https://www.sqlite.org/pragma.html#pragma_optimize
- const onClose = "PRAGMA analysis_limit=1000; PRAGMA optimize;"
- _, _ = c.conn.ExecContext(context.Background(), onClose, nil)
- return c.conn.Close()
-}
-
-type SQLiteTx struct {
- context.Context
- driver.Tx
-}
-
-func (tx *SQLiteTx) Commit() (err error) {
- err = retryOnBusy(tx.Context, func() error {
- err = tx.Tx.Commit()
- err = processSQLiteError(err)
- return err
- })
- return
-}
-
-func (tx *SQLiteTx) Rollback() (err error) {
- err = retryOnBusy(tx.Context, func() error {
- err = tx.Tx.Rollback()
- err = processSQLiteError(err)
- return err
- })
- return
-}
-
-type SQLiteStmt struct{ stmt }
-
-func (stmt *SQLiteStmt) Exec(args []driver.Value) (driver.Result, error) {
- return stmt.ExecContext(context.Background(), toNamedValues(args))
-}
-
-func (stmt *SQLiteStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (res driver.Result, err error) {
- err = retryOnBusy(ctx, func() error {
- res, err = stmt.stmt.ExecContext(ctx, args)
- err = processSQLiteError(err)
- return err
- })
- return
-}
-
-func (stmt *SQLiteStmt) Query(args []driver.Value) (driver.Rows, error) {
- return stmt.QueryContext(context.Background(), toNamedValues(args))
-}
-
-func (stmt *SQLiteStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (rows driver.Rows, err error) {
- err = retryOnBusy(ctx, func() error {
- rows, err = stmt.stmt.QueryContext(ctx, args)
- err = processSQLiteError(err)
- return err
- })
- return
-}
-
-type conn interface {
- driver.Conn
- driver.ConnPrepareContext
- driver.ExecerContext
- driver.QueryerContext
- driver.ConnBeginTx
-}
-
-type stmt interface {
- driver.Stmt
- driver.StmtExecContext
- driver.StmtQueryContext
-}
-
-// retryOnBusy will retry given function on returned 'errBusy'.
-func retryOnBusy(ctx context.Context, fn func() error) error {
- if err := fn(); err != errBusy {
- return err
- }
- return retryOnBusySlow(ctx, fn)
-}
-
-// retryOnBusySlow is the outlined form of retryOnBusy, to allow the fast path (i.e. only
-// 1 attempt) to be inlined, leaving the slow retry loop to be a separate function call.
-func retryOnBusySlow(ctx context.Context, fn func() error) error {
- var backoff time.Duration
-
- for i := 0; ; i++ {
- // backoff according to a multiplier of 2ms * 2^2n,
- // up to a maximum possible backoff time of 5 minutes.
- //
- // this works out as the following:
- // 4ms
- // 16ms
- // 64ms
- // 256ms
- // 1.024s
- // 4.096s
- // 16.384s
- // 1m5.536s
- // 4m22.144s
- backoff = 2 * time.Millisecond * (1 << (2*i + 1))
- if backoff >= 5*time.Minute {
- break
- }
-
- select {
- // Context cancelled.
- case <-ctx.Done():
- return ctx.Err()
-
- // Backoff for some time.
- case <-time.After(backoff):
- }
-
- // Perform func.
- err := fn()
-
- if err != errBusy {
- // May be nil, or may be
- // some other error, either
- // way return here.
- return err
- }
- }
-
- return gtserror.Newf("%w (waited > %s)", db.ErrBusyTimeout, backoff)
-}
-
-// toNamedValues converts older driver.Value types to driver.NamedValue types.
-func toNamedValues(args []driver.Value) []driver.NamedValue {
- if args == nil {
- return nil
- }
- args2 := make([]driver.NamedValue, len(args))
- for i := range args {
- args2[i] = driver.NamedValue{
- Ordinal: i + 1,
- Value: args[i],
- }
- }
- return args2
+ // register our SQL driver implementations.
+ sql.Register("pgx-gts", &postgres.Driver{})
+ sql.Register("sqlite-gts", &sqlite.Driver{})
}
diff --git a/internal/db/bundb/errors.go b/internal/db/bundb/errors.go
deleted file mode 100644
index f2633786a..000000000
--- a/internal/db/bundb/errors.go
+++ /dev/null
@@ -1,105 +0,0 @@
-// GoToSocial
-// Copyright (C) GoToSocial Authors admin@gotosocial.org
-// SPDX-License-Identifier: AGPL-3.0-or-later
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero 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 Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-package bundb
-
-import (
- "database/sql/driver"
- "errors"
-
- "github.com/jackc/pgx/v5/pgconn"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "modernc.org/sqlite"
- sqlite3 "modernc.org/sqlite/lib"
-)
-
-// errBusy is a sentinel error indicating
-// busy database (e.g. retry needed).
-var errBusy = errors.New("busy")
-
-// processPostgresError processes an error, replacing any postgres specific errors with our own error type
-func processPostgresError(err error) error {
- // Catch nil errs.
- if err == nil {
- return nil
- }
-
- // Attempt to cast as postgres
- pgErr, ok := err.(*pgconn.PgError)
- if !ok {
- return err
- }
-
- // Handle supplied error code:
- // (https://www.postgresql.org/docs/10/errcodes-appendix.html)
- switch pgErr.Code { //nolint
- case "23505" /* unique_violation */ :
- return db.ErrAlreadyExists
- }
-
- return err
-}
-
-// processSQLiteError processes an error, replacing any sqlite specific errors with our own error type
-func processSQLiteError(err error) error {
- // Catch nil errs.
- if err == nil {
- return nil
- }
-
- // Attempt to cast as sqlite
- sqliteErr, ok := err.(*sqlite.Error)
- if !ok {
- return err
- }
-
- // Handle supplied error code:
- switch sqliteErr.Code() {
- case sqlite3.SQLITE_CONSTRAINT_UNIQUE,
- sqlite3.SQLITE_CONSTRAINT_PRIMARYKEY:
- return db.ErrAlreadyExists
- case sqlite3.SQLITE_BUSY,
- sqlite3.SQLITE_BUSY_SNAPSHOT,
- sqlite3.SQLITE_BUSY_RECOVERY:
- return errBusy
- case sqlite3.SQLITE_BUSY_TIMEOUT:
- return db.ErrBusyTimeout
-
- // WORKAROUND:
- // text copied from matrix dev chat:
- //
- // okay i've found a workaround for now. so between
- // v1.29.0 and v1.29.2 (modernc.org/sqlite) is that
- // slightly tweaked interruptOnDone() behaviour, which
- // causes interrupt to (imo, correctly) get called when
- // a context is cancelled to cancel the running query. the
- // issue is that every single query after that point seems
- // to still then return interrupted. so as you thought,
- // maybe that query count isn't being decremented. i don't
- // think it's our code, but i haven't ruled it out yet.
- //
- // the workaround for now is adding to our sqlite error
- // processor to replace an SQLITE_INTERRUPTED code with
- // driver.ErrBadConn, which hints to the golang sql package
- // that the conn needs to be closed and a new one opened
- //
- case sqlite3.SQLITE_INTERRUPT:
- return driver.ErrBadConn
- }
-
- return err
-}
diff --git a/internal/db/bundb/tag_test.go b/internal/db/bundb/tag_test.go
index 324398d27..3647c92de 100644
--- a/internal/db/bundb/tag_test.go
+++ b/internal/db/bundb/tag_test.go
@@ -19,6 +19,7 @@ package bundb_test
import (
"context"
+ "errors"
"testing"
"github.com/stretchr/testify/suite"
@@ -82,10 +83,20 @@ func (suite *TagTestSuite) TestPutTag() {
// Subsequent inserts should fail
// since all these tags are equivalent.
- suite.ErrorIs(err, db.ErrAlreadyExists)
+ if !suite.ErrorIs(err, db.ErrAlreadyExists) {
+ suite.T().Logf("%T(%v) %v", err, err, unwrap(err))
+ }
}
}
func TestTagTestSuite(t *testing.T) {
suite.Run(t, new(TagTestSuite))
}
+
+func unwrap(err error) (errs []error) {
+ for err != nil {
+ errs = append(errs, err)
+ err = errors.Unwrap(err)
+ }
+ return
+}