diff --git a/conf/defaults.ini b/conf/defaults.ini index 61d0ca7e318..166cbf9c326 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -214,6 +214,10 @@ instrument_queries = false # This is useful when databases have auto-generated primary keys enabled. delete_auto_gen_ids = false +# Set to true to skip dashboard UID migrations on startup. +# Improves startup performance for instances with large numbers of annotations who do not plan to downgrade Grafana. +skip_dashboard_uid_migration_on_startup = false + #################################### Cache server ############################# [remote_cache] # Either "redis", "memcached" or "database" default is "database" diff --git a/conf/sample.ini b/conf/sample.ini index bee580aaa98..e9eb8a4ab62 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -207,6 +207,10 @@ # This is useful when databases have auto-generated primary keys enabled. ;delete_auto_gen_ids = false +# Set to true to skip dashboard UID migrations on startup. +# Improves startup performance for instances with large numbers of annotations who do not plan to downgrade Grafana. +;skip_dashboard_uid_migration_on_startup = false + #################################### Cache server ############################# [remote_cache] # Either "redis", "memcached" or "database" default is "database" diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index ea83a3bb553..6455df37ede 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -475,6 +475,10 @@ This setting applies to `sqlite` only and controls the number of times the syste Set to `true` to add metrics and tracing for database queries. The default value is `false`. +#### `skip_dashboard_uid_migration_on_startup` + +Set to true to skip dashboard UID migrations on startup. Improves startup performance for instances with large numbers of annotations who do not plan to downgrade Grafana. The default value is `false`. +
### `[remote_cache]` diff --git a/pkg/services/annotations/annotationsimpl/xorm_store.go b/pkg/services/annotations/annotationsimpl/xorm_store.go index c59dc43f43f..914643858d3 100644 --- a/pkg/services/annotations/annotationsimpl/xorm_store.go +++ b/pkg/services/annotations/annotationsimpl/xorm_store.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "strings" + "sync" "time" "github.com/prometheus/client_golang/prometheus" @@ -40,6 +41,8 @@ func validateTimeRange(item *annotations.Item) error { return nil } +var xormMigrationTrigger sync.Once + type xormRepositoryImpl struct { cfg *setting.Cfg db db.DB @@ -51,10 +54,9 @@ type xormRepositoryImpl struct { } func NewXormStore(cfg *setting.Cfg, l log.Logger, db db.DB, tagService tag.Service, reg prometheus.Registerer) *xormRepositoryImpl { - err := migrations.RunDashboardUIDMigrations(db.GetEngine().NewSession(), db.GetEngine().DriverName()) - if err != nil { - l.Error("failed to populate dashboard_uid for annotations", "error", err) - } + xormMigrationTrigger.Do(func() { + triggerAlwaysOnMigrations(cfg, l, db) + }) repo := &xormRepositoryImpl{ cfg: cfg, @@ -96,6 +98,19 @@ func NewXormStore(cfg *setting.Cfg, l log.Logger, db db.DB, tagService tag.Servi return repo } +func triggerAlwaysOnMigrations(cfg *setting.Cfg, l log.Logger, db db.DB) { + sec := cfg.Raw.Section("database") + skipDashboardUIDMigration := sec.Key("skip_dashboard_uid_migration_on_startup").MustBool(false) + if skipDashboardUIDMigration { + l.Debug("skipped dashboard UID startup migration") + return + } + err := migrations.RunDashboardUIDMigrations(db.GetEngine().NewSession(), db.GetEngine().DriverName()) + if err != nil { + l.Error("failed to populate dashboard_uid for annotations", "error", err) + } +} + func (r *xormRepositoryImpl) Type() string { return "sql" } diff --git a/pkg/services/annotations/annotationsimpl/xorm_store_test.go b/pkg/services/annotations/annotationsimpl/xorm_store_test.go index 43fff3be327..de81c7bba8c 100644 --- a/pkg/services/annotations/annotationsimpl/xorm_store_test.go +++ b/pkg/services/annotations/annotationsimpl/xorm_store_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strings" + "sync" "testing" "github.com/prometheus/client_golang/prometheus" @@ -654,6 +655,140 @@ func TestIntegrationAnnotations(t *testing.T) { }) } +func TestIntegrationAnnotationsAlwaysOnMigrations(t *testing.T) { + tutil.SkipIntegrationTestInShortMode(t) + + sql := db.InitTestDB(t) + cfg := setting.NewCfg() + cfg.AnnotationMaximumTagsLength = 60 + + t.Run("NewXormStore should call triggerAlwaysOnMigrations and skip migrations", func(t *testing.T) { + cfg.Raw.Section("database").Key("skip_dashboard_uid_migration_on_startup").SetValue("true") + + l := log.New("annotation.test") + + dashboard := testutil.CreateDashboard(t, sql, cfg, featuremgmt.WithFeatures(), dashboards.SaveDashboardCommand{ + UserID: 1, + OrgID: 1, + Dashboard: simplejson.NewFromAny(map[string]any{ + "title": "Test Skip Dashboard", + "uid": "test-skip-uid", + }), + }) + + tempStore := NewXormStore(cfg, l, sql, tagimpl.ProvideService(sql), nil) + annotation := &annotations.Item{ + OrgID: 1, + UserID: 1, + DashboardID: dashboard.ID, // nolint: staticcheck + DashboardUID: dashboard.UID, + Text: "test migration skip", + Type: "alert", + Epoch: 100, + } + err := tempStore.Add(context.Background(), annotation) + require.NoError(t, err) + + t.Cleanup(func() { + err := tempStore.Delete(context.Background(), &annotations.DeleteParams{ID: annotation.ID, OrgID: 1}) + assert.NoError(t, err) + }) + + err = sql.WithDbSession(context.Background(), func(sess *db.Session) error { + _, err := sess.Exec("UPDATE annotation SET dashboard_uid = NULL WHERE id = ?", annotation.ID) + return err + }) + require.NoError(t, err) + + xormMigrationTrigger = sync.Once{} + store := NewXormStore(cfg, l, sql, tagimpl.ProvideService(sql), prometheus.NewRegistry()) + + require.NotNil(t, store) + assert.Equal(t, "sql", store.Type()) + + var result struct { + DashboardUID *string `xorm:"dashboard_uid"` + } + err = sql.WithDbSession(context.Background(), func(sess *db.Session) error { + has, err := sess.Table("annotation"). + Where("id = ?", annotation.ID). + Get(&result) + if err != nil { + return err + } + if !has { + return fmt.Errorf("annotation not found") + } + return nil + }) + require.NoError(t, err) + assert.Nil(t, result.DashboardUID, "dashboard_uid should still be NULL when migration is skipped") + }) + + t.Run("NewXormStore should call triggerAlwaysOnMigrations and run migrations", func(t *testing.T) { + cfg.Raw.Section("database").Key("skip_dashboard_uid_migration_on_startup").SetValue("false") + l := log.New("annotation.test") + + dashboard := testutil.CreateDashboard(t, sql, cfg, featuremgmt.WithFeatures(), dashboards.SaveDashboardCommand{ + UserID: 1, + OrgID: 1, + Dashboard: simplejson.NewFromAny(map[string]any{ + "title": "Test Run Dashboard", + "uid": "test-run-uid", + }), + }) + + tempStore := NewXormStore(cfg, l, sql, tagimpl.ProvideService(sql), nil) + annotation := &annotations.Item{ + OrgID: 1, + UserID: 1, + DashboardID: dashboard.ID, // nolint: staticcheck + DashboardUID: dashboard.UID, + Text: "test migration run", + Type: "alert", + Epoch: 100, + } + err := tempStore.Add(context.Background(), annotation) + require.NoError(t, err) + + t.Cleanup(func() { + err := tempStore.Delete(context.Background(), &annotations.DeleteParams{ID: annotation.ID, OrgID: 1}) + assert.NoError(t, err) + }) + + err = sql.WithDbSession(context.Background(), func(sess *db.Session) error { + _, err := sess.Exec("UPDATE annotation SET dashboard_uid = NULL WHERE id = ?", annotation.ID) + return err + }) + require.NoError(t, err) + + xormMigrationTrigger = sync.Once{} + store := NewXormStore(cfg, l, sql, tagimpl.ProvideService(sql), prometheus.NewRegistry()) + + require.NotNil(t, store) + assert.Equal(t, "sql", store.Type()) + + var result struct { + DashboardUID *string `xorm:"dashboard_uid"` + } + err = sql.WithDbSession(context.Background(), func(sess *db.Session) error { + has, err := sess.Table("annotation"). + Where("id = ?", annotation.ID). + Get(&result) + if err != nil { + return err + } + if !has { + return fmt.Errorf("annotation not found") + } + return nil + }) + require.NoError(t, err) + require.NotNil(t, result.DashboardUID, "dashboard_uid should not be NULL when migration runs") + assert.Equal(t, "test-run-uid", *result.DashboardUID, "dashboard_uid should be populated when migration runs") + }) +} + func BenchmarkFindTags_10k(b *testing.B) { benchmarkFindTags(b, 10000) }