From 4d22e4aee70a425d7b7c318a677011bde7fa6aad Mon Sep 17 00:00:00 2001 From: Richard Petersen <richard.petersen@open-xchange.com> Date: Wed, 30 Nov 2022 17:20:01 +0100 Subject: [PATCH] add: check for version-mismatches when ui-containers provide a version header --- integration/config_test.js | 6 +- integration/update-version_test.js | 10 +-- performance-tests/all-files.js | 9 +- spec/file_caching_test.js | 4 +- spec/server_test.js | 4 +- spec/util.js | 4 + spec/util_test.js | 43 ++++++++- spec/version_mismatches_test.js | 74 ++++++++++++++++ src/errors.js | 22 +++++ src/files.js | 32 +++++-- src/plugins/serve-files.js | 6 +- src/util.js | 21 +++++ src/version.js | 135 +++++++++++++++++++---------- 13 files changed, 298 insertions(+), 72 deletions(-) create mode 100644 spec/version_mismatches_test.js diff --git a/integration/config_test.js b/integration/config_test.js index 5d807de..eb62a09 100644 --- a/integration/config_test.js +++ b/integration/config_test.js @@ -1,6 +1,6 @@ import request from 'supertest' import { expect } from 'chai' -import { generateSimpleViteManifest, mockApp, mockConfig, mockFetch } from '../spec/util.js' +import { generateSimpleViteManifest, mockApp, mockConfig, mockFetch, wait } from '../spec/util.js' import * as td from 'testdouble' import { getRedisKey } from '../src/util.js' @@ -44,8 +44,8 @@ describe('Configuration', function () { config.baseUrls = [] const { pubClient } = await import('../src/redis.js') - pubClient.publish(getRedisKey({ name: 'updateLatestVersion' }), '1234') - await new Promise(resolve => setTimeout(resolve, 200)) + pubClient.publish(getRedisKey({ name: 'updateVersionInfo' }), JSON.stringify({ version: '1234' })) + await wait(200) const response2 = await request(app.server).get('/meta') expect(response2.body).to.have.length(1) diff --git a/integration/update-version_test.js b/integration/update-version_test.js index 0d40a75..e1b9d20 100644 --- a/integration/update-version_test.js +++ b/integration/update-version_test.js @@ -1,6 +1,6 @@ import request from 'supertest' import { expect } from 'chai' -import { generateSimpleViteManifest, mockApp, mockConfig, mockFetch } from '../spec/util.js' +import { generateSimpleViteManifest, mockApp, mockConfig, mockFetch, wait } from '../spec/util.js' import * as td from 'testdouble' import { getRedisKey } from '../src/util.js' @@ -69,7 +69,7 @@ describe('Updates the version', function () { // wait for the update event to happen await new Promise(resolve => { - const key = getRedisKey({ name: 'updateLatestVersion' }) + const key = getRedisKey({ name: 'updateVersionInfo' }) subClient.subscribe(key) subClient.on('message', async (channel, version) => { if (channel !== key) return @@ -89,8 +89,8 @@ describe('Updates the version', function () { const { pubClient } = await import('../src/redis.js') // just publish event, don't change the value on redis. - pubClient.publish(getRedisKey({ name: 'updateLatestVersion' }), '1234') - await new Promise(resolve => setTimeout(resolve, 10)) + pubClient.publish(getRedisKey({ name: 'updateVersionInfo' }), JSON.stringify({ version: '1234' })) + await wait(10) const responseAfterUpdate = await request(app.server).get('/index.html') expect(responseAfterUpdate.statusCode).to.equal(200) @@ -119,7 +119,7 @@ describe('Updates the version', function () { const { client } = await import('../src/redis.js') await client.flushdb() // preconfigure redis - await client.set(getRedisKey({ name: 'latestVersion' }), '12345') + await client.set(getRedisKey({ name: 'versionInfo' }), JSON.stringify({ version: '12345' })) app = await mockApp() }) diff --git a/performance-tests/all-files.js b/performance-tests/all-files.js index bf0104c..0547a34 100644 --- a/performance-tests/all-files.js +++ b/performance-tests/all-files.js @@ -3,6 +3,7 @@ import { Worker } from 'node:worker_threads' import { config } from 'dotenv-defaults' import autocannon from 'autocannon' +import { wait } from '../spec/util' config() @@ -40,7 +41,7 @@ const manifests = await result.json() // 5.1 setup autocannon with cold cache console.log('Setup finished, start autocannon...') -await new Promise(resolve => setTimeout(resolve, 50)) +await wait(50) const coldCacheResult = await autocannon({ url: uiMWPath, connections: 1, @@ -52,13 +53,13 @@ const coldCacheResult = await autocannon({ }) // 6.1 handle result -await new Promise(resolve => setTimeout(resolve, 50)) +await wait(50) console.log('Autocannon results with cold cache:') console.log(autocannon.printResult(coldCacheResult)) // 5.2 setup autocannon options with all files console.log('Setup finished, start autocannon with warm cache...') -await new Promise(resolve => setTimeout(resolve, 50)) +await wait(50) const warmCacheResult = await autocannon({ url: uiMWPath, connections: 1, @@ -70,7 +71,7 @@ const warmCacheResult = await autocannon({ }) // 6.2 handle result -await new Promise(resolve => setTimeout(resolve, 50)) +await wait(50) console.log('Autocannon results with warm cache:') console.log(autocannon.printResult(warmCacheResult)) diff --git a/spec/file_caching_test.js b/spec/file_caching_test.js index cd6f198..1cf64bd 100644 --- a/spec/file_caching_test.js +++ b/spec/file_caching_test.js @@ -1,5 +1,5 @@ import request from 'supertest' -import { brotliParser, generateSimpleViteManifest, mockApp, mockConfig, mockFetch, mockRedis } from './util.js' +import { brotliParser, generateSimpleViteManifest, mockApp, mockConfig, mockFetch, mockRedis, wait } from './util.js' import fs from 'fs' import { expect } from 'chai' import * as td from 'testdouble' @@ -340,7 +340,7 @@ describe('File caching service', function () { mockFetch({ 'http://ui-server': { '/manifest.json': spy = sandbox.spy(async () => { - await new Promise(resolve => setTimeout(resolve, 10)) + await wait(10) return new Response(JSON.stringify(generateSimpleViteManifest({})), { headers: { 'content-type': 'application/json' } }) }), '/example.js': () => new Response('this is example', { headers: { 'content-type': 'application/javascript' } }) diff --git a/spec/server_test.js b/spec/server_test.js index 6c725c9..0e01528 100644 --- a/spec/server_test.js +++ b/spec/server_test.js @@ -1,5 +1,5 @@ import request from 'supertest' -import { brotliParser, generateSimpleViteManifest, mockApp, mockConfig, mockFetch, mockRedis } from './util.js' +import { brotliParser, generateSimpleViteManifest, mockApp, mockConfig, mockFetch, mockRedis, wait } from './util.js' import { expect } from 'chai' import * as td from 'testdouble' import RedisMock from 'ioredis-mock' @@ -42,7 +42,7 @@ describe('UI Middleware', function () { '/example.js': '' } - await new Promise(resolve => setTimeout(resolve, 150)) + await wait(150) const response2 = await request(app.server).get('/manifests').parse(brotliParser) expect(response2.statusCode).to.equal(200) diff --git a/spec/util.js b/spec/util.js index a052a3c..c29bab7 100644 --- a/spec/util.js +++ b/spec/util.js @@ -80,3 +80,7 @@ export async function brotliParser (res, cb) { cb(null, result) }) } + +export async function wait (timeout) { + return new Promise(resolve => setTimeout(resolve, timeout)) +} diff --git a/spec/util_test.js b/spec/util_test.js index 228a1e8..3f5f972 100644 --- a/spec/util_test.js +++ b/spec/util_test.js @@ -1,6 +1,9 @@ -import { hash } from '../src/util.js' -import { generateSimpleViteManifest } from './util.js' +import { asyncThrottle, hash } from '../src/util.js' +import { generateSimpleViteManifest, wait } from './util.js' import { expect } from 'chai' +import sinon from 'sinon' + +const sandbox = sinon.createSandbox() describe('Util', function () { describe('hash function', function () { @@ -27,4 +30,40 @@ describe('Util', function () { expect(hash(manifestChanged)).to.equal('2547998666') }) }) + + describe('asyncThrottle function', function () { + let spy + beforeEach(function () { + spy = sandbox.spy(function () { + return new Promise((resolve, reject) => { + // @ts-ignore + spy.resolve = resolve + // @ts-ignore + spy.reject = resolve + }) + }) + }) + + it('works', function () { + const throttled = asyncThrottle(spy) + expect(spy.callCount).to.equal(0) + throttled() + expect(spy.callCount).to.equal(1) + }) + + it('is only called once again on the trailing edge', async function () { + const throttled = asyncThrottle(spy) + expect(spy.callCount).to.equal(0) + throttled() + expect(spy.callCount).to.equal(1) + throttled() + expect(spy.callCount).to.equal(1) + spy.resolve() + await wait(0) + expect(spy.callCount).to.equal(2) + spy.resolve() + await wait(0) + expect(spy.callCount).to.equal(2) + }) + }) }) diff --git a/spec/version_mismatches_test.js b/spec/version_mismatches_test.js new file mode 100644 index 0000000..236f767 --- /dev/null +++ b/spec/version_mismatches_test.js @@ -0,0 +1,74 @@ +import request from 'supertest' +import { generateSimpleViteManifest, mockApp, mockConfig, mockFetch, mockRedis } from './util.js' +import { expect } from 'chai' +import * as td from 'testdouble' +import RedisMock from 'ioredis-mock' + +describe('version mismatches', function () { + let app + + beforeEach(async function () { + mockConfig({ baseUrls: ['http://ui-server/'] }) + mockRedis() + mockFetch({ + 'http://ui-server': { + '/manifest.json': generateSimpleViteManifest({ + 'foo.js': { }, + 'bar.js': { } + }), + '/foo.js': td.when(td.func()(td.matchers.anything(), td.matchers.anything())).thenReturn( + new Response('foo1', { headers: { version: '1' } }), + new Response('foo2', { headers: { version: '2' } }) + ), + '/bar.js': () => new Response('bar', { headers: { version: '2' } }), + '/whatever.js': () => new Response('whatever', { headers: { version: '2' } }), + '/meta.json': td.when(td.func()(td.matchers.anything(), td.matchers.anything())).thenReturn( + new Response(JSON.stringify({ }), { headers: { 'Content-Type': 'application/json', version: '1' } }), + new Response(JSON.stringify({ }), { headers: { 'Content-Type': 'application/json', version: '2' } }) + ) + } + }) + app = await mockApp() + }) + + afterEach(async function () { + await new RedisMock().flushdb() + td.reset() + }) + + it('detects version mismatches when files are fetched', async function () { + // get foo.js with initial version + let response = await request(app.server).get('/foo.js') + expect(response.statusCode).to.equal(200) + expect(response.text).to.equal('foo1') + expect(response.headers.version).to.equal('85101541') + + // get bar.js. This will cause the server to detect the version mismatch + response = await request(app.server).get('/bar.js') + expect(response.statusCode).to.equal(404) + + // get foo.js again. Since the versions should coincide now, the client should receive the new file + response = await request(app.server).get('/foo.js') + expect(response.statusCode).to.equal(200) + expect(response.text).to.equal('foo2') + expect(response.headers.version).to.equal('85102502') + }) + + it('detects version mismatches in files not referenced in manifest.json when files are fetched', async function () { + // get foo.js with initial version + let response = await request(app.server).get('/foo.js') + expect(response.statusCode).to.equal(200) + expect(response.text).to.equal('foo1') + expect(response.headers.version).to.equal('85101541') + + // get bar.js. This will cause the server to detect the version mismatch + response = await request(app.server).get('/whatever.js') + expect(response.statusCode).to.equal(404) + + // get foo.js again. Since the versions should coincide now, the client should receive the new file + response = await request(app.server).get('/foo.js') + expect(response.statusCode).to.equal(200) + expect(response.text).to.equal('foo2') + expect(response.headers.version).to.equal('85102502') + }) +}) diff --git a/src/errors.js b/src/errors.js index fd34e41..b931f0c 100644 --- a/src/errors.js +++ b/src/errors.js @@ -4,3 +4,25 @@ export class NotFoundError extends Error { this.status = options.status } } + +export class VersionMismatchError extends Error {} + +/** + * Returns true, if the error is a VersionMismatchError or if the error is an aggregate error containing a VersionMismatchError + * @param {AggregateError | Error} err + * @return boolean + */ +export function isVersionMismatchError (err) { + const errors = err instanceof AggregateError ? err.errors : [err] + return errors.reduce((memo, error) => memo && error instanceof VersionMismatchError, true) +} + +/** + * Returns true, if the error is a NotFoundError or if the error is an aggregate error containing a NotFoundError + * @param {AggregateError | Error} err + * @return boolean + */ +export function isNotFoundError (err) { + const errors = err instanceof AggregateError ? err.errors : [err] + return errors.reduce((memo, error) => memo && error instanceof NotFoundError, true) +} diff --git a/src/files.js b/src/files.js index 2897905..54362b0 100644 --- a/src/files.js +++ b/src/files.js @@ -3,9 +3,10 @@ import { isJSFile } from './util.js' import { getCSSDependenciesFor, getViteManifests } from './manifests.js' import * as cache from './cache.js' import { logger } from './logger.js' -import { NotFoundError } from './errors.js' +import { isVersionMismatchError, NotFoundError, VersionMismatchError } from './errors.js' import zlib from 'node:zlib' import { promisify } from 'node:util' +import { getVersionInfo, updateVersionProcessor } from './version.js' const gzip = promisify(zlib.gzip) const brotliCompress = promisify(zlib.brotliCompress) @@ -32,7 +33,7 @@ async function createFileBuffer (response, dependencies) { return buffer } -export async function fetchFileWithHeadersFromBaseUrl (path, baseUrl, version) { +export async function fetchFileWithHeadersFromBaseUrl ({ path, baseUrl, version }) { const [response, dependencies] = await Promise.all([ fetch(new URL(path, baseUrl), { cache: 'no-store' }), isJSFile(path) && getCSSDependenciesFor({ file: path.substr(1), version }) @@ -44,6 +45,14 @@ export async function fetchFileWithHeadersFromBaseUrl (path, baseUrl, version) { throw new NotFoundError(`Error fetching file: ${path}`, { status: response.status }) } + if (response.headers.get('version')) { + const requestedVersion = (await getVersionInfo()).details[baseUrl] + const receivedVersion = response.headers.get('version') + if (requestedVersion !== receivedVersion) { + throw new VersionMismatchError(`${path} does not contain the right version. Needs ${requestedVersion} but received ${receivedVersion}`) + } + } + const result = { body: await createFileBuffer(response, dependencies), headers: { @@ -67,21 +76,30 @@ export async function fetchFileWithHeadersFromBaseUrl (path, baseUrl, version) { export async function fetchFileWithHeaders ({ path, version }) { const viteManifests = await getViteManifests({ version }) + const module = viteManifests[path.substr(1)] if (module?.meta?.baseUrl) { + const baseUrl = module?.meta?.baseUrl try { - return fetchFileWithHeadersFromBaseUrl(path, module.meta.baseUrl, version) + return fetchFileWithHeadersFromBaseUrl({ path, baseUrl, version }) } catch (err) { logger.debug(`[Files] File ${path} had a baseUrl but could not be found on that server: ${err}`) + if (err instanceof VersionMismatchError) throw err } } - return Promise.any(configMap.urls.map(baseUrl => fetchFileWithHeadersFromBaseUrl(path, baseUrl, version))) + return Promise.any(configMap.urls.map(baseUrl => fetchFileWithHeadersFromBaseUrl({ path, baseUrl, version }))) } export function getFile ({ version, path }) { return cache.getFile({ name: path, version }, ({ name: path, version }) => { - return fetchFileWithHeaders({ version, path }) + return fetchFileWithHeaders({ version, path }).catch((err) => { + if (!isVersionMismatchError(err)) throw err + logger.warn(`[Files] The file ${path} has been delivered with the wrong version from the UI container.`) + updateVersionProcessor({ immediate: true }) + + throw err + }) }) } @@ -96,6 +114,10 @@ export async function warmCache ({ version }) { try { await getFile({ version, path }) } catch (err) { + if (isVersionMismatchError(err)) { + logger.info(`[File] Cache warming has been canceled because of a version mismatch at "${path}". Canceled after ${Math.floor((+new Date() - start) / 1000)}s`) + return + } logger.info(`[File] could not prefetch file ${path}`) } } diff --git a/src/plugins/serve-files.js b/src/plugins/serve-files.js index df9a17a..5339d1b 100644 --- a/src/plugins/serve-files.js +++ b/src/plugins/serve-files.js @@ -1,5 +1,5 @@ import { getFile } from '../files.js' -import { NotFoundError } from '../errors.js' +import { isNotFoundError, isVersionMismatchError } from '../errors.js' import createError from 'http-errors' export default async function serveFilePlugin (fastify, options) { @@ -14,9 +14,7 @@ export default async function serveFilePlugin (fastify, options) { reply.headers(headers) reply.send(body) } catch (err) { - const errors = err.errors || [err] - const fileNotFound = errors.reduce((memo, error) => memo && error instanceof NotFoundError, true) - if (fileNotFound) throw createError(404, `File "${req.urlData('path')}" does not exist.`) + if (isNotFoundError(err) || isVersionMismatchError(err)) throw createError(404, `File "${req.urlData('path')}" does not exist.`) throw err } }) diff --git a/src/util.js b/src/util.js index 616d8f2..70c7360 100644 --- a/src/util.js +++ b/src/util.js @@ -71,3 +71,24 @@ export function once (fn, context) { return res } } + +export function asyncThrottle (fn, context) { + let next = null + let current = null + + const wrapper = async function () { + const result = await fn.apply(context || this, arguments) + current = null + if (next) current = next() + next = null + return result + } + + return function () { + if (current) { + next = wrapper.bind(this, ...arguments) + return current + } + return (current = wrapper.apply(this || context, arguments)) + } +} diff --git a/src/version.js b/src/version.js index 4103c7d..bfdf8b2 100644 --- a/src/version.js +++ b/src/version.js @@ -1,5 +1,5 @@ import { configMap } from './configMap.js' -import { getRedisKey, hash } from './util.js' +import { asyncThrottle, getRedisKey, hash } from './util.js' import { logger } from './logger.js' import * as cache from './cache.js' import * as redis from './redis.js' @@ -7,13 +7,16 @@ import { Gauge } from 'prom-client' import { getViteManifests } from './manifests.js' import { warmCache } from './files.js' -let latestVersion +const versionInfo = { + version: null, + details: {} +} const manifestFileEntriesGauge = new Gauge({ name: 'manifest_file_entries', help: 'Number of entries in merged vite manifest (number of all known files)', async collect () { - const version = latestVersion + const version = versionInfo.version this.set({ version }, Object.keys(await getViteManifests({ version })).length) }, labelNames: ['version'] @@ -25,11 +28,18 @@ const versionUpdateGauge = new Gauge({ labelNames: ['version'] }) -export const fetchLatestVersion = async () => { - const infos = await Promise.all(configMap.urls.map(async baseUrl => { +/** + * Fetches latest version information from all the ui-containers + * @returns {Promise<{version, details}>} Return a promise containing this information + */ +export async function fetchVersionInfo () { + const versions = await Promise.all(configMap.urls.map(async baseUrl => { try { const response = await fetch(new URL('meta.json', baseUrl), { cache: 'no-store' }) if (!response.ok) throw new Error() + + if (response.headers.get('version')) return response.headers.get('version') + const meta = await response.json() const version = meta.commitSha || meta.buildDate || meta.version if (!version) throw new Error() @@ -42,92 +52,127 @@ export const fetchLatestVersion = async () => { const manifest = await response.json() return hash(manifest) })) - return `${hash(infos)}${configMap.salt ? `-${configMap.salt}` : ''}` + const details = Object.fromEntries(configMap.urls.map((url, i) => [url, versions[i]])) + const version = `${hash(Object.values(details))}${configMap.salt ? `-${configMap.salt}` : ''}` + + return { details, version } } -export async function getLatestVersion () { - if (latestVersion) return latestVersion +/** + * Gets version information from redis or the ui-containers, if not cached. + * @returns {Promise<{version, details}>} Return a promise containing this information + */ +export async function getVersionInfo () { + if (versionInfo.version) return versionInfo if (redis.isEnabled()) { - const version = await redis.client.get(getRedisKey({ name: 'latestVersion' })) - if (version) { - logger.info(`[Version] Got initial version from redis: '${version}'`) - versionUpdateGauge.setToCurrentTime({ version }) - return (latestVersion = version) + const redisVersionInfo = await redis.client.get(getRedisKey({ name: 'versionInfo' })) + if (redisVersionInfo) { + try { + Object.assign(versionInfo, JSON.parse(redisVersionInfo)) + logger.info(`[Version] Got initial version from redis: '${versionInfo.version}'`) + versionUpdateGauge.setToCurrentTime({ version: versionInfo.version }) + return versionInfo + } catch (err) { + logger.error('[') + } } } await configMap.load() - const version = await fetchLatestVersion() - logger.info(`[Version] Fetched initial version: '${version}'`) + const fetchedVersionInfo = await fetchVersionInfo() + logger.info(`[Version] Fetched initial version: '${fetchedVersionInfo.version}' - [${JSON.stringify(fetchedVersionInfo.details)}]`) - latestVersion = version + Object.assign(versionInfo, fetchedVersionInfo) if (redis.isEnabled()) { - redis.pubClient.publish(getRedisKey({ name: 'updateLatestVersion' }), version) - await redis.client.set(getRedisKey({ name: 'latestVersion' }), version) + const stringifiedVersionInfo = JSON.stringify(versionInfo) + redis.pubClient.publish(getRedisKey({ name: 'updateVersionInfo' }), stringifiedVersionInfo) + await redis.client.set(getRedisKey({ name: 'versionInfo' }), stringifiedVersionInfo) } - return version + return versionInfo +} + +/** + * Fetches latest version from all the ui-containers + * @returns {Promise<number>} Return a promise containing this information + */ +export async function fetchLatestVersion () { + const versionInfo = await fetchVersionInfo() + return versionInfo.version +} + +/** + * Gets latest version from redis or the ui-containers, if not cached. + * @returns {Promise<number>} Return a promise containing this information + */ +export async function getLatestVersion () { + const versionInfo = await getVersionInfo() + return versionInfo.version } export function registerLatestVersionListener (client) { if (!redis.isEnabled()) return - const key = getRedisKey({ name: 'updateLatestVersion' }) + const key = getRedisKey({ name: 'updateVersionInfo' }) client.subscribe(key, (errs, count) => logger.info(`[Redis] Subscribed to ${key}.`)) - client.on('message', async (channel, version) => { + client.on('message', async (channel, stringifiedVersionInfo) => { if (channel !== key) return - if (latestVersion === version) return logger.info(`[Version] Received 'updateLatestVersion' event but already contains that version '${version}'`) - logger.info(`[Version] Received 'updateLatestVersion' event. Clearing cache. New version: '${version}'`) + const updatedVersionInfo = JSON.parse(stringifiedVersionInfo) + if (versionInfo.version === updatedVersionInfo.version) return logger.info(`[Version] Received 'updateVersionInfo' event but already contains that version '${updatedVersionInfo.version}'`) + logger.info(`[Version] Received 'updateVersionInfo' event. Clearing cache. New version: '${updatedVersionInfo.version}'`) await configMap.load() - versionUpdateGauge.setToCurrentTime({ version }) + versionUpdateGauge.setToCurrentTime({ version: updatedVersionInfo.version }) cache.clear() - warmCache({ version }).catch(err => logger.error(err)) - latestVersion = version + warmCache({ version: updatedVersionInfo.version }).catch(err => logger.error(err)) + Object.assign(versionInfo, updatedVersionInfo) }) } -export async function updateVersionProcessor () { +export const updateVersionProcessor = asyncThrottle(async function updateVersionProcessor ({ immediate = false } = {}) { try { logger.info('[Version] Check for new version') await configMap.load() - const [storedVersion, fetchedVersion] = await Promise.all([ + const [storedVersion, fetchedVersionInfo] = await Promise.all([ getLatestVersion(), - fetchLatestVersion() + fetchVersionInfo() ]) // don't wait for the data, can be done in background - getViteManifests({ version: fetchedVersion }).then(manifests => { - manifestFileEntriesGauge.set({ version: fetchedVersion }, Object.keys(manifests).length) + getViteManifests({ version: fetchedVersionInfo.version }).then(manifests => { + manifestFileEntriesGauge.set({ version: fetchedVersionInfo.version }, Object.keys(manifests).length) }) - if (storedVersion === fetchedVersion) { + if (storedVersion === fetchedVersionInfo.version) { logger.info(`[Version] No new version has been found. No update needed. Current version: ${storedVersion}`) - return fetchedVersion + return storedVersion } - logger.info(`[Version] Found new source version. Current version: '${storedVersion}', new version: '${fetchedVersion}'`) + logger.info(`[Version] Found new source version. Current version: '${storedVersion}', new version: '${fetchedVersionInfo.version}'`) if (redis.isEnabled()) { const prevProcessedVersion = await redis.client.get(getRedisKey({ name: 'prevProcessedVersion' })) // that means, that between the previous update processing and this one, there was no version change - if (prevProcessedVersion === fetchedVersion) { + if (prevProcessedVersion === fetchedVersionInfo.version || immediate) { logger.info('[Version] publish update to other nodes.') - redis.pubClient.publish(getRedisKey({ name: 'updateLatestVersion' }), fetchedVersion) - await redis.client.set(getRedisKey({ name: 'latestVersion' }), fetchedVersion) - latestVersion = fetchedVersion + // update local version info + Object.assign(versionInfo, fetchedVersionInfo) + const stringifiedVersionInfo = JSON.stringify(versionInfo) + redis.pubClient.publish(getRedisKey({ name: 'updateVersionInfo' }), stringifiedVersionInfo) + await redis.client.set(getRedisKey({ name: 'versionInfo' }), stringifiedVersionInfo) + versionUpdateGauge.setToCurrentTime({ version: versionInfo.version }) } else { - logger.info(`[Version] do not execute update yet. Store version ${fetchedVersion} as previous version.`) - await redis.client.set(getRedisKey({ name: 'prevProcessedVersion' }), fetchedVersion) + logger.info(`[Version] do not execute update yet. Store version ${fetchedVersionInfo.version} as previous version.`) + await redis.client.set(getRedisKey({ name: 'prevProcessedVersion' }), fetchedVersionInfo.version) } } else { - versionUpdateGauge.setToCurrentTime({ version: latestVersion }) + versionUpdateGauge.setToCurrentTime({ version: versionInfo.version }) // if redis is disabled, this will only be trigger by a setInterval and not from a redis event logger.info('[Version] Clear local cache due to version update.') cache.clear() - warmCache({ version: fetchedVersion }).catch(err => logger.error(err)) - latestVersion = fetchedVersion + Object.assign(versionInfo, fetchedVersionInfo) + warmCache({ version: versionInfo.version }).catch(err => logger.error(err)) } - return latestVersion + return versionInfo.version } catch (err) { logger.error(`[Version] comparing version is not possible. Error: ${err.message}`) } -} +}) -- GitLab