diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index dfa24406d21..2829497ad0b 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -1218,4 +1218,9 @@ export interface FeatureToggles { * @default true */ preventPanelChromeOverflow?: boolean; + /** + * Load plugins during store service startup instead of wire provider + * @default false + */ + pluginStoreServiceLoading?: boolean; } diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index c97d4edb0f9..4e2f477c74c 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -549,7 +549,10 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api } errorRegistry := pluginerrs.ProvideErrorTracker() loaderLoader := loader.ProvideService(pluginManagementCfg, discovery, bootstrap, validate, initialize, terminate, errorRegistry) - pluginstoreService := pluginstore.ProvideService(inMemory, sourcesService, loaderLoader) + pluginstoreService, err := pluginstore.ProvideService(inMemory, sourcesService, loaderLoader, featureToggles) + if err != nil { + return nil, err + } filestoreService := filestore.ProvideService(inMemory) fileStoreManager := dashboards.ProvideFileStoreManager(pluginstoreService, filestoreService) folderPermissionsService, err := ossaccesscontrol.ProvideFolderPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, folderimplService, acimplService, teamService, userService, actionSetService) @@ -1155,7 +1158,10 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac } errorRegistry := pluginerrs.ProvideErrorTracker() loaderLoader := loader.ProvideService(pluginManagementCfg, discovery, bootstrap, validate, initialize, terminate, errorRegistry) - pluginstoreService := pluginstore.ProvideService(inMemory, sourcesService, loaderLoader) + pluginstoreService, err := pluginstore.ProvideService(inMemory, sourcesService, loaderLoader, featureToggles) + if err != nil { + return nil, err + } filestoreService := filestore.ProvideService(inMemory) fileStoreManager := dashboards.ProvideFileStoreManager(pluginstoreService, filestoreService) folderPermissionsService, err := ossaccesscontrol.ProvideFolderPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, folderimplService, acimplService, teamService, userService, actionSetService) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 2dd58e490a4..cb7ed8020a5 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -2111,6 +2111,14 @@ var ( Owner: grafanaFrontendPlatformSquad, Expression: "true", }, + { + Name: "pluginStoreServiceLoading", + Description: "Load plugins during store service startup instead of wire provider", + Stage: FeatureStageExperimental, + FrontendOnly: false, + Owner: grafanaPluginsPlatformSquad, + Expression: "false", + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index ad0a99827db..744e7c10dfa 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -271,3 +271,4 @@ tempoSearchBackendMigration,GA,@grafana/oss-big-tent,false,true,false cdnPluginsLoadFirst,experimental,@grafana/plugins-platform-backend,false,false,false cdnPluginsUrls,experimental,@grafana/plugins-platform-backend,false,false,false preventPanelChromeOverflow,preview,@grafana/grafana-frontend-platform,false,false,true +pluginStoreServiceLoading,experimental,@grafana/plugins-platform-backend,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index c2a51b6360a..c501ebb4e91 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -1093,4 +1093,8 @@ const ( // FlagPreventPanelChromeOverflow // Restrict PanelChrome contents with overflow: hidden; FlagPreventPanelChromeOverflow = "preventPanelChromeOverflow" + + // FlagPluginStoreServiceLoading + // Load plugins during store service startup instead of wire provider + FlagPluginStoreServiceLoading = "pluginStoreServiceLoading" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 07201201e35..cab882b702c 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -2936,6 +2936,19 @@ "expression": "false" } }, + { + "metadata": { + "name": "pluginStoreServiceLoading", + "resourceVersion": "1760712768362", + "creationTimestamp": "2025-10-17T14:52:48Z" + }, + "spec": { + "description": "Load plugins during store service startup instead of wire provider", + "stage": "experimental", + "codeowner": "@grafana/plugins-platform-backend", + "expression": "false" + } + }, { "metadata": { "name": "pluginsAutoUpdate", diff --git a/pkg/services/pluginsintegration/pluginstore/store.go b/pkg/services/pluginsintegration/pluginstore/store.go index f486218040e..e9cc99d78f5 100644 --- a/pkg/services/pluginsintegration/pluginstore/store.go +++ b/pkg/services/pluginsintegration/pluginstore/store.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/loader" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/sources" + "github.com/grafana/grafana/pkg/services/featuremgmt" "golang.org/x/sync/errgroup" ) @@ -33,11 +34,36 @@ type Service struct { pluginRegistry registry.Service pluginLoader loader.Service pluginSources sources.Registry + loadOnStartup bool } func ProvideService(pluginRegistry registry.Service, pluginSources sources.Registry, - pluginLoader loader.Service) *Service { - return New(pluginRegistry, pluginLoader, pluginSources) + pluginLoader loader.Service, features featuremgmt.FeatureToggles) (*Service, error) { + if features.IsEnabledGlobally(featuremgmt.FlagPluginStoreServiceLoading) { + s := New(pluginRegistry, pluginLoader, pluginSources) + s.loadOnStartup = true + return s, nil + } + + ctx := context.Background() + start := time.Now() + totalPlugins := 0 + logger := log.New("plugin.store") + logger.Info("Loading plugins...") + + for _, ps := range pluginSources.List(ctx) { + loadedPlugins, err := pluginLoader.Load(ctx, ps) + if err != nil { + logger.Error("Loading plugin source failed", "source", ps.PluginClass(ctx), "error", err) + return nil, err + } + + totalPlugins += len(loadedPlugins) + } + + logger.Info("Plugins loaded", "count", totalPlugins, "duration", time.Since(start)) + + return New(pluginRegistry, pluginLoader, pluginSources), nil } func (s *Service) Run(ctx context.Context) error { @@ -50,6 +76,7 @@ func (s *Service) Run(ctx context.Context) error { func NewPluginStoreForTest(pluginRegistry registry.Service, pluginLoader loader.Service, pluginSources sources.Registry) (*Service, error) { s := New(pluginRegistry, pluginLoader, pluginSources) + s.loadOnStartup = true if err := s.StartAsync(context.Background()); err != nil { return nil, err } @@ -70,6 +97,9 @@ func New(pluginRegistry registry.Service, pluginLoader loader.Service, pluginSou } func (s *Service) starting(ctx context.Context) error { + if !s.loadOnStartup { + return nil + } start := time.Now() totalPlugins := 0 logger := log.New(ServiceName) @@ -85,7 +115,6 @@ func (s *Service) starting(ctx context.Context) error { } logger.Info("Plugins loaded", "count", totalPlugins, "duration", time.Since(start)) - return nil } diff --git a/pkg/services/pluginsintegration/pluginstore/store_test.go b/pkg/services/pluginsintegration/pluginstore/store_test.go index ae1329e0379..c3201744096 100644 --- a/pkg/services/pluginsintegration/pluginstore/store_test.go +++ b/pkg/services/pluginsintegration/pluginstore/store_test.go @@ -11,45 +11,83 @@ import ( "github.com/grafana/grafana/pkg/plugins/backendplugin" "github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/plugins/manager/pluginfakes" + "github.com/grafana/grafana/pkg/services/featuremgmt" ) func TestStore_ProvideService(t *testing.T) { t.Run("Plugin sources are added in order", func(t *testing.T) { - var loadedSrcs []plugins.Class - l := &pluginfakes.FakeLoader{ - LoadFunc: func(ctx context.Context, src plugins.PluginSource) ([]*plugins.Plugin, error) { - loadedSrcs = append(loadedSrcs, src.PluginClass(ctx)) - return nil, nil + tests := []struct { + name string + featureEnabled bool + expectedLoadOnStartup bool + expectedBeforeStart []plugins.Class + expectedAfterStart []plugins.Class + }{ + { + name: "with FlagPluginStoreServiceLoading disabled", + featureEnabled: false, + expectedLoadOnStartup: false, + expectedBeforeStart: []plugins.Class{"1", "2", "3"}, + expectedAfterStart: []plugins.Class{"1", "2", "3"}, + }, + { + name: "with FlagPluginStoreServiceLoading enabled", + featureEnabled: true, + expectedLoadOnStartup: true, + expectedBeforeStart: nil, + expectedAfterStart: []plugins.Class{"1", "2", "3"}, }, } - srcs := &pluginfakes.FakeSourceRegistry{ListFunc: func(_ context.Context) []plugins.PluginSource { - return []plugins.PluginSource{ - &pluginfakes.FakePluginSource{ - PluginClassFunc: func(ctx context.Context) plugins.Class { - return "1" + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var loadedSrcs []plugins.Class + l := &pluginfakes.FakeLoader{ + LoadFunc: func(ctx context.Context, src plugins.PluginSource) ([]*plugins.Plugin, error) { + loadedSrcs = append(loadedSrcs, src.PluginClass(ctx)) + return nil, nil }, - }, - &pluginfakes.FakePluginSource{ - PluginClassFunc: func(ctx context.Context) plugins.Class { - return "2" - }, - }, - &pluginfakes.FakePluginSource{ - PluginClassFunc: func(ctx context.Context) plugins.Class { - return "3" - }, - }, - } - }} + } - service := ProvideService(pluginfakes.NewFakePluginRegistry(), srcs, l) - ctx := context.Background() - err := service.StartAsync(ctx) - require.NoError(t, err) - err = service.AwaitRunning(ctx) - require.NoError(t, err) - require.Equal(t, []plugins.Class{"1", "2", "3"}, loadedSrcs) + srcs := &pluginfakes.FakeSourceRegistry{ListFunc: func(_ context.Context) []plugins.PluginSource { + return []plugins.PluginSource{ + &pluginfakes.FakePluginSource{ + PluginClassFunc: func(ctx context.Context) plugins.Class { + return "1" + }, + }, + &pluginfakes.FakePluginSource{ + PluginClassFunc: func(ctx context.Context) plugins.Class { + return "2" + }, + }, + &pluginfakes.FakePluginSource{ + PluginClassFunc: func(ctx context.Context) plugins.Class { + return "3" + }, + }, + } + }} + + var features featuremgmt.FeatureToggles + if tt.featureEnabled { + features = featuremgmt.WithFeatures(featuremgmt.FlagPluginStoreServiceLoading) + } else { + features = featuremgmt.WithFeatures() + } + + service, err := ProvideService(pluginfakes.NewFakePluginRegistry(), srcs, l, features) + require.Equal(t, tt.expectedLoadOnStartup, service.loadOnStartup) + require.Equal(t, tt.expectedBeforeStart, loadedSrcs) + require.NoError(t, err) + ctx := context.Background() + err = service.StartAsync(ctx) + require.NoError(t, err) + err = service.AwaitRunning(ctx) + require.NoError(t, err) + require.Equal(t, tt.expectedAfterStart, loadedSrcs) + }) + } }) }