[release-12.3.1] refactor(annotations): Allow skipping always on dashboard UID migrations (#114096)

refactor(annotations): Allow skipping always on dashboard UID migrations (#113780)

(cherry picked from commit b70c6a726f)

Co-authored-by: Andres Torres <janthoe@users.noreply.github.com>
This commit is contained in:
grafana-delivery-bot[bot]
2025-11-18 17:08:55 -05:00
committed by GitHub
co-authored by Andres Torres
parent 0fa02c1212
commit 95c7703d35
5 changed files with 166 additions and 4 deletions
+4
View File
@@ -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"
+4
View File
@@ -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"
@@ -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`.
<hr />
### `[remote_cache]`
@@ -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"
}
@@ -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)
}