Revert read replica POC (#93551)
* Revert "chore: add replDB to team service (#91799)" This reverts commitc6ae2d7999. * Revert "experiment: use read replica for Get and Find Dashboards (#91706)" This reverts commit54177ca619. * Revert "QuotaService: refactor to use ReplDB for Get queries (#91333)" This reverts commit299c142f6a. * Revert "refactor replCfg to look more like plugins/plugin config (#91142)" This reverts commitac0b4bb34d. * Revert "chore (replstore): fix registration with multiple sql drivers, again (#90990)" This reverts commitdaedb358dd. * Revert "Chore (sqlstore): add validation and testing for repl config (#90683)" This reverts commitaf19f039b6. * Revert "ReplStore: Add support for round robin load balancing between multiple read replicas (#90530)" This reverts commit27b52b1507. * Revert "DashboardStore: Use ReplDB and get dashboard quotas from the ReadReplica (#90235)" This reverts commit8a6107cd35. * Revert "accesscontrol service read replica (#89963)" This reverts commit77a4869fca. * Revert "Fix: add mapping for the new mysqlRepl driver (#89551)" This reverts commitab5a079bcc. * Revert "fix: sql instrumentation dual registration error (#89508)" This reverts commitd988f5c3b0. * Revert "Experimental Feature Toggle: databaseReadReplica (#89232)" This reverts commit50244ed4a1.
This commit is contained in:
@@ -10,7 +10,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
"gopkg.in/ini.v1"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
@@ -66,15 +65,9 @@ func NewDatabaseConfig(cfg *setting.Cfg, features featuremgmt.FeatureToggles) (*
|
||||
return dbCfg, nil
|
||||
}
|
||||
|
||||
// readConfigSection reads the database configuration from the given block of
|
||||
// the configuration file. This method allows us to add a "database_replica"
|
||||
// section to the configuration file while using the same cfg struct.
|
||||
func (dbCfg *DatabaseConfig) readConfigSection(cfg *setting.Cfg, section string) error {
|
||||
sec := cfg.Raw.Section(section)
|
||||
return dbCfg.parseConfigIni(sec)
|
||||
}
|
||||
func (dbCfg *DatabaseConfig) readConfig(cfg *setting.Cfg) error {
|
||||
sec := cfg.Raw.Section("database")
|
||||
|
||||
func (dbCfg *DatabaseConfig) parseConfigIni(sec *ini.Section) error {
|
||||
cfgURL := sec.Key("url").String()
|
||||
if len(cfgURL) != 0 {
|
||||
dbURL, err := url.Parse(cfgURL)
|
||||
@@ -108,6 +101,7 @@ func (dbCfg *DatabaseConfig) parseConfigIni(sec *ini.Section) error {
|
||||
dbCfg.MaxOpenConn = sec.Key("max_open_conn").MustInt(0)
|
||||
dbCfg.MaxIdleConn = sec.Key("max_idle_conn").MustInt(2)
|
||||
dbCfg.ConnMaxLifetime = sec.Key("conn_max_lifetime").MustInt(14400)
|
||||
|
||||
dbCfg.SslMode = sec.Key("ssl_mode").String()
|
||||
dbCfg.SSLSNI = sec.Key("ssl_sni").String()
|
||||
dbCfg.CaCertPath = sec.Key("ca_cert_path").String()
|
||||
@@ -116,20 +110,19 @@ func (dbCfg *DatabaseConfig) parseConfigIni(sec *ini.Section) error {
|
||||
dbCfg.ServerCertName = sec.Key("server_cert_name").String()
|
||||
dbCfg.Path = sec.Key("path").MustString("data/grafana.db")
|
||||
dbCfg.IsolationLevel = sec.Key("isolation_level").String()
|
||||
|
||||
dbCfg.CacheMode = sec.Key("cache_mode").MustString("private")
|
||||
dbCfg.WALEnabled = sec.Key("wal").MustBool(false)
|
||||
dbCfg.SkipMigrations = sec.Key("skip_migrations").MustBool()
|
||||
dbCfg.MigrationLock = sec.Key("migration_locking").MustBool(true)
|
||||
dbCfg.MigrationLockAttemptTimeout = sec.Key("locking_attempt_timeout_sec").MustInt()
|
||||
|
||||
dbCfg.QueryRetries = sec.Key("query_retries").MustInt()
|
||||
dbCfg.TransactionRetries = sec.Key("transaction_retries").MustInt(5)
|
||||
dbCfg.LogQueries = sec.Key("log_queries").MustBool(false)
|
||||
return nil
|
||||
}
|
||||
|
||||
// readConfig is a wrapper around readConfigSection that read the "database" configuration block.
|
||||
func (dbCfg *DatabaseConfig) readConfig(cfg *setting.Cfg) error {
|
||||
return dbCfg.readConfigSection(cfg, "database")
|
||||
dbCfg.LogQueries = sec.Key("log_queries").MustBool(false)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dbCfg *DatabaseConfig) buildConnectionString(cfg *setting.Cfg, features featuremgmt.FeatureToggles) error {
|
||||
@@ -235,50 +228,3 @@ func buildExtraConnectionString(sep rune, urlQueryParams map[string][]string) st
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func validateReplicaConfigs(primary *DatabaseConfig, cfgs []DatabaseConfig) error {
|
||||
if cfgs == nil {
|
||||
return errors.New("cfg cannot be nil")
|
||||
}
|
||||
|
||||
// Return multiple errors so we can fix them all at once!
|
||||
var result error
|
||||
|
||||
// Check for duplicate connection strings
|
||||
seen := make(map[string]struct{})
|
||||
seen[primary.ConnectionString] = struct{}{}
|
||||
for _, cfg := range cfgs {
|
||||
if _, ok := seen[cfg.ConnectionString]; ok {
|
||||
result = errors.Join(result, errors.New("duplicate connection string"))
|
||||
} else {
|
||||
seen[cfg.ConnectionString] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that every database is the same type and version, and that it matches the primary database.
|
||||
// The database Yype may include a "withHooks" suffix, which is used to differentiate drivers for instrumentation and ignored for the purpose of this check.
|
||||
for _, cfg := range cfgs {
|
||||
if databaseDriverFromName(cfg.Type) != databaseDriverFromName(primary.Type) {
|
||||
result = errors.Join(result, fmt.Errorf("the replicas must have the same database type as the primary database (%s != %s)", primary.Type, cfg.Type))
|
||||
break // Only need to report this once
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// databaseDriverFromName strips any suffixes from the driver type that are not relevant to the database driver.
|
||||
// This is used to remove the "WithHooks" or "ReplWithHooks" suffixes which are used to differentiate drivers for instrumentation.
|
||||
func databaseDriverFromName(driverTy string) string {
|
||||
if strings.HasPrefix(driverTy, migrator.MySQL) {
|
||||
return migrator.MySQL
|
||||
}
|
||||
if strings.HasPrefix(driverTy, migrator.Postgres) {
|
||||
return migrator.Postgres
|
||||
}
|
||||
if strings.HasPrefix(driverTy, migrator.SQLite) {
|
||||
return migrator.SQLite
|
||||
}
|
||||
// default
|
||||
return driverTy
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/ini.v1"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
@@ -222,93 +221,3 @@ func TestBuildConnectionStringPostgres(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateReplicaConfigs(t *testing.T) {
|
||||
t.Run("valid config", func(t *testing.T) {
|
||||
inicfg, err := ini.Load([]byte(testReplCfg))
|
||||
require.NoError(t, err)
|
||||
cfg, err := setting.NewCfgFromINIFile(inicfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
dbCfgs, err := NewRODatabaseConfigs(cfg, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = validateReplicaConfigs(&DatabaseConfig{Type: "mysql"}, dbCfgs)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("valid but awkward config", func(t *testing.T) {
|
||||
inicfg, err := ini.Load([]byte(testReplCfg))
|
||||
require.NoError(t, err)
|
||||
cfg, err := setting.NewCfgFromINIFile(inicfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
dbCfgs, err := NewRODatabaseConfigs(cfg, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The primary is mysql, but the replicas are mysqlWithHooks. This can
|
||||
// occur when some but not all the replicas (or primary) have
|
||||
// instrument_queries enabled
|
||||
err = validateReplicaConfigs(&DatabaseConfig{Type: "mysqlWithHooks"}, dbCfgs)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("invalid config: primary database type mismatch", func(t *testing.T) {
|
||||
// valid repl config, the issue is that the primary has a different type
|
||||
inicfg, err := ini.Load([]byte(testReplCfg))
|
||||
require.NoError(t, err)
|
||||
cfg, err := setting.NewCfgFromINIFile(inicfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
dbCfgs, err := NewRODatabaseConfigs(cfg, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = validateReplicaConfigs(&DatabaseConfig{Type: "postgres"}, dbCfgs)
|
||||
require.Error(t, err)
|
||||
|
||||
if uw, ok := err.(interface{ Unwrap() []error }); ok {
|
||||
errs := uw.Unwrap()
|
||||
require.Equal(t, 1, len(errs))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid repl config", func(t *testing.T) {
|
||||
// Type mismatch + duplicate hosts
|
||||
inicfg, err := ini.Load([]byte(invalidReplCfg))
|
||||
require.NoError(t, err)
|
||||
cfg, err := setting.NewCfgFromINIFile(inicfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
dbCfgs, err := NewRODatabaseConfigs(cfg, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = validateReplicaConfigs(&DatabaseConfig{Type: "mysql"}, dbCfgs)
|
||||
require.Error(t, err)
|
||||
|
||||
if uw, ok := err.(interface{ Unwrap() []error }); ok {
|
||||
errs := uw.Unwrap()
|
||||
require.Equal(t, 2, len(errs))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// This cfg has a duplicate host for repls 0 and 1, and a type mismatch in repl 2
|
||||
var invalidReplCfg = `
|
||||
[database_replicas]
|
||||
type = mysql
|
||||
name = grafana
|
||||
user = grafana
|
||||
password = password
|
||||
host = 127.0.0.1:3306
|
||||
[database_replica.one]
|
||||
name = grafana
|
||||
user = grafana
|
||||
password = password
|
||||
type = mysql
|
||||
host = 127.0.0.1:3306
|
||||
[database_replica.two]
|
||||
name = grafana
|
||||
user = grafana
|
||||
password = password
|
||||
type = postgres
|
||||
host = 127.0.0.1:3308`
|
||||
|
||||
@@ -58,27 +58,6 @@ func WrapDatabaseDriverWithHooks(dbType string, tracer tracing.Tracer) string {
|
||||
return driverWithHooks
|
||||
}
|
||||
|
||||
// WrapDatabaseDriverWithHooks creates a fake database driver that
|
||||
// executes pre and post functions which we use to gather metrics about
|
||||
// database queries. It also registers the metrics.
|
||||
func WrapDatabaseReplDriverWithHooks(dbType string, index uint, tracer tracing.Tracer) string {
|
||||
drivers := map[string]driver.Driver{
|
||||
migrator.SQLite: &sqlite3.SQLiteDriver{},
|
||||
migrator.MySQL: &mysql.MySQLDriver{},
|
||||
migrator.Postgres: &pq.Driver{},
|
||||
}
|
||||
|
||||
d, exist := drivers[dbType]
|
||||
if !exist {
|
||||
return dbType
|
||||
}
|
||||
|
||||
driverWithHooks := dbType + fmt.Sprintf("ReplicaWithHooks%d", index)
|
||||
sql.Register(driverWithHooks, sqlhooks.Wrap(d, &databaseQueryWrapper{log: log.New("sqlstore.metrics"), tracer: tracer}))
|
||||
core.RegisterDriver(driverWithHooks, &databaseQueryWrapperDriver{dbType: dbType})
|
||||
return driverWithHooks
|
||||
}
|
||||
|
||||
// databaseQueryWrapper satisfies the sqlhook.databaseQueryWrapper interface
|
||||
// which allow us to wrap all SQL queries with a `Before` & `After` hook.
|
||||
type databaseQueryWrapper struct {
|
||||
|
||||
@@ -6,10 +6,9 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/session"
|
||||
"golang.org/x/exp/slices"
|
||||
"xorm.io/xorm"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/session"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -107,14 +106,12 @@ type LockCfg struct {
|
||||
type dialectFunc func() Dialect
|
||||
|
||||
var supportedDialects = map[string]dialectFunc{
|
||||
MySQL: NewMysqlDialect,
|
||||
SQLite: NewSQLite3Dialect,
|
||||
Postgres: NewPostgresDialect,
|
||||
MySQL + "WithHooks": NewMysqlDialect,
|
||||
MySQL + "ReplicaWithHooks": NewMysqlDialect,
|
||||
SQLite + "WithHooks": NewSQLite3Dialect,
|
||||
Postgres + "WithHooks": NewPostgresDialect,
|
||||
Postgres + "ReplicaWithHooks": NewPostgresDialect,
|
||||
MySQL: NewMysqlDialect,
|
||||
SQLite: NewSQLite3Dialect,
|
||||
Postgres: NewPostgresDialect,
|
||||
MySQL + "WithHooks": NewMysqlDialect,
|
||||
SQLite + "WithHooks": NewSQLite3Dialect,
|
||||
Postgres + "WithHooks": NewPostgresDialect,
|
||||
}
|
||||
|
||||
func NewDialect(driverName string) Dialect {
|
||||
|
||||
@@ -816,7 +816,7 @@ func setupTest(t *testing.T, numFolders, numDashboards int, permissions []access
|
||||
func setupNestedTest(t *testing.T, usr *user.SignedInUser, perms []accesscontrol.Permission, orgID int64, features featuremgmt.FeatureToggles) db.DB {
|
||||
t.Helper()
|
||||
|
||||
db, cfg := db.InitTestReplDBWithCfg(t)
|
||||
db, cfg := db.InitTestDBWithCfg(t)
|
||||
|
||||
// dashboard store commands that should be called.
|
||||
dashStore, err := database.ProvideDashboardStore(db, cfg, features, tagimpl.ProvideService(db), quotatest.New(false, nil))
|
||||
|
||||
@@ -74,7 +74,7 @@ func setupBenchMark(b *testing.B, usr user.SignedInUser, features featuremgmt.Fe
|
||||
nestingLevel = folder.MaxNestedFolderDepth
|
||||
}
|
||||
|
||||
store, cfg := db.InitTestReplDBWithCfg(b)
|
||||
store, cfg := db.InitTestDBWithCfg(b)
|
||||
|
||||
quotaService := quotatest.New(false, nil)
|
||||
|
||||
|
||||
@@ -1,277 +0,0 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/dlmiddlecote/sqlstats"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"xorm.io/xorm"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
// ReplStore is a wrapper around a main SQLStore and a read-only SQLStore. The
|
||||
// main SQLStore is anonymous, so the ReplStore may be used directly as a
|
||||
// SQLStore.
|
||||
type ReplStore struct {
|
||||
*SQLStore
|
||||
repls []*SQLStore
|
||||
|
||||
// next is the index of the next read-only SQLStore in the chain.
|
||||
next uint64
|
||||
}
|
||||
|
||||
// DB returns the main SQLStore.
|
||||
func (rs *ReplStore) DB() *SQLStore {
|
||||
return rs.SQLStore
|
||||
}
|
||||
|
||||
// ReadReplica returns the read-only SQLStore. If no read replica is configured,
|
||||
// it returns the main SQLStore.
|
||||
func (rs *ReplStore) ReadReplica() *SQLStore {
|
||||
if len(rs.repls) == 0 {
|
||||
rs.log.Debug("ReadReplica not configured, using main SQLStore")
|
||||
return rs.SQLStore
|
||||
}
|
||||
return rs.nextRepl()
|
||||
}
|
||||
|
||||
// nextRepl() returns the next read-only SQLStore in the chain. If no read replica is configured, the Primary is returned.
|
||||
func (rs *ReplStore) nextRepl() *SQLStore {
|
||||
// start by grabbing the replica at the current index
|
||||
selected := rs.repls[(int(rs.next))%len(rs.repls)]
|
||||
|
||||
// then increment the index for the next call
|
||||
atomic.AddUint64(&rs.next, 1)
|
||||
|
||||
return selected
|
||||
}
|
||||
|
||||
// ProvideServiceWithReadReplica creates a new *SQLStore connection intended for
|
||||
// use as a ReadReplica of the main SQLStore. The primary SQLStore must already
|
||||
// be initialized.
|
||||
func ProvideServiceWithReadReplica(primary *SQLStore, cfg *setting.Cfg,
|
||||
features featuremgmt.FeatureToggles, migrations registry.DatabaseMigrator,
|
||||
bus bus.Bus, tracer tracing.Tracer) (*ReplStore, error) {
|
||||
// start with the initialized SQLStore
|
||||
replStore := &ReplStore{primary, nil, 0}
|
||||
|
||||
// FeatureToggle fallback: If the FlagDatabaseReadReplica feature flag is not enabled, return a single SQLStore.
|
||||
if !features.IsEnabledGlobally(featuremgmt.FlagDatabaseReadReplica) {
|
||||
primary.log.Debug("ReadReplica feature flag not enabled, using main SQLStore")
|
||||
return replStore, nil
|
||||
}
|
||||
|
||||
// This change will make xorm use an empty default schema for postgres and
|
||||
// by that mimic the functionality of how it was functioning before
|
||||
// xorm's changes above.
|
||||
xorm.DefaultPostgresSchema = ""
|
||||
|
||||
// Parsing the configuration to get the number of repls
|
||||
replCfgs, err := NewRODatabaseConfigs(cfg, features)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := validateReplicaConfigs(primary.dbCfg, replCfgs); err != nil {
|
||||
return nil, fmt.Errorf("failed to validate replica configurations: %w", err)
|
||||
}
|
||||
|
||||
if len(replCfgs) > 0 {
|
||||
replStore.repls = make([]*SQLStore, len(replCfgs))
|
||||
}
|
||||
|
||||
for i, replCfg := range replCfgs {
|
||||
// If the database_instrument_queries feature is enabled, wrap the driver with hooks.
|
||||
if cfg.DatabaseInstrumentQueries {
|
||||
replCfg.Type = WrapDatabaseReplDriverWithHooks(replCfg.Type, uint(i), tracer)
|
||||
}
|
||||
|
||||
s, err := newReadOnlySQLStore(cfg, &replCfg, features, bus, tracer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// initialize and register metrics wrapper around the *sql.DB
|
||||
db := s.engine.DB().DB
|
||||
|
||||
// register the go_sql_stats_connections_* metrics
|
||||
if err := prometheus.Register(sqlstats.NewStatsCollector("grafana_repl", db)); err != nil {
|
||||
s.log.Warn("Failed to register sqlstore stats collector", "error", err)
|
||||
}
|
||||
replStore.repls[i] = s
|
||||
}
|
||||
return replStore, nil
|
||||
}
|
||||
|
||||
// newReadOnlySQLStore creates a new *SQLStore intended for use with a
|
||||
// fully-populated read replica of the main Grafana Database. It provides no
|
||||
// write capabilities and does not run migrations, but other tracing and logging
|
||||
// features are enabled.
|
||||
func newReadOnlySQLStore(cfg *setting.Cfg, dbCfg *DatabaseConfig, features featuremgmt.FeatureToggles, bus bus.Bus, tracer tracing.Tracer) (*SQLStore, error) {
|
||||
s := &SQLStore{
|
||||
log: log.New("replstore"),
|
||||
bus: bus,
|
||||
tracer: tracer,
|
||||
features: features,
|
||||
dbCfg: dbCfg,
|
||||
cfg: cfg,
|
||||
}
|
||||
|
||||
err := s.initReadOnlyEngine(s.engine)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// When there are multiple read replicas, we append an index to the driver name (ex: mysqlWithHooks11).
|
||||
// Remove the index from the end of the driver name to get the original driver name that xorm and other libraries recognize.
|
||||
driverName := digitsRegexp.ReplaceAllString(s.engine.DriverName(), "")
|
||||
|
||||
s.dialect = migrator.NewDialect(driverName)
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// digitsRegexp is used to remove the index from the end of the driver name.
|
||||
var digitsRegexp = regexp.MustCompile("[0-9]+")
|
||||
|
||||
// initReadOnlyEngine initializes ss.engine for read-only operations. The database must be a fully-populated read replica.
|
||||
func (ss *SQLStore) initReadOnlyEngine(engine *xorm.Engine) error {
|
||||
if ss.engine != nil {
|
||||
ss.log.Debug("Already connected to database replica")
|
||||
return nil
|
||||
}
|
||||
|
||||
if engine == nil {
|
||||
var err error
|
||||
engine, err = xorm.NewEngine(ss.dbCfg.Type, ss.dbCfg.ConnectionString)
|
||||
if err != nil {
|
||||
ss.log.Error("failed to connect to database replica", "error", err)
|
||||
return err
|
||||
}
|
||||
// Only for MySQL or MariaDB, verify we can connect with the current connection string's system var for transaction isolation.
|
||||
// If not, create a new engine with a compatible connection string.
|
||||
if ss.dbCfg.Type == migrator.MySQL {
|
||||
engine, err = ss.ensureTransactionIsolationCompatibility(engine, ss.dbCfg.ConnectionString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
engine.SetMaxOpenConns(ss.dbCfg.MaxOpenConn)
|
||||
engine.SetMaxIdleConns(ss.dbCfg.MaxIdleConn)
|
||||
engine.SetConnMaxLifetime(time.Second * time.Duration(ss.dbCfg.ConnMaxLifetime))
|
||||
|
||||
// configure sql logging
|
||||
debugSQL := ss.cfg.Raw.Section("database_replica").Key("log_queries").MustBool(false)
|
||||
if !debugSQL {
|
||||
engine.SetLogger(&xorm.DiscardLogger{})
|
||||
} else {
|
||||
// add stack to database calls to be able to see what repository initiated queries. Top 7 items from the stack as they are likely in the xorm library.
|
||||
engine.SetLogger(NewXormLogger(log.LvlInfo, log.WithSuffix(log.New("replsstore.xorm"), log.CallerContextKey, log.StackCaller(log.DefaultCallerDepth))))
|
||||
engine.ShowSQL(true)
|
||||
engine.ShowExecTime(true)
|
||||
}
|
||||
|
||||
ss.engine = engine
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewRODatabaseConfig creates a new read-only database configuration.
|
||||
func NewRODatabaseConfigs(cfg *setting.Cfg, features featuremgmt.FeatureToggles) ([]DatabaseConfig, error) {
|
||||
if cfg == nil {
|
||||
return nil, errors.New("cfg cannot be nil")
|
||||
}
|
||||
|
||||
// If one replica is configured in the database_replicas section, use it as the default
|
||||
defaultReplCfg := DatabaseConfig{}
|
||||
if err := defaultReplCfg.readConfigSection(cfg, "database_replicas"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err := defaultReplCfg.buildConnectionString(cfg, features)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := []DatabaseConfig{defaultReplCfg}
|
||||
|
||||
// Check for individual replicas in the database_replica section (e.g. database_replica.one, database_replica.cheetara)
|
||||
repls := cfg.Raw.Section("database_replica")
|
||||
if len(repls.ChildSections()) > 0 {
|
||||
for _, sec := range repls.ChildSections() {
|
||||
replCfg := DatabaseConfig{}
|
||||
if err := replCfg.parseConfigIni(sec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := replCfg.buildConnectionString(cfg, features); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret = append(ret, replCfg)
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// ProvideServiceWithReadReplicaForTests wraps the SQLStore in a ReplStore, with the main sqlstore as both the primary and read replica.
|
||||
// TODO: eventually this should be replaced with a more robust test setup which in
|
||||
func ProvideServiceWithReadReplicaForTests(testDB *SQLStore, t sqlutil.ITestDB, cfg *setting.Cfg, features featuremgmt.FeatureToggles, migrations registry.DatabaseMigrator) (*ReplStore, error) {
|
||||
return newReplStore(testDB, testDB), nil
|
||||
}
|
||||
|
||||
// InitTestReplDB initializes a test DB and returns it wrapped in a ReplStore with the main SQLStore as both the primary and read replica.
|
||||
func InitTestReplDB(t sqlutil.ITestDB, opts ...InitTestDBOpt) (*ReplStore, *setting.Cfg) {
|
||||
t.Helper()
|
||||
features := getFeaturesForTesting(opts...)
|
||||
cfg := getCfgForTesting(opts...)
|
||||
ss, err := initTestDB(t, cfg, features, migrations.ProvideOSSMigrations(features), opts...)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to initialize sql repl store: %s", err)
|
||||
}
|
||||
return newReplStore(ss, ss), cfg
|
||||
}
|
||||
|
||||
// InitTestReplDBWithMigration initializes the test DB given custom migrations.
|
||||
func InitTestReplDBWithMigration(t sqlutil.ITestDB, migration registry.DatabaseMigrator, opts ...InitTestDBOpt) *ReplStore {
|
||||
t.Helper()
|
||||
features := getFeaturesForTesting(opts...)
|
||||
cfg := getCfgForTesting(opts...)
|
||||
ss, err := initTestDB(t, cfg, features, migration, opts...)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to initialize sql store: %s", err)
|
||||
}
|
||||
return newReplStore(ss, ss)
|
||||
}
|
||||
|
||||
// newReplStore is a wrapper function that returns a ReplStore with the given primary and read replicas.
|
||||
func newReplStore(primary *SQLStore, readReplicas ...*SQLStore) *ReplStore {
|
||||
ret := &ReplStore{
|
||||
SQLStore: primary,
|
||||
repls: make([]*SQLStore, len(readReplicas)),
|
||||
next: 0,
|
||||
}
|
||||
ret.repls = readReplicas
|
||||
return ret
|
||||
}
|
||||
|
||||
// FakeReplStoreFromStore returns a ReplStore with the given primary
|
||||
// SQLStore and no read replicas. This is a bare-minimum wrapper for testing,
|
||||
// and should be removed when all services are using ReplStore in favor of
|
||||
// InitTestReplDB.
|
||||
func FakeReplStoreFromStore(primary *SQLStore) *ReplStore {
|
||||
return &ReplStore{
|
||||
SQLStore: primary,
|
||||
next: 0,
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/ini.v1"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func TestReplStore_ReadReplica(t *testing.T) {
|
||||
// Using the connection strings to differentiate between the replicas
|
||||
replStore, _ := InitTestReplDB(t)
|
||||
replStore.repls[0].dbCfg.ConnectionString = "repl0"
|
||||
|
||||
repl1 := &SQLStore{dbCfg: &DatabaseConfig{ConnectionString: "repl1"}}
|
||||
repl2 := &SQLStore{dbCfg: &DatabaseConfig{ConnectionString: "repl2"}}
|
||||
replStore.repls = append(replStore.repls, repl1, repl2)
|
||||
|
||||
got := make([]string, 5)
|
||||
for i := 0; i < 5; i++ {
|
||||
got[i] = replStore.ReadReplica().dbCfg.ConnectionString
|
||||
}
|
||||
|
||||
want := []string{"repl0", "repl1", "repl2", "repl0", "repl1"}
|
||||
if cmp.Equal(got, want) == false {
|
||||
t.Fatal("wrong result. Got:", got, "Want:", want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRODatabaseConfig(t *testing.T) {
|
||||
t.Run("valid config", func(t *testing.T) {
|
||||
inicfg, err := ini.Load([]byte(testReplCfg))
|
||||
require.NoError(t, err)
|
||||
cfg, err := setting.NewCfgFromINIFile(inicfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
dbCfgs, err := NewRODatabaseConfigs(cfg, nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, dbCfgs, 3)
|
||||
|
||||
var connStr = func(port int) string {
|
||||
return fmt.Sprintf("grafana:password@tcp(127.0.0.1:%d)/grafana?collation=utf8mb4_unicode_ci&allowNativePasswords=true&clientFoundRows=true", port)
|
||||
}
|
||||
for i, c := range dbCfgs {
|
||||
if !cmp.Equal(c.ConnectionString, connStr(i+3306)) {
|
||||
t.Errorf("wrong result for connection string %d.\nGot: %s,\nWant: %s", i, c.ConnectionString, connStr(i+3306))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var testReplCfg = `
|
||||
[database_replicas]
|
||||
type = mysql
|
||||
name = grafana
|
||||
user = grafana
|
||||
password = password
|
||||
host = 127.0.0.1:3306
|
||||
[database_replica.one]
|
||||
host = 127.0.0.1:3307
|
||||
type = mysql
|
||||
name = grafana
|
||||
user = grafana
|
||||
password = password
|
||||
[database_replica.two]
|
||||
host = 127.0.0.1:3308
|
||||
type = mysql
|
||||
name = grafana
|
||||
user = grafana
|
||||
password = password`
|
||||
Reference in New Issue
Block a user