import { configMap } from './config_map.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' import { Gauge } from 'prom-client' import { getViteManifests } from './manifests.js' import { warmCache } from './files.js' 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 = versionInfo.version this.set({ version }, Object.keys(await getViteManifests({ version })).length) }, labelNames: ['version'] }) const versionUpdateGauge = new Gauge({ name: 'version_update_event', help: 'Timestamp of a version update event', labelNames: ['version'] }) /** * 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() return version } catch (err) { logger.warn(`[Version] UI container at ${baseUrl} does not have meta.json. Fall back to version hash based on manifest.`) } const response = await fetch(new URL('manifest.json', baseUrl), { cache: 'no-store' }) if (!response.ok) throw new Error(`Cannot fetch manifest.json from ${baseUrl}`) const manifest = await response.json() return hash(manifest) })) 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 } } /** * 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 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('[Version] Error in getVersionInfo', err) } } await configMap.load() const fetchedVersionInfo = await fetchVersionInfo() logger.info(`[Version] Fetched initial version: '${fetchedVersionInfo.version}' - [${JSON.stringify(fetchedVersionInfo.details)}]`) Object.assign(versionInfo, fetchedVersionInfo) const stringifiedVersionInfo = JSON.stringify(versionInfo) redis.pubClient.publish(getRedisKey({ name: 'updateVersionInfo' }), stringifiedVersionInfo) await redis.client.set(getRedisKey({ name: 'versionInfo' }), stringifiedVersionInfo) 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) { const key = getRedisKey({ name: 'updateVersionInfo' }) client.subscribe(key, (errs, count) => logger.info(`[Redis] Subscribed to ${key}.`)) client.on('message', async (channel, stringifiedVersionInfo) => { if (channel !== key) return 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: updatedVersionInfo.version }) cache.clear() warmCache({ version: updatedVersionInfo.version }).catch(err => logger.error(err)) Object.assign(versionInfo, updatedVersionInfo) }) } export const updateVersionProcessor = asyncThrottle(async function updateVersionProcessor ({ immediate = false } = {}) { try { logger.info('[Version] Check for new version') await configMap.load() const [storedVersion, fetchedVersionInfo] = await Promise.all([ getLatestVersion(), fetchVersionInfo() ]) // don't wait for the data, can be done in background getViteManifests({ version: fetchedVersionInfo.version }).then(manifests => { manifestFileEntriesGauge.set({ version: fetchedVersionInfo.version }, Object.keys(manifests).length) }) if (storedVersion === fetchedVersionInfo.version) { logger.info(`[Version] No new version has been found. No update needed. Current version: ${storedVersion}`) return storedVersion } logger.info(`[Version] Found new source version. Current version: '${storedVersion}', new version: '${fetchedVersionInfo.version}'`) 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 === fetchedVersionInfo.version || immediate) { logger.info('[Version] publish update to other nodes.') // 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 ${fetchedVersionInfo.version} as previous version.`) await redis.client.set(getRedisKey({ name: 'prevProcessedVersion' }), fetchedVersionInfo.version) } return versionInfo.version } catch (err) { logger.error(`[Version] comparing version is not possible. Error: ${err.message}`) } })