diff options
Diffstat (limited to 'internal/db/bundb')
| -rw-r--r-- | internal/db/bundb/admin_test.go | 1 | ||||
| -rw-r--r-- | internal/db/bundb/bundb.go | 25 | ||||
| -rw-r--r-- | internal/db/bundb/drivers.go | 346 | ||||
| -rw-r--r-- | internal/db/bundb/errors.go | 105 | ||||
| -rw-r--r-- | internal/db/bundb/tag_test.go | 13 | 
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 +}  | 
