[7.x] use cache busting for KP bundles (#64414) (#64816)

This commit is contained in:
Spencer 2020-04-29 12:56:48 -07:00 committed by GitHub
parent f06097f679
commit 73ae576158
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 195 additions and 61 deletions

View file

@ -96,9 +96,12 @@ export function uiRenderMixin(kbnServer, server, config) {
? await uiSettings.get('theme:darkMode')
: false;
const buildHash = server.newPlatform.env.packageInfo.buildNum;
const basePath = config.get('server.basePath');
const regularBundlePath = `${basePath}/bundles`;
const dllBundlePath = `${basePath}/built_assets/dlls`;
const regularBundlePath = `${basePath}/${buildHash}/bundles`;
const dllBundlePath = `${basePath}/${buildHash}/built_assets/dlls`;
const dllStyleChunks = DllCompiler.getRawDllConfig().chunks.map(
chunk => `${dllBundlePath}/vendors${chunk}.style.dll.css`
);
@ -108,15 +111,15 @@ export function uiRenderMixin(kbnServer, server, config) {
const styleSheetPaths = [
...(isCore ? [] : dllStyleChunks),
`${basePath}/bundles/kbn-ui-shared-deps/${UiSharedDeps.baseCssDistFilename}`,
`${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.baseCssDistFilename}`,
...(darkMode
? [
`${basePath}/bundles/kbn-ui-shared-deps/${UiSharedDeps.darkCssDistFilename}`,
`${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.darkCssDistFilename}`,
`${basePath}/node_modules/@kbn/ui-framework/dist/kui_dark.css`,
`${regularBundlePath}/dark_theme.style.css`,
]
: [
`${basePath}/bundles/kbn-ui-shared-deps/${UiSharedDeps.lightCssDistFilename}`,
`${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.lightCssDistFilename}`,
`${basePath}/node_modules/@kbn/ui-framework/dist/kui_light.css`,
`${regularBundlePath}/light_theme.style.css`,
]),
@ -131,7 +134,7 @@ export function uiRenderMixin(kbnServer, server, config) {
)
.map(path =>
path.localPath.endsWith('.scss')
? `${basePath}/built_assets/css/${path.publicPath}`
? `${basePath}/${buildHash}/built_assets/css/${path.publicPath}`
: `${basePath}/${path.publicPath}`
)
.reverse(),

View file

@ -32,6 +32,7 @@ import { PUBLIC_PATH_PLACEHOLDER } from '../../public_path_placeholder';
const chance = new Chance();
const outputFixture = resolve(__dirname, './fixtures/output');
const pluginNoPlaceholderFixture = resolve(__dirname, './fixtures/plugin/no_placeholder');
const randomWordsCache = new Set();
const uniqueRandomWord = () => {
@ -58,6 +59,9 @@ describe('optimizer/bundle route', () => {
dllBundlesPath = outputFixture,
basePublicPath = '',
builtCssPath = outputFixture,
npUiPluginPublicDirs = [],
buildHash = '1234',
isDist = false,
} = options;
const server = new Hapi.Server();
@ -69,6 +73,9 @@ describe('optimizer/bundle route', () => {
dllBundlesPath,
basePublicPath,
builtCssPath,
npUiPluginPublicDirs,
buildHash,
isDist,
})
);
@ -158,7 +165,7 @@ describe('optimizer/bundle route', () => {
it('responds with exact file data', async () => {
const server = createServer();
const response = await server.inject({
url: '/bundles/image.png',
url: '/1234/bundles/image.png',
});
expect(response.statusCode).to.be(200);
@ -173,7 +180,7 @@ describe('optimizer/bundle route', () => {
it('responds with no content-length and exact file data', async () => {
const server = createServer();
const response = await server.inject({
url: '/bundles/no_placeholder.js',
url: '/1234/bundles/no_placeholder.js',
});
expect(response.statusCode).to.be(200);
@ -187,12 +194,12 @@ describe('optimizer/bundle route', () => {
});
describe('js file with placeholder', () => {
it('responds with no content-length and modified file data', async () => {
it('responds with no content-length and modifiedfile data ', async () => {
const basePublicPath = `/${uniqueRandomWord()}`;
const server = createServer({ basePublicPath });
const response = await server.inject({
url: '/bundles/with_placeholder.js',
url: '/1234/bundles/with_placeholder.js',
});
expect(response.statusCode).to.be(200);
@ -204,7 +211,7 @@ describe('optimizer/bundle route', () => {
);
expect(response.result.indexOf(source)).to.be(-1);
expect(response.result).to.be(
replaceAll(source, PUBLIC_PATH_PLACEHOLDER, `${basePublicPath}/bundles/`)
replaceAll(source, PUBLIC_PATH_PLACEHOLDER, `${basePublicPath}/1234/bundles/`)
);
});
});
@ -213,7 +220,7 @@ describe('optimizer/bundle route', () => {
it('responds with no content-length and exact file data', async () => {
const server = createServer();
const response = await server.inject({
url: '/bundles/no_placeholder.css',
url: '/1234/bundles/no_placeholder.css',
});
expect(response.statusCode).to.be(200);
@ -231,7 +238,7 @@ describe('optimizer/bundle route', () => {
const server = createServer({ basePublicPath });
const response = await server.inject({
url: '/bundles/with_placeholder.css',
url: '/1234/bundles/with_placeholder.css',
});
expect(response.statusCode).to.be(200);
@ -240,7 +247,7 @@ describe('optimizer/bundle route', () => {
expect(response.headers).to.have.property('content-type', 'text/css; charset=utf-8');
expect(response.result.indexOf(source)).to.be(-1);
expect(response.result).to.be(
replaceAll(source, PUBLIC_PATH_PLACEHOLDER, `${basePublicPath}/bundles/`)
replaceAll(source, PUBLIC_PATH_PLACEHOLDER, `${basePublicPath}/1234/bundles/`)
);
});
});
@ -250,7 +257,7 @@ describe('optimizer/bundle route', () => {
const server = createServer();
const response = await server.inject({
url: '/bundles/../outside_output.js',
url: '/1234/bundles/../outside_output.js',
});
expect(response.statusCode).to.be(404);
@ -267,7 +274,7 @@ describe('optimizer/bundle route', () => {
const server = createServer();
const response = await server.inject({
url: '/bundles/non_existent.js',
url: '/1234/bundles/non_existent.js',
});
expect(response.statusCode).to.be(404);
@ -286,7 +293,7 @@ describe('optimizer/bundle route', () => {
});
const response = await server.inject({
url: '/bundles/with_placeholder.js',
url: '/1234/bundles/with_placeholder.js',
});
expect(response.statusCode).to.be(404);
@ -306,7 +313,7 @@ describe('optimizer/bundle route', () => {
sinon.assert.notCalled(createHash);
const resp1 = await server.inject({
url: '/bundles/no_placeholder.js',
url: '/1234/bundles/no_placeholder.js',
});
sinon.assert.calledOnce(createHash);
@ -314,23 +321,23 @@ describe('optimizer/bundle route', () => {
expect(resp1.statusCode).to.be(200);
const resp2 = await server.inject({
url: '/bundles/no_placeholder.js',
url: '/1234/bundles/no_placeholder.js',
});
sinon.assert.notCalled(createHash);
expect(resp2.statusCode).to.be(200);
});
it('is unique per basePublicPath although content is the same', async () => {
it('is unique per basePublicPath although content is the same (by default)', async () => {
const basePublicPath1 = `/${uniqueRandomWord()}`;
const basePublicPath2 = `/${uniqueRandomWord()}`;
const [resp1, resp2] = await Promise.all([
createServer({ basePublicPath: basePublicPath1 }).inject({
url: '/bundles/no_placeholder.js',
url: '/1234/bundles/no_placeholder.js',
}),
createServer({ basePublicPath: basePublicPath2 }).inject({
url: '/bundles/no_placeholder.js',
url: '/1234/bundles/no_placeholder.js',
}),
]);
@ -349,13 +356,13 @@ describe('optimizer/bundle route', () => {
it('responds with 304 when etag and last modified are sent back', async () => {
const server = createServer();
const resp = await server.inject({
url: '/bundles/with_placeholder.js',
url: '/1234/bundles/with_placeholder.js',
});
expect(resp.statusCode).to.be(200);
const resp2 = await server.inject({
url: '/bundles/with_placeholder.js',
url: '/1234/bundles/with_placeholder.js',
headers: {
'if-modified-since': resp.headers['last-modified'],
'if-none-match': resp.headers.etag,
@ -366,4 +373,80 @@ describe('optimizer/bundle route', () => {
expect(resp2.result).to.have.length(0);
});
});
describe('kibana platform assets', () => {
describe('caching', () => {
describe('for non-distributable mode', () => {
it('uses "etag" header to invalidate cache', async () => {
const basePublicPath = `/${uniqueRandomWord()}`;
const npUiPluginPublicDirs = [
{
id: 'no_placeholder',
path: pluginNoPlaceholderFixture,
},
];
const responce = await createServer({ basePublicPath, npUiPluginPublicDirs }).inject({
url: '/1234/bundles/plugin/no_placeholder/no_placeholder.plugin.js',
});
expect(responce.statusCode).to.be(200);
expect(responce.headers.etag).to.be.a('string');
expect(responce.headers['cache-control']).to.be('must-revalidate');
});
it('creates the same "etag" header for the same content with the same basePath', async () => {
const npUiPluginPublicDirs = [
{
id: 'no_placeholder',
path: pluginNoPlaceholderFixture,
},
];
const [resp1, resp2] = await Promise.all([
createServer({ basePublicPath: '', npUiPluginPublicDirs }).inject({
url: '/1234/bundles/plugin/no_placeholder/no_placeholder.plugin.js',
}),
createServer({ basePublicPath: '', npUiPluginPublicDirs }).inject({
url: '/1234/bundles/plugin/no_placeholder/no_placeholder.plugin.js',
}),
]);
expect(resp1.statusCode).to.be(200);
expect(resp2.statusCode).to.be(200);
expect(resp1.rawPayload).to.eql(resp2.rawPayload);
expect(resp1.headers.etag).to.be.a('string');
expect(resp2.headers.etag).to.be.a('string');
expect(resp1.headers.etag).to.eql(resp2.headers.etag);
});
});
describe('for distributable mode', () => {
it('commands to cache assets for each release for a year', async () => {
const basePublicPath = `/${uniqueRandomWord()}`;
const npUiPluginPublicDirs = [
{
id: 'no_placeholder',
path: pluginNoPlaceholderFixture,
},
];
const responce = await createServer({
basePublicPath,
npUiPluginPublicDirs,
isDist: true,
}).inject({
url: '/1234/bundles/plugin/no_placeholder/no_placeholder.plugin.js',
});
expect(responce.statusCode).to.be(200);
expect(responce.headers.etag).to.be(undefined);
expect(responce.headers['cache-control']).to.be('max-age=31536000');
});
});
});
});
});

View file

@ -47,12 +47,16 @@ export function createBundlesRoute({
basePublicPath,
builtCssPath,
npUiPluginPublicDirs = [],
buildHash,
isDist = false,
}: {
regularBundlesPath: string;
dllBundlesPath: string;
basePublicPath: string;
builtCssPath: string;
npUiPluginPublicDirs?: NpUiPluginPublicDirs;
buildHash: string;
isDist?: boolean;
}) {
// rather than calculate the fileHash on every request, we
// provide a cache object to `resolveDynamicAssetResponse()` that
@ -82,45 +86,51 @@ export function createBundlesRoute({
return [
buildRouteForBundles({
publicPath: `${basePublicPath}/bundles/kbn-ui-shared-deps/`,
routePath: '/bundles/kbn-ui-shared-deps/',
publicPath: `${basePublicPath}/${buildHash}/bundles/kbn-ui-shared-deps/`,
routePath: `/${buildHash}/bundles/kbn-ui-shared-deps/`,
bundlesPath: UiSharedDeps.distDir,
fileHashCache,
replacePublicPath: false,
isDist,
}),
...npUiPluginPublicDirs.map(({ id, path }) =>
buildRouteForBundles({
publicPath: `${basePublicPath}/bundles/plugin/${id}/`,
routePath: `/bundles/plugin/${id}/`,
publicPath: `${basePublicPath}/${buildHash}/bundles/plugin/${id}/`,
routePath: `/${buildHash}/bundles/plugin/${id}/`,
bundlesPath: path,
fileHashCache,
replacePublicPath: false,
isDist,
})
),
buildRouteForBundles({
publicPath: `${basePublicPath}/bundles/core/`,
routePath: `/bundles/core/`,
publicPath: `${basePublicPath}/${buildHash}/bundles/core/`,
routePath: `/${buildHash}/bundles/core/`,
bundlesPath: fromRoot(join('src', 'core', 'target', 'public')),
fileHashCache,
replacePublicPath: false,
isDist,
}),
buildRouteForBundles({
publicPath: `${basePublicPath}/bundles/`,
routePath: '/bundles/',
publicPath: `${basePublicPath}/${buildHash}/bundles/`,
routePath: `/${buildHash}/bundles/`,
bundlesPath: regularBundlesPath,
fileHashCache,
isDist,
}),
buildRouteForBundles({
publicPath: `${basePublicPath}/built_assets/dlls/`,
routePath: '/built_assets/dlls/',
publicPath: `${basePublicPath}/${buildHash}/built_assets/dlls/`,
routePath: `/${buildHash}/built_assets/dlls/`,
bundlesPath: dllBundlesPath,
fileHashCache,
isDist,
}),
buildRouteForBundles({
publicPath: `${basePublicPath}/`,
routePath: '/built_assets/css/',
routePath: `/${buildHash}/built_assets/css/`,
bundlesPath: builtCssPath,
fileHashCache,
isDist,
}),
];
}
@ -131,12 +141,14 @@ function buildRouteForBundles({
bundlesPath,
fileHashCache,
replacePublicPath = true,
isDist,
}: {
publicPath: string;
routePath: string;
bundlesPath: string;
fileHashCache: FileHashCache;
replacePublicPath?: boolean;
isDist: boolean;
}) {
return {
method: 'GET',
@ -159,6 +171,7 @@ function buildRouteForBundles({
fileHashCache,
publicPath,
replacePublicPath,
isDist,
});
},
},

View file

@ -17,8 +17,8 @@
* under the License.
*/
import { resolve } from 'path';
import Fs from 'fs';
import { resolve } from 'path';
import { promisify } from 'util';
import Boom from 'boom';
@ -26,8 +26,13 @@ import Hapi from 'hapi';
import { FileHashCache } from './file_hash_cache';
import { getFileHash } from './file_hash';
// @ts-ignore
import { replacePlaceholder } from '../public_path_placeholder';
const MINUTE = 60;
const HOUR = 60 * MINUTE;
const DAY = 24 * HOUR;
const asyncOpen = promisify(Fs.open);
const asyncClose = promisify(Fs.close);
const asyncFstat = promisify(Fs.fstat);
@ -58,6 +63,7 @@ export async function createDynamicAssetResponse({
publicPath,
fileHashCache,
replacePublicPath,
isDist,
}: {
request: Hapi.Request;
h: Hapi.ResponseToolkit;
@ -65,6 +71,7 @@ export async function createDynamicAssetResponse({
publicPath: string;
fileHashCache: FileHashCache;
replacePublicPath: boolean;
isDist: boolean;
}) {
let fd: number | undefined;
@ -82,7 +89,7 @@ export async function createDynamicAssetResponse({
fd = await asyncOpen(path, 'r');
const stat = await asyncFstat(fd);
const hash = await getFileHash(fileHashCache, path, stat, fd);
const hash = isDist ? undefined : await getFileHash(fileHashCache, path, stat, fd);
const read = Fs.createReadStream(null as any, {
fd,
@ -92,15 +99,21 @@ export async function createDynamicAssetResponse({
fd = undefined; // read stream is now responsible for fd
const content = replacePublicPath ? replacePlaceholder(read, publicPath) : read;
const etag = replacePublicPath ? `${hash}-${publicPath}` : hash;
return h
const response = h
.response(content)
.takeover()
.code(200)
.etag(etag)
.header('cache-control', 'must-revalidate')
.type(request.server.mime.path(path).type);
if (isDist) {
response.header('cache-control', `max-age=${365 * DAY}`);
} else {
response.etag(`${hash}-${publicPath}`);
response.header('cache-control', 'must-revalidate');
}
return response;
} catch (error) {
if (fd) {
try {

View file

@ -17,11 +17,19 @@
* under the License.
*/
export function createProxyBundlesRoute({ host, port }: { host: string; port: number }) {
export function createProxyBundlesRoute({
host,
port,
buildHash,
}: {
host: string;
port: number;
buildHash: string;
}) {
return [
buildProxyRouteForBundles('/bundles/', host, port),
buildProxyRouteForBundles('/built_assets/dlls/', host, port),
buildProxyRouteForBundles('/built_assets/css/', host, port),
buildProxyRouteForBundles(`/${buildHash}/bundles/`, host, port),
buildProxyRouteForBundles(`/${buildHash}/built_assets/dlls/`, host, port),
buildProxyRouteForBundles(`/${buildHash}/built_assets/css/`, host, port),
];
}

View file

@ -16,7 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
import KbnServer from '../legacy/server/kbn_server';
export type NpUiPluginPublicDirs = Array<{

View file

@ -57,6 +57,8 @@ export const optimizeMixin = async (
basePublicPath: config.get('server.basePath'),
builtCssPath: fromRoot('built_assets/css'),
npUiPluginPublicDirs: getNpUiPluginPublicDirs(kbnServer),
buildHash: kbnServer.newPlatform.env.packageInfo.buildNum.toString(),
isDist: kbnServer.newPlatform.env.packageInfo.dist,
})
);

View file

@ -49,7 +49,8 @@ export default async (kbnServer, kibanaHapiServer, config) => {
config.get('optimize.watchPort'),
config.get('server.basePath'),
watchOptimizer,
getNpUiPluginPublicDirs(kbnServer)
getNpUiPluginPublicDirs(kbnServer),
kbnServer.newPlatform.env.packageInfo.buildNum.toString()
);
watchOptimizer.status$.subscribe({

View file

@ -26,6 +26,7 @@ export default (kbnServer, server, config) => {
createProxyBundlesRoute({
host: config.get('optimize.watchHost'),
port: config.get('optimize.watchPort'),
buildHash: kbnServer.newPlatform.env.packageInfo.buildNum.toString(),
})
);

View file

@ -106,7 +106,7 @@ export default class WatchOptimizer extends BaseOptimizer {
});
}
bindToServer(server, basePath, npUiPluginPublicDirs) {
bindToServer(server, basePath, npUiPluginPublicDirs, buildHash) {
// pause all requests received while the compiler is running
// and continue once an outcome is reached (aborting the request
// with an error if it was a failure).
@ -118,6 +118,7 @@ export default class WatchOptimizer extends BaseOptimizer {
server.route(
createBundlesRoute({
npUiPluginPublicDirs: npUiPluginPublicDirs,
buildHash,
regularBundlesPath: this.compiler.outputPath,
dllBundlesPath: DllCompiler.getRawDllConfig().outputPath,
basePublicPath: basePath,

View file

@ -21,10 +21,11 @@ import { Server } from 'hapi';
import { registerHapiPlugins } from '../../legacy/server/http/register_hapi_plugins';
export default class WatchServer {
constructor(host, port, basePath, optimizer, npUiPluginPublicDirs) {
constructor(host, port, basePath, optimizer, npUiPluginPublicDirs, buildHash) {
this.basePath = basePath;
this.optimizer = optimizer;
this.npUiPluginPublicDirs = npUiPluginPublicDirs;
this.buildHash = buildHash;
this.server = new Server({
host: host,
port: port,
@ -35,7 +36,12 @@ export default class WatchServer {
async init() {
await this.optimizer.init();
this.optimizer.bindToServer(this.server, this.basePath, this.npUiPluginPublicDirs);
this.optimizer.bindToServer(
this.server,
this.basePath,
this.npUiPluginPublicDirs,
this.buildHash
);
await this.server.start();
}
}

View file

@ -25,6 +25,7 @@ import { DllCompiler } from '../../src/optimize/dynamic_dll_plugin';
const TOTAL_CI_SHARDS = 4;
const ROOT = dirname(require.resolve('../../package.json'));
const buildHash = String(Number.MAX_SAFE_INTEGER);
module.exports = function(grunt) {
function pickBrowser() {
@ -57,27 +58,30 @@ module.exports = function(grunt) {
'http://localhost:5610/test_bundle/karma/globals.js',
...UiSharedDeps.jsDepFilenames.map(
chunkFilename => `http://localhost:5610/bundles/kbn-ui-shared-deps/${chunkFilename}`
chunkFilename =>
`http://localhost:5610/${buildHash}/bundles/kbn-ui-shared-deps/${chunkFilename}`
),
`http://localhost:5610/bundles/kbn-ui-shared-deps/${UiSharedDeps.jsFilename}`,
`http://localhost:5610/${buildHash}/bundles/kbn-ui-shared-deps/${UiSharedDeps.jsFilename}`,
'http://localhost:5610/built_assets/dlls/vendors_runtime.bundle.dll.js',
`http://localhost:5610/${buildHash}/built_assets/dlls/vendors_runtime.bundle.dll.js`,
...DllCompiler.getRawDllConfig().chunks.map(
chunk => `http://localhost:5610/built_assets/dlls/vendors${chunk}.bundle.dll.js`
chunk =>
`http://localhost:5610/${buildHash}/built_assets/dlls/vendors${chunk}.bundle.dll.js`
),
shardNum === undefined
? `http://localhost:5610/bundles/tests.bundle.js`
: `http://localhost:5610/bundles/tests.bundle.js?shards=${TOTAL_CI_SHARDS}&shard_num=${shardNum}`,
? `http://localhost:5610/${buildHash}/bundles/tests.bundle.js`
: `http://localhost:5610/${buildHash}/bundles/tests.bundle.js?shards=${TOTAL_CI_SHARDS}&shard_num=${shardNum}`,
`http://localhost:5610/bundles/kbn-ui-shared-deps/${UiSharedDeps.baseCssDistFilename}`,
`http://localhost:5610/${buildHash}/bundles/kbn-ui-shared-deps/${UiSharedDeps.baseCssDistFilename}`,
// this causes tilemap tests to fail, probably because the eui styles haven't been
// included in the karma harness a long some time, if ever
// `http://localhost:5610/bundles/kbn-ui-shared-deps/${UiSharedDeps.lightCssDistFilename}`,
...DllCompiler.getRawDllConfig().chunks.map(
chunk => `http://localhost:5610/built_assets/dlls/vendors${chunk}.style.dll.css`
chunk =>
`http://localhost:5610/${buildHash}/built_assets/dlls/vendors${chunk}.style.dll.css`
),
'http://localhost:5610/bundles/tests.style.css',
`http://localhost:5610/${buildHash}/bundles/tests.style.css`,
];
}
@ -127,9 +131,9 @@ module.exports = function(grunt) {
proxies: {
'/tests/': 'http://localhost:5610/tests/',
'/bundles/': 'http://localhost:5610/bundles/',
'/built_assets/dlls/': 'http://localhost:5610/built_assets/dlls/',
'/test_bundle/': 'http://localhost:5610/test_bundle/',
[`/${buildHash}/bundles/`]: `http://localhost:5610/${buildHash}/bundles/`,
[`/${buildHash}/built_assets/dlls/`]: `http://localhost:5610/${buildHash}/built_assets/dlls/`,
},
client: {