From 60c64449b3f7cf0413e077ff92ea85878e85f2a5 Mon Sep 17 00:00:00 2001 From: Marcus Andersson Date: Tue, 18 Jan 2022 10:43:42 +0100 Subject: [PATCH] Chore: Change so we cache loading plugins by its version (#41367) (#44108) * making it possible to cache plugins based on the version. * feat(plugincache): introduce function to invalidate entries * removed todo's * added tests for the cache buster. * fixed tests. * fixed failing tests. Co-authored-by: Jack Westbrook (cherry picked from commit e5421dd53e6992c955c045f98fd5b0ca39e9b8bb) --- packages/grafana-data/src/types/config.ts | 12 ++++- packages/grafana-data/src/types/index.ts | 2 +- packages/grafana-runtime/src/config.ts | 3 +- pkg/api/frontendsettings.go | 26 ++++++++--- public/app/app.ts | 4 +- .../plugins/pluginCacheBuster.test.ts | 42 +++++++++++++++++ .../app/features/plugins/pluginCacheBuster.ts | 45 +++++++++++++++++++ public/app/features/plugins/plugin_loader.ts | 21 +++++---- 8 files changed, 133 insertions(+), 22 deletions(-) create mode 100644 public/app/features/plugins/pluginCacheBuster.test.ts create mode 100644 public/app/features/plugins/pluginCacheBuster.ts diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index e28bd2455da..7f2f1eea7af 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -76,6 +76,16 @@ export interface SentryConfig { sampleRate: number; } +/** + * Describes the plugins that should be preloaded prior to start Grafana. + * + * @public + */ +export type PreloadPlugin = { + path: string; + version: string; +}; + /** * Describes all the different Grafana configuration values available for an instance. * @@ -119,7 +129,7 @@ export interface GrafanaConfig { liveEnabled: boolean; theme: GrafanaTheme; theme2: GrafanaTheme2; - pluginsToPreload: string[]; + pluginsToPreload: PreloadPlugin[]; featureToggles: FeatureToggles; licenseInfo: LicenseInfo; http2Enabled: boolean; diff --git a/packages/grafana-data/src/types/index.ts b/packages/grafana-data/src/types/index.ts index afc91463a8e..4ae1866c26e 100644 --- a/packages/grafana-data/src/types/index.ts +++ b/packages/grafana-data/src/types/index.ts @@ -32,5 +32,5 @@ export * from './live'; export * from './variables'; export * from './geometry'; export { isUnsignedPluginSignature } from './pluginSignature'; -export { GrafanaConfig, BuildInfo, FeatureToggles, LicenseInfo } from './config'; +export { GrafanaConfig, BuildInfo, FeatureToggles, LicenseInfo, PreloadPlugin } from './config'; export * from './alerts'; diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index 014fccb77ea..5878cd33397 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -10,6 +10,7 @@ import { LicenseInfo, MapLayerOptions, PanelPluginMeta, + PreloadPlugin, systemDateFormats, SystemDateFormatSettings, } from '@grafana/data'; @@ -58,7 +59,7 @@ export class GrafanaBootConfig implements GrafanaConfig { liveEnabled = true; theme: GrafanaTheme; theme2: GrafanaTheme2; - pluginsToPreload: string[] = []; + pluginsToPreload: PreloadPlugin[] = []; featureToggles: FeatureToggles = { ngalert: false, accesscontrol: false, diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 3b3c533679f..f1a88e109a9 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -15,6 +15,11 @@ import ( "github.com/grafana/grafana/pkg/setting" ) +type PreloadPlugin struct { + Path string `json:"path"` + Version string `json:"version"` +} + func (hs *HTTPServer) getFSDataSources(c *models.ReqContext, enabledPlugins *plugins.EnabledPlugins) (map[string]interface{}, error) { orgDataSources := make([]*models.DataSource, 0) @@ -129,10 +134,13 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i return nil, err } - pluginsToPreload := []string{} + pluginsToPreload := []*PreloadPlugin{} for _, app := range enabledPlugins.Apps { if app.Preload { - pluginsToPreload = append(pluginsToPreload, app.Module) + pluginsToPreload = append(pluginsToPreload, &PreloadPlugin{ + Path: app.Module, + Version: app.Info.Version, + }) } } @@ -148,9 +156,12 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i defaultDS = n } - meta := dsM["meta"].(*plugins.DataSourcePlugin) - if meta.Preload { - pluginsToPreload = append(pluginsToPreload, meta.Module) + module, _ := dsM["module"].(string) + if preload, _ := dsM["preload"].(bool); preload && module != "" { + pluginsToPreload = append(pluginsToPreload, &PreloadPlugin{ + Path: module, + Version: dsM["info"].(map[string]interface{})["version"].(string), + }) } } @@ -161,7 +172,10 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i } if panel.Preload { - pluginsToPreload = append(pluginsToPreload, panel.Module) + pluginsToPreload = append(pluginsToPreload, &PreloadPlugin{ + Path: panel.Module, + Version: panel.Info.Version, + }) } panels[panel.Id] = map[string]interface{}{ diff --git a/public/app/app.ts b/public/app/app.ts index 44298b72740..c65d3edf89c 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -107,8 +107,8 @@ export class GrafanaApp { // Preload selected app plugins const promises: Array> = []; - for (const modulePath of config.pluginsToPreload) { - promises.push(importPluginModule(modulePath)); + for (const plugin of config.pluginsToPreload) { + promises.push(importPluginModule(plugin.path, plugin.version)); } await Promise.all(promises); diff --git a/public/app/features/plugins/pluginCacheBuster.test.ts b/public/app/features/plugins/pluginCacheBuster.test.ts new file mode 100644 index 00000000000..131d5982809 --- /dev/null +++ b/public/app/features/plugins/pluginCacheBuster.test.ts @@ -0,0 +1,42 @@ +import { invalidatePluginInCache, locateWithCache, registerPluginInCache } from './pluginCacheBuster'; + +describe('PluginCacheBuster', () => { + const now = 12345; + + it('should append plugin version as cache flag if plugin is registered in buster', () => { + const slug = 'bubble-chart-1'; + const version = 'v1.0.0'; + const path = resolvePath(slug); + const address = `http://localhost:3000/public/${path}.js`; + + registerPluginInCache({ path, version }); + + const url = `${address}?_cache=${encodeURI(version)}`; + expect(locateWithCache({ address }, now)).toBe(url); + }); + + it('should append Date.now as cache flag if plugin is not registered in buster', () => { + const slug = 'bubble-chart-2'; + const address = `http://localhost:3000/public/${resolvePath(slug)}.js`; + + const url = `${address}?_cache=${encodeURI(String(now))}`; + expect(locateWithCache({ address }, now)).toBe(url); + }); + + it('should append Date.now as cache flag if plugin is invalidated in buster', () => { + const slug = 'bubble-chart-3'; + const version = 'v1.0.0'; + const path = resolvePath(slug); + const address = `http://localhost:3000/public/${path}.js`; + + registerPluginInCache({ path, version }); + invalidatePluginInCache(slug); + + const url = `${address}?_cache=${encodeURI(String(now))}`; + expect(locateWithCache({ address }, now)).toBe(url); + }); +}); + +function resolvePath(slug: string): string { + return `plugins/${slug}/module`; +} diff --git a/public/app/features/plugins/pluginCacheBuster.ts b/public/app/features/plugins/pluginCacheBuster.ts new file mode 100644 index 00000000000..d8d3860bd7c --- /dev/null +++ b/public/app/features/plugins/pluginCacheBuster.ts @@ -0,0 +1,45 @@ +const cache: Record = {}; +const initializedAt: number = Date.now(); + +type CacheablePlugin = { + path: string; + version: string; +}; + +export function registerPluginInCache({ path, version }: CacheablePlugin): void { + if (!cache[path]) { + cache[path] = encodeURI(version); + } +} + +export function invalidatePluginInCache(pluginId: string): void { + const path = `plugins/${pluginId}/module`; + if (cache[path]) { + delete cache[path]; + } +} + +export function locateWithCache(load: { address: string }, defaultBust = initializedAt): string { + const { address } = load; + const path = extractPath(address); + + if (!path) { + return `${address}?_cache=${defaultBust}`; + } + + const version = cache[path]; + const bust = version || defaultBust; + return `${address}?_cache=${bust}`; +} + +function extractPath(address: string): string | undefined { + const match = /\/public\/(plugins\/.+\/module)\.js/i.exec(address); + if (!match) { + return; + } + const [_, path] = match; + if (!path) { + return; + } + return path; +} diff --git a/public/app/features/plugins/plugin_loader.ts b/public/app/features/plugins/plugin_loader.ts index 160ca5949d5..4a71b2bbb45 100644 --- a/public/app/features/plugins/plugin_loader.ts +++ b/public/app/features/plugins/plugin_loader.ts @@ -33,6 +33,8 @@ import * as emotion from '@emotion/css'; import * as grafanaData from '@grafana/data'; import * as grafanaUIraw from '@grafana/ui'; import * as grafanaRuntime from '@grafana/runtime'; +import { GenericDataSourcePlugin } from '../datasources/settings/PluginSettings'; +import { locateWithCache, registerPluginInCache } from './pluginCacheBuster'; // Help the 6.4 to 6.5 migration // The base classes were moved from @grafana/ui to @grafana/data @@ -49,13 +51,7 @@ import * as rxjsOperators from 'rxjs/operators'; // routing import * as reactRouter from 'react-router-dom'; -// add cache busting -const bust = `?_cache=${Date.now()}`; -function locate(load: { address: string }) { - return load.address + bust; -} - -grafanaRuntime.SystemJS.registry.set('plugin-loader', grafanaRuntime.SystemJS.newModule({ locate: locate })); +grafanaRuntime.SystemJS.registry.set('plugin-loader', grafanaRuntime.SystemJS.newModule({ locate: locateWithCache })); grafanaRuntime.SystemJS.config({ baseURL: 'public', @@ -173,7 +169,11 @@ for (const flotDep of flotDeps) { exposeToPlugin(flotDep, { fakeDep: 1 }); } -export async function importPluginModule(path: string): Promise { +export async function importPluginModule(path: string, version?: string): Promise { + if (version) { + registerPluginInCache({ path, version }); + } + const builtIn = builtInPlugins[path]; if (builtIn) { // for handling dynamic imports @@ -187,7 +187,7 @@ export async function importPluginModule(path: string): Promise { } export function importDataSourcePlugin(meta: grafanaData.DataSourcePluginMeta): Promise { - return importPluginModule(meta.module).then((pluginExports) => { + return importPluginModule(meta.module, meta.info?.version).then((pluginExports) => { if (pluginExports.plugin) { const dsPlugin = pluginExports.plugin as GenericDataSourcePlugin; dsPlugin.meta = meta; @@ -210,7 +210,7 @@ export function importDataSourcePlugin(meta: grafanaData.DataSourcePluginMeta): } export function importAppPlugin(meta: grafanaData.PluginMeta): Promise { - return importPluginModule(meta.module).then((pluginExports) => { + return importPluginModule(meta.module, meta.info?.version).then((pluginExports) => { const plugin = pluginExports.plugin ? (pluginExports.plugin as grafanaData.AppPlugin) : new grafanaData.AppPlugin(); plugin.init(meta); plugin.meta = meta; @@ -220,7 +220,6 @@ export function importAppPlugin(meta: grafanaData.PluginMeta): Promise;