DualWriter: Error when folder+dashboard modes do not match when running mode4+ (#111286)

This commit is contained in:
Ryan McKinley
2025-09-18 16:43:39 +03:00
committed by GitHub
parent 9d822646dd
commit 9743e0c903
5 changed files with 79 additions and 36 deletions
+8 -2
View File
@@ -507,7 +507,10 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
if err != nil {
return nil, err
}
dualwriteService := dualwrite.ProvideService(featureToggles, registerer, kvStore, cfg)
dualwriteService, err := dualwrite.ProvideService(featureToggles, kvStore, cfg)
if err != nil {
return nil, err
}
sortService := sort.ProvideService()
folderimplService := folderimpl.ProvideService(folderStoreImpl, accessControl, inProcBus, dashboardsStore, userService, sqlStore, featureToggles, bundleregistryService, publicDashboardServiceWrapperImpl, cfg, registerer, tracer, resourceClient, dualwriteService, sortService, eventualRestConfigProvider)
searchService := searchV2.ProvideService(cfg, sqlStore, entityEventsService, acimplService, tracingService, featureToggles, orgService, userService, folderimplService)
@@ -1102,7 +1105,10 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
if err != nil {
return nil, err
}
dualwriteService := dualwrite.ProvideService(featureToggles, registerer, kvStore, cfg)
dualwriteService, err := dualwrite.ProvideService(featureToggles, kvStore, cfg)
if err != nil {
return nil, err
}
sortService := sort.ProvideService()
folderimplService := folderimpl.ProvideService(folderStoreImpl, accessControl, inProcBus, dashboardsStore, userService, sqlStore, featureToggles, bundleregistryService, publicDashboardServiceWrapperImpl, cfg, registerer, tracer, resourceClient, dualwriteService, sortService, eventualRestConfigProvider)
searchService := searchV2.ProvideService(cfg, sqlStore, entityEventsService, acimplService, tracingService, featureToggles, orgService, userService, folderimplService)
@@ -77,7 +77,8 @@ func TestRuntime_Create(t *testing.T) {
tt.setupStorageFn(us.Mock, tt.input)
}
m := ProvideService(featuremgmt.WithFeatures(featuremgmt.FlagManagedDualWriter), nil, kvstore.NewFakeKVStore(), nil)
m, err := ProvideService(featuremgmt.WithFeatures(featuremgmt.FlagManagedDualWriter), kvstore.NewFakeKVStore(), nil)
require.NoError(t, err)
dw, err := m.NewStorage(kind, ls, us)
require.NoError(t, err)
@@ -149,7 +150,8 @@ func TestRuntime_Get(t *testing.T) {
tt.setupStorageFn(us.Mock, name)
}
m := ProvideService(featuremgmt.WithFeatures(featuremgmt.FlagManagedDualWriter), nil, kvstore.NewFakeKVStore(), nil)
m, err := ProvideService(featuremgmt.WithFeatures(featuremgmt.FlagManagedDualWriter), kvstore.NewFakeKVStore(), nil)
require.NoError(t, err)
dw, err := m.NewStorage(kind, ls, us)
require.NoError(t, err)
status, err := m.Status(context.Background(), kind)
@@ -233,7 +235,8 @@ func TestRuntime_CreateWhileMigrating(t *testing.T) {
}
// Shared provider across all tests
dual := ProvideService(featuremgmt.WithFeatures(featuremgmt.FlagManagedDualWriter), nil, kvstore.NewFakeKVStore(), nil)
dual, err := ProvideService(featuremgmt.WithFeatures(featuremgmt.FlagManagedDualWriter), kvstore.NewFakeKVStore(), nil)
require.NoError(t, err)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
+21 -22
View File
@@ -5,7 +5,6 @@ import (
"fmt"
"time"
"github.com/prometheus/client_golang/prometheus"
"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/grafana/grafana-app-sdk/logging"
@@ -24,39 +23,39 @@ func ProvideStaticServiceForTests(cfg *setting.Cfg) Service {
func ProvideService(
features featuremgmt.FeatureToggles,
reg prometheus.Registerer,
kv kvstore.KVStore,
cfg *setting.Cfg,
) Service {
) (Service, error) {
enabled := features.IsEnabledGlobally(featuremgmt.FlagManagedDualWriter) ||
features.IsEnabledGlobally(featuremgmt.FlagProvisioning) // required for git provisioning
if cfg != nil {
// Avoid dynamic behavior when things are explicitly configured to mode5
allMode5 := true
for _, gr := range []string{
"dashboards.dashboard.grafana.app",
"folders.folder.grafana.app",
} {
if cfg.UnifiedStorage[gr].DualWriterMode != rest.Mode5 {
allMode5 = false
break
if !enabled {
return &staticService{cfg}, nil
}
if cfg != nil {
foldersMode := cfg.UnifiedStorage["folders.folder.grafana.app"].DualWriterMode
dashboardsMode := cfg.UnifiedStorage["dashboards.dashboard.grafana.app"].DualWriterMode
// If both are fully on unified (Mode5), the dynamic service is not needed.
if foldersMode == rest.Mode5 && dashboardsMode == rest.Mode5 {
return &staticService{cfg}, nil
}
if (foldersMode >= rest.Mode4 || dashboardsMode >= rest.Mode4) && foldersMode != dashboardsMode {
return nil, fmt.Errorf("dashboards and folders must use the same mode when reading from unified storage")
}
}
if allMode5 || !enabled {
return &staticService{cfg} // fallback to using the dual write flags from cfg
}
}
db := &keyvalueDB{
db: kv,
logger: logging.DefaultLogger.With("logger", "dualwrite.kv"),
}
return &service{
db: db,
db: &keyvalueDB{
db: kv,
logger: logging.DefaultLogger.With("logger", "dualwrite.kv"),
},
enabled: enabled,
}
}, nil
}
type service struct {
@@ -17,7 +17,8 @@ import (
func TestService(t *testing.T) {
t.Run("dynamic", func(t *testing.T) {
ctx := context.Background()
mode := ProvideService(featuremgmt.WithFeatures(), nil, kvstore.NewFakeKVStore(), nil)
mode, err := ProvideService(featuremgmt.WithFeatures(), kvstore.NewFakeKVStore(), nil)
require.NoError(t, err)
gr := schema.GroupResource{Group: "ggg", Resource: "rrr"}
status, err := mode.Status(ctx, gr)
@@ -69,6 +70,7 @@ func TestService(t *testing.T) {
isStatic bool
foldersFromUnified bool
dashboardsFromUnified bool
error string
}
for _, tc := range []testCase{{
@@ -93,14 +95,40 @@ func TestService(t *testing.T) {
cfg: setting.Cfg{
UnifiedStorage: map[string]setting.UnifiedStorageConfig{
"dashboards.dashboard.grafana.app": {
DualWriterMode: rest.Mode5,
DualWriterMode: rest.Mode3,
},
},
}}, {
name: "invalid folder mode4",
flags: featuremgmt.WithFeatures(featuremgmt.FlagProvisioning),
error: "must use the same mode",
cfg: setting.Cfg{
UnifiedStorage: map[string]setting.UnifiedStorageConfig{
"folders.folder.grafana.app": {
DualWriterMode: rest.Mode4,
},
},
}}, {
name: "invalid dashboards mode4",
flags: featuremgmt.WithFeatures(featuremgmt.FlagProvisioning),
error: "must use the same mode",
cfg: setting.Cfg{
UnifiedStorage: map[string]setting.UnifiedStorageConfig{
"dashboards.dashboard.grafana.app": {
DualWriterMode: rest.Mode4,
},
},
}},
} {
t.Run(tc.name, func(t *testing.T) {
ctx := context.Background()
svc := ProvideService(tc.flags, nil, kvstore.NewFakeKVStore(), &tc.cfg)
svc, err := ProvideService(tc.flags, kvstore.NewFakeKVStore(), &tc.cfg)
if tc.error != "" {
require.ErrorContains(t, err, tc.error)
require.Nil(t, svc, "expect a nil service when an error exts")
return
}
require.NoError(t, err)
_, isStatic := svc.(*staticService)
require.Equal(t, tc.isStatic, isStatic)
+13 -6
View File
@@ -19,7 +19,6 @@ import (
"k8s.io/client-go/rest"
dashboardV0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
foldersV1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
@@ -43,7 +42,7 @@ func TestIntegrationFolderTree(t *testing.T) {
}
modes := []grafanarest.DualWriterMode{
// grafanarest.Mode1, (nothing new tested)
// grafanarest.Mode1, (nothing new tested in mode 0 or 1)
grafanarest.Mode2, // write both, read legacy
grafanarest.Mode3, // write both, read unified
grafanarest.Mode4,
@@ -51,7 +50,7 @@ func TestIntegrationFolderTree(t *testing.T) {
}
for _, mode := range modes {
t.Run(fmt.Sprintf("mode %d", mode), func(t *testing.T) {
flags := []string{featuremgmt.FlagManagedDualWriter}
flags := []string{}
if mode >= grafanarest.Mode3 { // make sure modes 0-3 work without it
flags = append(flags, featuremgmt.FlagUnifiedStorageSearch)
}
@@ -61,12 +60,13 @@ func TestIntegrationFolderTree(t *testing.T) {
APIServerStorageType: "unified",
EnableFeatureToggles: flags,
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
foldersV1.RESOURCEGROUP: {
"dashboards.dashboard.grafana.app": {
DualWriterMode: mode,
},
"folders.folder.grafana.app": {
DualWriterMode: mode,
},
},
// We set it to 1 here, so we always get forced pagination based on the response size.
UnifiedStorageMaxPageSizeBytes: 1,
})
defer helper.Shutdown()
@@ -209,6 +209,13 @@ func (f *FolderDefinition) CreateWithLegacyAPI(t *testing.T, h *apis.K8sTestHelp
require.NoError(t, result.Error(), f.Name)
require.Equal(t, int(http.StatusOK), statusCode, f.Name)
}
// Now check that we could get the folder
result = client.Get().AbsPath("api", "folders", f.Name).
Do(context.Background()).
StatusCode(&statusCode)
require.NoErrorf(t, result.Error(), "get folder after create: %s", f.Name)
require.Equal(t, int(http.StatusOK), statusCode, f.Name)
}
for _, child := range f.Children {