package database import ( "database/sql" "fmt" "net/url" "strings" "time" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/glebarez/sqlite" mysqlDriver "github.com/go-sql-driver/mysql" "gorm.io/driver/mysql" "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/gorm/logger" ) const ( mysqlMaxIdleConnections = 2 // Default as of Go 1.20 mysqlMaxOpenConnections = 10 ) type ( connector struct { db *gorm.DB encryptionSecret string } ) // ErrCoreMetaNotFound is the error thrown when reading a non-existent // core_kv key var ErrCoreMetaNotFound = errors.New("core meta entry not found") // New creates a new Connector with the given driver and database func New(driverName, connString, encryptionSecret string) (c Connector, err error) { var ( dbTuner func(*sql.DB, error) error innerDB gorm.Dialector ) switch driverName { case "mysql": if err = mysqlDriver.SetLogger(NewLogrusLogWriterWithLevel(logrus.StandardLogger(), logrus.ErrorLevel, driverName)); err != nil { return nil, fmt.Errorf("setting logger on mysql driver: %w", err) } innerDB = mysql.Open(connString) dbTuner = tuneMySQLDatabase case "postgres": innerDB = postgres.Open(connString) case "sqlite": var err error if connString, err = patchSQLiteConnString(connString); err != nil { return nil, errors.Wrap(err, "patching connection string") } innerDB = sqlite.Open(connString) dbTuner = tuneSQLiteDatabase default: return nil, errors.Errorf("unknown database driver %s", driverName) } db, err := gorm.Open(innerDB, &gorm.Config{ DisableForeignKeyConstraintWhenMigrating: true, Logger: logger.New(NewLogrusLogWriterWithLevel(logrus.StandardLogger(), logrus.TraceLevel, driverName), logger.Config{ SlowThreshold: time.Second, Colorful: false, IgnoreRecordNotFoundError: false, ParameterizedQueries: false, LogLevel: logger.Info, }), }) if err != nil { return nil, errors.Wrap(err, "connecting database") } if dbTuner != nil { if err = dbTuner(db.DB()); err != nil { return nil, errors.Wrap(err, "tuning database") } } conn := &connector{ db: db, encryptionSecret: encryptionSecret, } return conn, errors.Wrap(conn.applyCoreSchema(), "applying core schema") } func (connector) Close() error { return nil } func (connector) CopyDatabase(src, target *gorm.DB) error { return CopyObjects(src, target, &coreKV{}) } func (c connector) DB() *gorm.DB { return c.db } func (c connector) applyCoreSchema() error { return errors.Wrap(c.db.AutoMigrate(&coreKV{}), "applying coreKV schema") } func patchSQLiteConnString(connString string) (string, error) { u, err := url.Parse(connString) if err != nil { return connString, errors.Wrap(err, "parsing connString") } q := u.Query() q.Add("_pragma", "locking_mode(EXCLUSIVE)") q.Add("_pragma", "synchronous(FULL)") u.RawQuery = strings.NewReplacer( "%28", "(", "%29", ")", ).Replace(q.Encode()) return u.String(), nil } func tuneMySQLDatabase(db *sql.DB, err error) error { if err != nil { return errors.Wrap(err, "getting database") } // By default the package allows unlimited connections and the // default value of a MySQL / MariaDB server is to allow 151 // connections at most. Therefore we tune the connection pool to // sane values in order not to flood the database with connections // in case a lot of events occur at the same time. db.SetConnMaxIdleTime(time.Hour) db.SetConnMaxLifetime(time.Hour) db.SetMaxIdleConns(mysqlMaxIdleConnections) db.SetMaxOpenConns(mysqlMaxOpenConnections) return nil } func tuneSQLiteDatabase(db *sql.DB, err error) error { if err != nil { return errors.Wrap(err, "getting database") } db.SetConnMaxIdleTime(0) db.SetConnMaxLifetime(0) db.SetMaxIdleConns(1) db.SetMaxOpenConns(1) return nil }