From a1495f40cf79b2a59ac8878f27fb8bbabeb9ac41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20B=C3=A4ume?= <julian.baeume@open-xchange.com> Date: Wed, 30 Aug 2023 13:29:34 +0200 Subject: [PATCH] Changed: OXUI-1345: Refactor cache handling Changelog: - introduce two separate roles for middleware deployments: - ui-middleware: read from redis only, listen to version updates, can easily be scaled - updater: split out all code that writes to redis, no need to scale > 1 --- .env.defaults | 1 + .../templates/deployment.yaml | 37 ++ .../core-ui-middleware/templates/updater.yaml | 75 ++++ integration/caching_test.js | 11 +- integration/config_test.js | 24 +- integration/update-version_test.js | 60 ++-- spec/app_root_test.js | 42 +-- spec/file-depencies_test.js | 60 +--- spec/file_caching_test.js | 340 +++--------------- spec/headers_test.js | 120 +------ spec/meta_test.js | 92 ++--- spec/redis_test.js | 63 ---- spec/salt_test.js | 16 +- spec/util.js | 5 +- spec/version_mismatches_test.js | 18 +- src/cache.js | 6 + src/errors.js | 2 +- src/files.js | 32 +- src/index.js | 30 +- src/lightship.js | 4 +- src/redis.js | 67 +--- src/routes/metadata.js | 27 +- src/updater.js | 69 ++++ src/version.js | 92 +++-- 24 files changed, 479 insertions(+), 814 deletions(-) create mode 100644 helm/core-ui-middleware/templates/updater.yaml delete mode 100644 spec/redis_test.js create mode 100644 src/updater.js diff --git a/.env.defaults b/.env.defaults index 0131a28..cf8181f 100644 --- a/.env.defaults +++ b/.env.defaults @@ -2,6 +2,7 @@ NODE_ENV=production CACHE_TTL=30000 PORT=8080 METRICS_PORT=9090 +LIGHTSHIP_PORT=9000 LOG_LEVEL=info APP_ROOT=/ EXPOSE_API_DOCS=false diff --git a/helm/core-ui-middleware/templates/deployment.yaml b/helm/core-ui-middleware/templates/deployment.yaml index c9d328a..422818d 100644 --- a/helm/core-ui-middleware/templates/deployment.yaml +++ b/helm/core-ui-middleware/templates/deployment.yaml @@ -86,6 +86,43 @@ spec: memory: 96Mi requests: memory: 96Mi + - name: updater + securityContext: {{ toYaml .Values.securityContext | nindent 12 }} + image: {{ include "ox-common.images.image" (dict "imageRoot" .Values.image "global" $ "context" . ) }} + command: ["/nodejs/bin/node", "src/updater.js"] + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: CACHE_TTL + value: "{{ .Values.cacheTTL | int }}" + - name: LOG_LEVEL + value: "{{ .Values.logLevel }}" + - name: APP_ROOT + value: "{{ include "ox-common.appsuite.appRoot" . }}/" + - name: COMPRESS_FILE_SIZE + value: "{{ .Values.compressFileSize }}" + - name: COMPRESS_FILE_TYPES + value: "{{ .Values.compressFileTypes }}" + - name: SLOW_REQUEST_THRESHOLD + value: "{{ .Values.slowRequestThreshold }}" + - name: METRICS_PORT + value: "9091" + - name: LIGHTSHIP_PORT + value: "9001" + - name: PORT + value: "8081" + - name: REDIS_MODE + value: "{{ .Values.redis.mode }}" + - name: REDIS_HOSTS + value: "{{ .Values.redis.hosts | join "," }}" + - name: REDIS_DB + value: "{{ .Values.redis.db | int }}" + - name: REDIS_PASSWORD + value: "{{ .Values.redis.password }}" + - name: REDIS_PREFIX + value: "{{ .Values.redis.prefix }}" + volumeMounts: + - name: manifest-config + mountPath: /app/config/ {{- end }} volumes: - name: manifest-config diff --git a/helm/core-ui-middleware/templates/updater.yaml b/helm/core-ui-middleware/templates/updater.yaml new file mode 100644 index 0000000..dab7502 --- /dev/null +++ b/helm/core-ui-middleware/templates/updater.yaml @@ -0,0 +1,75 @@ +{{- if not (and (eq (len .Values.redis.hosts) 1) (eq (index .Values.redis.hosts 0) "localhost:6379")) }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "ox-common.names.fullname" . }}-updater + labels: + {{- include "ox-common.labels.standard" . | nindent 4 }} +spec: + replicas: 1 + selector: + matchLabels: + {{- include "ox-common.labels.matchLabels" . | nindent 6 }} + template: + metadata: + annotations: {{ toYaml .Values.podAnnotations | nindent 8 }} + labels: + {{- include "ox-common.labels.podLabels" . | nindent 8 }} + spec: {{ include "ox-common.pods.podSpec" (dict "podRoot" .Values "global" $ "context" . ) | nindent 6 }} + containers: + - name: main + securityContext: {{ toYaml .Values.securityContext | nindent 12 }} + image: {{ include "ox-common.images.image" (dict "imageRoot" .Values.image "global" $ "context" . ) }} + command: ["/nodejs/bin/node", "src/updater.js"] + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: CACHE_TTL + value: "{{ .Values.cacheTTL | int }}" + - name: LOG_LEVEL + value: "{{ .Values.logLevel }}" + - name: APP_ROOT + value: "{{ include "ox-common.appsuite.appRoot" . }}/" + - name: COMPRESS_FILE_SIZE + value: "{{ .Values.compressFileSize }}" + - name: COMPRESS_FILE_TYPES + value: "{{ .Values.compressFileTypes }}" + - name: SLOW_REQUEST_THRESHOLD + value: "{{ .Values.slowRequestThreshold }}" + - name: REDIS_MODE + value: "{{ .Values.redis.mode }}" + - name: REDIS_HOSTS + value: "{{ .Values.redis.hosts | join "," }}" + - name: REDIS_DB + value: "{{ .Values.redis.db | int }}" + - name: REDIS_PASSWORD + value: "{{ .Values.redis.password }}" + - name: REDIS_PREFIX + value: "{{ .Values.redis.prefix }}" + ports: + - name: tcp-monitoring + containerPort: 9090 + protocol: TCP + {{- if .Values.probe.liveness.enabled }} + livenessProbe: + httpGet: + path: /live + port: 9000 + {{- omit .Values.probe.liveness "enabled" | toYaml | nindent 12 }} + {{- end }} + {{- if .Values.probe.readiness.enabled }} + readinessProbe: + httpGet: + path: /ready + port: 9000 + {{- omit .Values.probe.readiness "enabled" | toYaml | nindent 12 }} + {{- end }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - name: manifest-config + mountPath: /app/config/ + volumes: + - name: manifest-config + configMap: + name: {{ .Values.existingConfigMap | default (include "ox-common.names.fullname" .) }} +{{- end }} diff --git a/integration/caching_test.js b/integration/caching_test.js index 8c88ce9..2ee477b 100644 --- a/integration/caching_test.js +++ b/integration/caching_test.js @@ -27,7 +27,7 @@ import { getRedisKey } from '../src/util.js' import zlib from 'node:zlib' describe('File caching service', function () { - let app + let app, pubClient beforeEach(async function () { await mockConfig({ baseUrls: ['http://ui-server/'] }) @@ -39,7 +39,14 @@ describe('File caching service', function () { '/index.html': () => new Response('<html><head></head><body>it\'s me</body></html>', { headers: { 'content-type': 'text/html' } }) } }) - await import('../src/redis.js').then(({ client }) => client.flushdb()) + await import('../src/redis.js').then(({ client, createClient }) => { + pubClient = createClient() + return client.flushdb() + }) + await import('../src/version.js').then(async ({ updateVersionProcessor }) => { + await updateVersionProcessor(pubClient) + await updateVersionProcessor(pubClient) + }) app = await injectApp() }) diff --git a/integration/config_test.js b/integration/config_test.js index 497bca2..66fb7dc 100644 --- a/integration/config_test.js +++ b/integration/config_test.js @@ -21,13 +21,13 @@ */ import { expect } from 'chai' -import { generateSimpleViteManifest, injectApp, mockConfig, mockFetch, wait } from '../spec/util.js' +import { generateSimpleViteManifest, injectApp, mockConfig, mockFetch } from '../spec/util.js' import * as td from 'testdouble' -import { getRedisKey } from '../src/util.js' describe('Configuration', function () { let app let config + let pubClient beforeEach(async function () { // need to set the redis-prefix. Otherwise, the bull workers will interfere @@ -40,20 +40,23 @@ describe('Configuration', function () { }), '/index.html': () => new Response('<html><head></head><body>it\'s me</body></html>', { headers: { 'content-type': 'text/html' } }), '/meta.json': td.when(td.func()(td.matchers.anything(), td.matchers.anything())).thenReturn( + new Response(JSON.stringify({ commitSha: '1' }), { headers: { 'Content-Type': 'application/json' } }), new Response(JSON.stringify({ commitSha: '1' }), { headers: { 'Content-Type': 'application/json' } }), new Response(JSON.stringify({ commitSha: '2' }), { headers: { 'Content-Type': 'application/json' } }) ) } }) - await import('../src/redis.js').then(({ client }) => client.flushdb()) + await import('../src/redis.js').then(({ client, createClient }) => { + pubClient = createClient() + return client.flushdb() + }) + await import('../src/version.js').then(async ({ updateVersionProcessor }) => { + await updateVersionProcessor(pubClient) + }) app = await injectApp() }) afterEach(async function () { - const { getQueues, closeQueue } = await import('../src/redis.js') - for (const queue of getQueues()) { - await closeQueue(queue.name) - } td.reset() // reset, after the queues were removed process.env.REDIS_PREFIX = 'ui-middleware' @@ -64,9 +67,10 @@ describe('Configuration', function () { expect(response.json()).to.have.length(2) config.baseUrls = [] - const { pubClient } = await import('../src/redis.js') - pubClient.publish(getRedisKey({ name: 'updateVersionInfo' }), JSON.stringify({ version: '1234' })) - await wait(200) + await import('../src/version.js').then(async ({ updateVersionProcessor }) => { + await updateVersionProcessor(pubClient) + await updateVersionProcessor(pubClient) + }) const response2 = await app.inject({ url: '/meta' }) expect(response2.json()).to.have.length(1) diff --git a/integration/update-version_test.js b/integration/update-version_test.js index 06bd36b..949df58 100644 --- a/integration/update-version_test.js +++ b/integration/update-version_test.js @@ -21,12 +21,14 @@ */ import { expect } from 'chai' -import { generateSimpleViteManifest, injectApp, mockConfig, mockFetch, wait } from '../spec/util.js' +import { generateSimpleViteManifest, injectApp, mockConfig, mockFetch } from '../spec/util.js' import * as td from 'testdouble' import { getRedisKey } from '../src/util.js' describe('Updates the version', function () { let app + let pubClient + let runUpdate beforeEach(async function () { // need to set the redis-prefix. Otherwise, the bull workers will interfere @@ -40,20 +42,27 @@ describe('Updates the version', function () { '/index.html': () => new Response('<html><head></head><body>it\'s me</body></html>', { headers: { 'content-type': 'text/html' } }), '/meta.json': td.when(td.func()(td.matchers.anything(), td.matchers.anything())).thenReturn( new Response(JSON.stringify({ commitSha: '1' }), { headers: { 'Content-Type': 'application/json' } }), + new Response(JSON.stringify({ commitSha: '1' }), { headers: { 'Content-Type': 'application/json' } }), + new Response(JSON.stringify({ commitSha: '1' }), { headers: { 'Content-Type': 'application/json' } }), + new Response(JSON.stringify({ commitSha: '2' }), { headers: { 'Content-Type': 'application/json' } }), new Response(JSON.stringify({ commitSha: '2' }), { headers: { 'Content-Type': 'application/json' } }), new Response(JSON.stringify({ commitSha: '2' }), { headers: { 'Content-Type': 'application/json' } }) ) } }) - await import('../src/redis.js').then(({ client }) => client.flushdb()) + await import('../src/redis.js').then(({ client, createClient }) => { + pubClient = createClient('pubClient') + return client.flushdb() + }) + await import('../src/version.js').then(async ({ updateVersionProcessor }) => { + runUpdate = updateVersionProcessor + await runUpdate(pubClient) + return runUpdate(pubClient) + }) app = await injectApp() }) afterEach(async function () { - const { getQueues, closeQueue } = await import('../src/redis.js') - for (const queue of getQueues()) { - await closeQueue(queue.name) - } td.reset() // reset, after the queues were removed process.env.REDIS_PREFIX = 'ui-middleware' @@ -64,30 +73,19 @@ describe('Updates the version', function () { expect(responseBeforeUpdate.statusCode).to.equal(200) expect(responseBeforeUpdate.headers.version).to.equal('85101541') - const { getQueue } = await import('../src/redis.js') - // update has only been registered but not executed yet - expect(await getQueue('update-version').add({}).then(job => job.finished())).to.equal('85101541') - // update is executed with the second iteration - expect(await getQueue('update-version').add({}).then(job => job.finished())).to.equal('85102502') - + await runUpdate(pubClient) + await runUpdate(pubClient) const responseAfterUpdate = await app.inject({ url: '/index.html' }) expect(responseAfterUpdate.statusCode).to.equal(200) expect(responseAfterUpdate.headers.version).to.equal('85102502') }) it('with automatically triggered job', async function () { + const subClient = await import('../src/redis.js').then(({ createClient }) => createClient('subClient')) const responseBeforeUpdate = await app.inject({ url: '/index.html' }) expect(responseBeforeUpdate.statusCode).to.equal(200) expect(responseBeforeUpdate.headers.version).to.equal('85101541') - // speed up the update process - const { subClient, getQueue } = await import('../src/redis.js') - const queue = getQueue('update-version') - queue.add({}, { - jobId: 'update-version-job', - repeat: { every: 100 } - }) - // wait for the update event to happen await new Promise(resolve => { const key = getRedisKey({ name: 'updateVersionInfo' }) @@ -96,6 +94,7 @@ describe('Updates the version', function () { if (channel !== key) return resolve() }) + runUpdate(pubClient).then(() => runUpdate(pubClient)) }) const responseAfterUpdate = await app.inject({ url: '/index.html' }) @@ -103,21 +102,6 @@ describe('Updates the version', function () { expect(responseAfterUpdate.headers.version).to.equal('85102502') }) - it('receives version update via redis event', async function () { - const responseBeforeUpdate = await app.inject({ url: '/index.html' }) - expect(responseBeforeUpdate.statusCode).to.equal(200) - expect(responseBeforeUpdate.headers.version).to.equal('85101541') - - const { pubClient } = await import('../src/redis.js') - // just publish event, don't change the value on redis. - pubClient.publish(getRedisKey({ name: 'updateVersionInfo' }), JSON.stringify({ version: '1234' })) - await wait(10) - - const responseAfterUpdate = await app.inject({ url: '/index.html' }) - expect(responseAfterUpdate.statusCode).to.equal(200) - expect(responseAfterUpdate.headers.version).to.equal('1234') - }) - describe('with initial version', function () { beforeEach(async function () { td.reset() @@ -140,7 +124,11 @@ describe('Updates the version', function () { const { client } = await import('../src/redis.js') await client.flushdb() // preconfigure redis - await client.set(getRedisKey({ name: 'versionInfo' }), JSON.stringify({ version: '12345' })) + await Promise.all([ + client.set(getRedisKey({ name: 'versionInfo' }), JSON.stringify({ version: '12345' })), + client.set(getRedisKey({ version: '12345', name: '/index.html' }) + ':body', '<html><head></head><body>it\'s me</body></html>'), + client.set(getRedisKey({ version: '12345', name: '/index.html' }) + ':meta', JSON.stringify({ headers: { 'content-type': 'text/html', version: '12345' } })) + ]) app = await injectApp() }) diff --git a/spec/app_root_test.js b/spec/app_root_test.js index 2dbeb67..80bd741 100644 --- a/spec/app_root_test.js +++ b/spec/app_root_test.js @@ -20,7 +20,7 @@ * */ -import { generateSimpleViteManifest, injectApp, mockConfig, mockFetch, mockRedis } from './util.js' +import { injectApp, mockRedis, mockFetch, mockConfig } from './util.js' import { expect } from 'chai' import * as td from 'testdouble' import RedisMock from 'ioredis-mock' @@ -29,38 +29,22 @@ describe('With different app root', function () { let app beforeEach(async function () { - let count = 0 await mockConfig({ baseUrls: ['http://ui-server/'] }) - await mockRedis() + const { client } = await mockRedis() mockFetch({ 'http://ui-server': { - '/manifest.json': generateSimpleViteManifest({ - 'example.js': { imports: ['test.txt'] }, - 'test.txt': { }, - 'main.css': {}, - 'index.html': { - file: 'index.html.js', - isEntry: true, - imports: ['example.js'], - css: ['main.css'] - }, - 'image.png': {} - }), - '/example.js': () => new Response('this is example', { headers: { 'content-type': 'application/javascript' } }), - '/test.txt': () => new Response('this is test', { headers: { 'content-type': 'text/plain' } }), - '/index.html.js': () => new Response('this is index.html.js', { headers: { 'content-type': 'application/javascript' } }), - '/index.html': () => new Response('<html><head></head><body>it\'s me</body></html>', { headers: { 'content-type': 'text/html' } }), - '/main.css': () => new Response('.foo { color: #000; }', { headers: { 'content-type': 'text/css' } }), - '/favicon.ico': 'not really a favicon, though', - '/test.svg': () => { - if (count > 0) { - return new Response(null, { status: 404 }) - } - count++ - return new Response('<svg></svg>', { headers: { 'content-type': 'image/svg' } }) - } + '/favicon.ico': 'not really a favicon, though' } }) + await Promise.all([ + client.set('ui-middleware:viteManifests', JSON.stringify({})), + client.set('ui-middleware:/example.js:body', 'this is example'), + client.set('ui-middleware:/example.js:meta', JSON.stringify({ headers: { 'content-type': 'application/javascript' } })), + client.set('ui-middleware:/test.txt:body', 'this is test'), + client.set('ui-middleware:/test.txt:meta', JSON.stringify({ headers: { 'content-type': 'text/plain' } })), + client.set('ui-middleware:/index.html:body', '<html><head></head><body>it\'s me</body></html>'), + client.set('ui-middleware:/index.html:meta', JSON.stringify({ headers: { 'content-type': 'text/html' } })) + ]) app = await injectApp('/appsuite/') }) @@ -73,7 +57,7 @@ describe('With different app root', function () { it('serves files defined in manifest.json file', async function () { const response = await app.inject({ url: '/appsuite/example.js' }) expect(response.statusCode).to.equal(200) - expect(response.headers['content-type']).to.equal('application/javascript; charset=utf-8') + expect(response.headers['content-type']).to.equal('application/javascript') expect(response.body).to.equal('this is example') const response2 = await app.inject({ url: '/appsuite/test.txt' }) diff --git a/spec/file-depencies_test.js b/spec/file-depencies_test.js index 10a0a46..12c0a93 100644 --- a/spec/file-depencies_test.js +++ b/spec/file-depencies_test.js @@ -20,36 +20,26 @@ * */ -import { generateSimpleViteManifest, injectApp, mockConfig, mockFetch, mockRedis } from './util.js' +import { injectApp, mockRedis } from './util.js' import { expect } from 'chai' import * as td from 'testdouble' import RedisMock from 'ioredis-mock' describe('JS files with dependencies contain events', function () { let app - let mockFetchConfig + let client beforeEach(async function () { - await mockConfig({ baseUrls: ['http://ui-server/'] }) - await mockRedis() - mockFetch(mockFetchConfig = { - 'http://ui-server': { - '/manifest.json': generateSimpleViteManifest({ - 'example.js': {}, - 'main.css': {}, - 'index.html': { - file: 'index.html.js', - isEntry: true, - imports: ['example.js'], - css: ['main.css'] - } - }), - '/example.js': () => new Response('this is example', { headers: { 'content-type': 'application/javascript' } }), - '/index.html.js': () => new Response('console.log("this is index.html.js")', { headers: { 'content-type': 'application/javascript' } }), - '/index.html': () => new Response('<html><head></head><body>it\'s me</body></html>', { headers: { 'content-type': 'text/html' } }), - '/main.css': () => new Response('.foo { color: #000; }', { headers: { 'content-type': 'text/css' } }) - } - }) + client = (await mockRedis()).client + client.set('ui-middleware:versionInfo', JSON.stringify({ version: '123456' })) + client.set('ui-middleware:123456:/example.js:body', 'this is example') + client.set('ui-middleware:123456:/example.js:meta', JSON.stringify({ headers: { 'content-type': 'application/javascript' } })) + client.set('ui-middleware:123456:/index.html.js:body', 'console.log("this is index.html.js")') + client.set('ui-middleware:123456:/index.html.js:meta', JSON.stringify({ headers: { 'content-type': 'application/javascript', dependencies: ['main.css'] } })) + client.set('ui-middleware:123456:/index.html:body', '<html><head></head><body>it\'s me</body></html>') + client.set('ui-middleware:123456:/index.html:meta', JSON.stringify({ headers: { 'content-type': 'text/html' } })) + client.set('ui-middleware:123456:/main.css:body', '.foo { color: #000; }') + client.set('ui-middleware:123456:/main.css:meta', JSON.stringify({ headers: { 'content-type': 'text/css' } })) app = await injectApp() }) @@ -63,26 +53,15 @@ describe('JS files with dependencies contain events', function () { const r1 = await app.inject({ url: '/index.html.js' }) expect(r1.headers.dependencies[0]).to.equal('main.css') - mockFetchConfig['http://ui-server']['/manifest.json'] = generateSimpleViteManifest({ - 'example.js': {}, - 'other.css': {}, - 'index.html': { - file: 'index.html.js', - isEntry: true, - imports: ['example.js'], - css: ['other.css'] - } - }) + // client.set('ui-middleware:versionInfo', JSON.stringify({ version: '123457' })) + client.set('ui-middleware:123457:/index.html.js:body', 'console.log("this is index.html.js")') + client.set('ui-middleware:123457:/index.html.js:meta', JSON.stringify({ headers: { 'content-type': 'application/javascript', dependencies: ['other.css'] } })) - await import('../src/version.js').then(async ({ updateVersionProcessor }) => { - // need to process two times to actually trigger the update - await updateVersionProcessor() - await updateVersionProcessor() - }) - - const r2 = await app.inject({ url: '/index.html.js' }) + const r2 = await app.inject({ url: '/index.html.js', headers: { version: '123457' } }) expect(r2.headers.dependencies[0]).to.equal('other.css') + console.log('r1.headers.version', r1.headers.version) + console.log('r2.headers.version', r2.headers.version) const r3 = await app.inject({ url: '/index.html.js', headers: { version: r1.headers.version } @@ -94,8 +73,5 @@ describe('JS files with dependencies contain events', function () { headers: { version: r2.headers.version } }) expect(r4.headers.dependencies[0]).to.equal('other.css') - - const r5 = await app.inject({ url: '/index.html.js' }) - expect(r5.headers.dependencies[0]).to.equal('other.css') }) }) diff --git a/spec/file_caching_test.js b/spec/file_caching_test.js index ad48603..ec9668e 100644 --- a/spec/file_caching_test.js +++ b/spec/file_caching_test.js @@ -20,7 +20,7 @@ * */ -import { generateSimpleViteManifest, injectApp, mockConfig, mockFetch, mockRedis, wait } from './util.js' +import { injectApp, mockRedis, mockConfig, mockFetch } from './util.js' import fs from 'fs' import { expect } from 'chai' import * as td from 'testdouble' @@ -37,46 +37,27 @@ describe('File caching service', function () { let redis beforeEach(async function () { - let count = 0 await mockConfig({ baseUrls: ['http://ui-server/'] }) redis = await mockRedis() mockFetch({ - 'http://ui-server': { - '/manifest.json': generateSimpleViteManifest({ - 'example.js': { imports: ['test.txt'] }, - 'test.txt': { }, - 'main.css': {}, - 'index.html': { - file: 'index.html.js', - isEntry: true, - imports: ['example.js'], - css: ['main.css'] - }, - 'image.png': {} - }), - '/example.js': () => new Response('this is example', { headers: { 'content-type': 'application/javascript' } }), - '/test.txt': () => new Response('this is test', { headers: { 'content-type': 'text/plain' } }), - '/index.html.js': () => new Response('this is index.html.js', { headers: { 'content-type': 'application/javascript' } }), - '/index.html': () => new Response('<html><head></head><body>it\'s me</body></html>', { headers: { 'content-type': 'text/html' } }), - '/main.css': () => new Response('.foo { color: #000; }', { headers: { 'content-type': 'text/css' } }), - '/favicon.ico': 'not really a favicon, though', - '/test.svg': () => { - if (count > 0) { - return new Response(null, { status: 404 }) - } - count++ - return new Response('<svg></svg>', { headers: { 'content-type': 'image/svg' } }) - }, - '/image.png': () => { - return new Response(image, { - headers: { - 'Content-Type': 'image/png', - 'Content-Length': imageStat.size.toString() - } - }) - } - } + 'http://ui-server': {} }) + await Promise.all([ + redis.client.set('ui-middleware:versionInfo', JSON.stringify({ version: 554855300 })), + redis.client.set('ui-middleware:554855300:viteManifests', JSON.stringify({})), + redis.client.set('ui-middleware:554855300:oxManifests:body', '{}'), + redis.client.set('ui-middleware:554855300:oxManifests:meta', JSON.stringify({ headers: { 'content-type': 'application/json' } })), + redis.client.set('ui-middleware:554855300:/example.js:body', 'this is example'), + redis.client.set('ui-middleware:554855300:/example.js:meta', JSON.stringify({ headers: { 'content-type': 'application/javascript' } })), + redis.client.set('ui-middleware:554855300:/test.txt:body', 'this is test'), + redis.client.set('ui-middleware:554855300:/test.txt:meta', JSON.stringify({ headers: { 'content-type': 'text/plain' } })), + redis.client.set('ui-middleware:554855300:/index.html:body', '<html><head></head><body>it\'s me</body></html>'), + redis.client.set('ui-middleware:554855300:/index.html:meta', JSON.stringify({ headers: { 'content-type': 'text/html' } })), + redis.client.set('ui-middleware:554855300:/main.css:body', 'body { color: red; }'), + redis.client.set('ui-middleware:554855300:/main.css:meta', JSON.stringify({ headers: { 'content-type': 'text/css' } })), + redis.client.set('ui-middleware:554855300:/image.png:body', image), + redis.client.set('ui-middleware:554855300:/image.png:meta', JSON.stringify({ headers: { 'content-type': 'image/png' } })) + ]) app = await injectApp() }) @@ -88,7 +69,7 @@ describe('File caching service', function () { it('serves files defined in manifest.json file', async function () { const response = await app.inject({ url: '/example.js' }) expect(response.statusCode).to.equal(200) - expect(response.headers['content-type']).to.equal('application/javascript; charset=utf-8') + expect(response.headers['content-type']).to.equal('application/javascript') expect(response.body).to.equal('this is example') // expect(response.headers['content-security-policy']).to.contain('sha256-NzZhMTE2Njc2YTgyNTZmZTdlZGVjZDU3YTNmYzRjNmM1OWZkMTI2NjRkYzZmMWM3YTkwMGU3ZTdhNDlhZmVlMwo=') const response2 = await app.inject({ url: '/test.txt' }) @@ -111,12 +92,6 @@ describe('File caching service', function () { expect(response.body).to.equal('<html><head></head><body>it\'s me</body></html>') }) - it('directly fetches files not referenced in manifest.json files from the upstream servers', async function () { - const response = await app.inject({ url: '/favicon.ico' }) - expect(response.statusCode).to.equal(200) - expect(response.body).to.equal('not really a favicon, though') - }) - it('returns 404 if file can not be resolved', async function () { const response = await app.inject({ url: '/unknown-file.txt' }) expect(response.statusCode).to.equal(404) @@ -130,142 +105,34 @@ describe('File caching service', function () { }) it('only fetches files once', async function () { - let spy - mockFetch({ - 'http://ui-server': { - '/manifest.json': generateSimpleViteManifest({}), - '/example.js': spy = sandbox.spy(() => { - return new Response('this is example', { headers: { 'content-type': 'application/javascript; charset=utf-8' } }) - }) - } - }) - app = await injectApp() - - expect(spy.callCount).to.equal(0) - let response = await app.inject({ url: '/example.js' }) - expect(response.statusCode).to.equal(200) - expect(spy.callCount).to.equal(1) - response = await app.inject({ url: '/example.js' }) - expect(response.statusCode).to.equal(200) - expect(spy.callCount).to.equal(1) - }) - - it('only fetches files once, but deliver from local cache', async function () { - let spy - mockFetch({ - 'http://ui-server': { - '/manifest.json': generateSimpleViteManifest({}), - '/example.js': spy = sandbox.spy(() => { - return new Response('this is example', { headers: { 'content-type': 'application/javascript; charset=utf-8' } }) - }) - } - }) - app = await injectApp() + const spy = sandbox.spy(redis.client, 'get') expect(spy.callCount).to.equal(0) let response = await app.inject({ url: '/example.js' }) expect(response.statusCode).to.equal(200) - expect(spy.callCount).to.equal(1) - // delete file from redis - await redis.client.del(`${response.headers.version}:/example.js:body`) - await redis.client.del(`${response.headers.version}:/example.js:meta`) - // and fetch once more + expect(spy.withArgs(sinon.match(/\/example.js/)).callCount).to.equal(1) response = await app.inject({ url: '/example.js' }) expect(response.statusCode).to.equal(200) - expect(spy.callCount).to.equal(1) + expect(spy.withArgs(sinon.match(/\/example.js/)).callCount).to.equal(1) }) it('delivers binary files from cache', async function () { - let spy - mockFetch({ - 'http://ui-server': { - '/manifest.json': generateSimpleViteManifest({}), - '/image.png': spy = sandbox.spy(() => { - return new Response(image, { - headers: { - 'Content-Type': 'image/png', - 'Content-Length': imageStat.size.toString() - } - }) - }) - } - }) - app = await injectApp() + const spy = sandbox.spy(redis.client, 'get') + expect(spy.callCount).to.equal(0) let response = await app.inject({ url: '/image.png' }) expect(response.statusCode).to.equal(200) expect(response.rawPayload).to.deep.equal(image) - expect(spy.callCount).to.equal(1) + expect(spy.withArgs(sinon.match(/\/image.png/)).callCount).to.equal(1) response = await app.inject({ url: '/image.png' }) expect(response.statusCode).to.equal(200) expect(response.rawPayload).to.deep.equal(image) - expect(spy.callCount).to.equal(1) - }) - - it('a file is not cached again, if loaded from cache', async function () { - const spy = sandbox.spy(redis.client, 'set') - - let response = await app.inject({ url: '/example.js' }) - expect(response.statusCode).to.equal(200) - - // called 4 times. - // once for manifests - // once for dependencies - // two times for for example.js (meta and body) - expect(spy.callCount).to.equal(4) - - response = await app.inject({ url: '/example.js' }) - expect(response.statusCode).to.equal(200) - - // should still be called 4 times, because everything is in cache - expect(spy.callCount).to.equal(4) - }) - - it('requests known file only from one server', async function () { - let spy1, spy2 - await mockConfig({ baseUrls: ['http://ui-server1/', 'http://ui-server2/'] }) - // we have example.js in both files. the first one will be overwritten and therefore not be called - mockFetch({ - 'http://ui-server1': { - '/manifest.json': generateSimpleViteManifest({ - 'example.js': { } - }), - '/example.js': spy1 = sandbox.spy(() => { - return new Response('example', { headers: { 'content-type': 'text/plain' } }) - }) - }, - 'http://ui-server2': { - '/manifest.json': generateSimpleViteManifest({ - 'example.js': { } - }), - '/example.js': spy2 = sandbox.spy(() => { - return new Response('example', { headers: { 'content-type': 'text/plain' } }) - }) - } - }) - app = await injectApp() - - const response = await app.inject({ url: '/example.js' }) - expect(response.statusCode).to.equal(200) - expect(spy1.callCount).to.equal(0) - expect(spy2.callCount).to.equal(1) + expect(spy.withArgs(sinon.match(/\/image.png/)).callCount).to.equal(1) }) it('serves cached files with version', async function () { - // we have example.js in both files. the first one will be overwritten and therefore not be called - mockFetch({ - 'http://ui-server': { - '/manifest.json': generateSimpleViteManifest({ - 'example.js': { } - }), - '/example.js': td.when(td.func()(td.matchers.anything(), td.matchers.anything())).thenReturn( - new Response('first', { headers: { 'content-type': 'text/plain' } }), - new Response('second', { headers: { 'content-type': 'text/plain' } }) - ) - } - }) - app = await injectApp() - + redis.client.set('ui-middleware:1234:/example.js:body', 'first') + redis.client.set('ui-middleware:1234:/example.js:meta', JSON.stringify({ headers: { 'content-type': 'application/javascript' } })) const response1 = await app.inject({ url: '/example.js', headers: { version: '1234' } @@ -275,85 +142,33 @@ describe('File caching service', function () { const response2 = await app.inject({ url: '/example.js' }) expect(response2.statusCode).to.equal(200) - expect(response2.body).to.equal('second') + expect(response2.body).to.equal('this is example') - const latestVersion = response2.headers['latest-version'] + const latestVersion = response1.headers['latest-version'] const response3 = await app.inject({ - url: '/example.js', - headers: { version: '1234' } - }) - expect(response3.statusCode).to.equal(200) - expect(response3.body).to.equal('first') - - const response4 = await app.inject({ url: '/example.js' }) - expect(response4.statusCode).to.equal(200) - expect(response4.body).to.equal('second') - - const response5 = await app.inject({ url: '/example.js', headers: { version: latestVersion } }) - expect(response5.statusCode).to.equal(200) - expect(response5.body).to.equal('second') + expect(response3.statusCode).to.equal(200) + expect(response3.body).to.equal('this is example') }) it('checks again for files after an error occurred', async function () { - // we have example.js in both files. the first one will be overwritten and therefore not be called - mockFetch({ - 'http://ui-server': { - '/manifest.json': generateSimpleViteManifest({ - 'example.js': { } - }), - '/example.js': td.when(td.func()(td.matchers.anything(), td.matchers.anything())).thenReturn( - new Response('UI-container not available', { headers: { 'content-type': 'text/plain' }, status: 503 }), - new Response('Now available', { headers: { 'content-type': 'text/plain' } }) - ) - } - }) - app = await injectApp() + redis.client.set('ui-middleware:123:viteManifests', JSON.stringify({})) + const response1 = await app.inject({ url: '/example.js', headers: { version: '123' } }) + expect(response1.statusCode).to.equal(404) - const response1 = await app.inject({ url: '/example.js' }) - expect(response1.statusCode).to.equal(503) + redis.client.set('ui-middleware:123:/example.js:body', 'Now available') + redis.client.set('ui-middleware:123:/example.js:meta', JSON.stringify({ headers: { 'content-type': 'application/javascript' } })) - const response2 = await app.inject({ url: '/example.js' }) + const response2 = await app.inject({ url: '/example.js', headers: { version: '123' } }) expect(response2.statusCode).to.equal(200) expect(response2.body).to.equal('Now available') }) - it('does not check again, when a 404 occurred', async function () { - // we have example.js in both files. the first one will be overwritten and therefore not be called - mockFetch({ - 'http://ui-server': { - '/manifest.json': generateSimpleViteManifest({ - 'example.js': { } - }), - '/example.js': td.when(td.func()(td.matchers.anything(), td.matchers.anything())).thenReturn( - new Response('Not found', { headers: { 'content-type': 'text/plain' }, status: 404 }), - new Response('Now found', { headers: { 'content-type': 'text/plain' } }) - ) - } - }) - app = await injectApp() - - const response1 = await app.inject({ url: '/example.js' }) - expect(response1.statusCode).to.equal(404) - - const response2 = await app.inject({ url: '/example.js' }) - expect(response2.statusCode).to.equal(404) - }) - it('only fetches files once, even when requested simultanously', async function () { - let spy - mockFetch({ - 'http://ui-server': { - '/manifest.json': generateSimpleViteManifest({}), - '/example.js': spy = sandbox.spy(() => { - return new Response('this is example', { headers: { 'content-type': 'application/javascript' } }) - }) - } - }) - app = await injectApp() + const spy = sandbox.spy(redis.client, 'get') expect(spy.callCount).to.equal(0) const [res1, res2] = await Promise.all([ @@ -362,21 +177,11 @@ describe('File caching service', function () { ]) expect(res1.statusCode).to.equal(200) expect(res2.statusCode).to.equal(200) - expect(spy.callCount).to.equal(1) + expect(spy.withArgs(sinon.match(/\/example.js/)).callCount).to.equal(1) }) it('only fetches manifests once, even when requested simultaneously', async function () { - let spy - mockFetch({ - 'http://ui-server': { - '/manifest.json': spy = sandbox.spy(async () => { - 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' } }) - } - }) - app = await injectApp() + const spy = sandbox.spy(redis.client, 'get') expect(spy.callCount).to.equal(0) const [res1, res2] = await Promise.all([ @@ -385,7 +190,7 @@ describe('File caching service', function () { ]) expect(res1.statusCode).to.equal(200) expect(res2.statusCode).to.equal(200) - expect(spy.callCount).to.equal(1) + expect(spy.withArgs(sinon.match(/oxManifests/)).callCount).to.equal(1) }) describe('redis request latency', function () { @@ -402,16 +207,7 @@ describe('File caching service', function () { }) it('only requests files once, even though the response takes some time', async function () { - let spy - mockFetch({ - 'http://ui-server': { - '/manifest.json': generateSimpleViteManifest({}), - '/example.js': spy = sandbox.spy(() => { - return new Response('this is example', { headers: { 'content-type': 'application/javascript' } }) - }) - } - }) - app = await injectApp() + const spy = sandbox.spy(redis.client, 'get') expect(spy.callCount).to.equal(0) const [res1, res2] = await Promise.all([ @@ -425,14 +221,8 @@ describe('File caching service', function () { }) it('serves index.html gzip compressed', async function () { - mockFetch({ - 'http://ui-server': { - '/manifest.json': generateSimpleViteManifest({}), - '/index.html': () => new Response([...new Array(2500)].join(' '), { headers: { 'content-type': 'text/html' } }) - } - }) - app = await injectApp() - + redis.client.set('ui-middleware:554855300:/index.html:body', zlib.gzipSync('<html><head></head><body>it\'s me</body></html>')) + redis.client.set('ui-middleware:554855300:/index.html:meta', JSON.stringify({ headers: { 'content-type': 'text/html', 'content-encoding': 'gzip' } })) const response = await app.inject({ url: '/index.html' }) expect(response.statusCode).to.equal(200) expect(response.headers['content-encoding']).to.equal('gzip') @@ -441,14 +231,8 @@ describe('File caching service', function () { }) it('serves files as brotli compressed', async function () { - mockFetch({ - 'http://ui-server': { - '/manifest.json': generateSimpleViteManifest({}), - '/large.js': () => new Response([...new Array(2500)].join('a'), { headers: { 'content-type': 'application/javascript' } }) - } - }) - app = await injectApp() - + redis.client.set('ui-middleware:554855300:/large.js:body', zlib.brotliCompressSync('this is example')) + redis.client.set('ui-middleware:554855300:/large.js:meta', JSON.stringify({ headers: { 'content-type': 'application/javascript', 'content-encoding': 'br' } })) const response = await app.inject({ url: '/large.js' }) expect(response.statusCode).to.equal(200) expect(response.headers['content-encoding']).to.equal('br') @@ -458,14 +242,8 @@ describe('File caching service', function () { }) it('does not serve small files with compression', async function () { - mockFetch({ - 'http://ui-server': { - '/manifest.json': generateSimpleViteManifest({}), - '/small.js': () => new Response('small', { headers: { 'content-type': 'application/javascript' } }) - } - }) - app = await injectApp() - + redis.client.set('ui-middleware:554855300:/small.js:body', 'this is example') + redis.client.set('ui-middleware:554855300:/small.js:meta', JSON.stringify({ headers: { 'content-type': 'text/plain' } })) const response = await app.inject({ url: '/small.js' }) expect(response.statusCode).to.equal(200) expect(response.headers).to.not.have.property('content-encoding') @@ -475,14 +253,8 @@ describe('File caching service', function () { }) it('does not serve other mime types with compression', async function () { - mockFetch({ - 'http://ui-server': { - '/manifest.json': generateSimpleViteManifest({}), - '/file.mp3': () => new Response('123', { headers: { 'content-type': 'audio/mpeg' } }) - } - }) - app = await injectApp() - + redis.client.set('ui-middleware:554855300:/file.mp3:body', 'this is example') + redis.client.set('ui-middleware:554855300:/file.mp3:meta', JSON.stringify({ headers: { 'content-type': 'audio/mpeg' } })) const response = await app.inject({ url: '/file.mp3' }) expect(response.statusCode).to.equal(200) expect(response.headers).to.not.have.property('content-encoding') @@ -492,14 +264,8 @@ describe('File caching service', function () { }) it('does serve svg with brotli compression (also escapes chars in regex)', async function () { - mockFetch({ - 'http://ui-server': { - '/manifest.json': generateSimpleViteManifest({}), - '/file.svg': () => new Response([...new Array(2500)].join(' '), { headers: { 'content-type': 'image/svg+xml' } }) - } - }) - app = await injectApp() - + redis.client.set('ui-middleware:554855300:/file.svg:body', zlib.brotliCompressSync([...new Array(2500)].join(' '))) + redis.client.set('ui-middleware:554855300:/file.svg:meta', JSON.stringify({ headers: { 'content-type': 'image/svg+xml', 'content-encoding': 'br' } })) const response = await app.inject({ url: '/file.svg' }) expect(response.statusCode).to.equal(200) expect(response.headers['content-encoding']).to.equal('br') diff --git a/spec/headers_test.js b/spec/headers_test.js index 81c2a43..95d6e76 100644 --- a/spec/headers_test.js +++ b/spec/headers_test.js @@ -20,35 +20,23 @@ * */ -import { generateSimpleViteManifest, injectApp, mockConfig, mockFetch, mockRedis } from './util.js' +import { injectApp, mockRedis } from './util.js' import { expect } from 'chai' import * as td from 'testdouble' import RedisMock from 'ioredis-mock' describe('Responses contain custom headers', function () { - let app - - before(async function () { - await mockConfig({ baseUrls: ['http://ui-server/'] }) - mockFetch({ - 'http://ui-server': { - '/manifest.json': generateSimpleViteManifest({ - 'example.js': {}, - 'main.css': {}, - 'index.html': { - file: 'index.html.js', - isEntry: true, - imports: ['example.js'], - css: ['main.css'] - } - }), - '/example.js': () => new Response('this is example', { headers: { 'content-type': 'application/javascript' } }), - '/index.html.js': () => new Response('this is index.html.js', { headers: { 'content-type': 'application/javascript' } }), - '/index.html': () => new Response('<html><head></head><body>it\'s me</body></html>', { headers: { 'content-type': 'text/html' } }), - '/main.css': () => new Response('.foo { color: #000; }', { headers: { 'content-type': 'text/css' } }) - } - }); - (await mockRedis()).isReady() + let app, client + + beforeEach(async function () { + client = (await mockRedis()).client + await Promise.all([ + client.set('ui-middleware:versionInfo', JSON.stringify({ version: 3215668592 })), + client.set('ui-middleware:3215668592:/index.html:body', '<html><head></head><body>it\'s me</body></html>'), + client.set('ui-middleware:3215668592:/index.html:meta', JSON.stringify({ headers: { 'content-type': 'text/html' } })), + client.set('ui-middleware:3215668592:/index.html.js:body', 'console.log("it\'s me")'), + client.set('ui-middleware:3215668592:/index.html.js:meta', JSON.stringify({ headers: { 'content-type': 'application/javascript', dependencies: ['main.css'] } })) + ]) app = await injectApp() }) @@ -68,95 +56,19 @@ describe('Responses contain custom headers', function () { }) it('serves requested version', async function () { + client.set('ui-middleware:123456:/index.html.js:body', 'console.log("it\'s old me")') + client.set('ui-middleware:123456:/index.html.js:meta', JSON.stringify({ headers: { 'content-type': 'application/javascript' } })) const response = await app.inject({ url: '/index.html.js', headers: { version: '123456' } }) expect(response.statusCode).to.equal(200) expect(response.headers.version).to.equal('123456') + expect(response.body).to.equal('console.log("it\'s old me")') expect(response.headers['latest-version']).to.equal('3215668592') }) it('javascript file contains dependencies', async function () { const response = await app.inject({ url: '/index.html.js' }) expect(response.statusCode).to.equal(200) + expect(response.body).to.equal('console.log("it\'s me")') expect(response.headers.dependencies[0]).to.equal('main.css') }) - - describe('with different files', function () { - beforeEach(async function () { - td.reset() - await new RedisMock().flushdb() - - await mockConfig({ baseUrls: ['http://ui-server/'] }) - await mockRedis() - mockFetch({ - 'http://ui-server': { - '/manifest.json': generateSimpleViteManifest({ - 'index.html': {} - }), - '/index.html': () => new Response('<html><head></head><body>it\'s me</body></html>', { headers: { 'content-type': 'text/html' } }) - } - }) - app = await injectApp() - }) - - it('index.html has version', async function () { - const response = await app.inject({ url: '/index.html' }) - expect(response.statusCode).to.equal(200) - // important here is, that it is different than in the test without meta.json - expect(response.headers.version).to.equal('3961519424') - }) - }) - - describe('with meta.json', function () { - beforeEach(async function () { - td.reset() - await new RedisMock().flushdb() - - await mockConfig({ baseUrls: ['http://ui-server/'] }) - await mockRedis() - mockFetch({ - 'http://ui-server': { - '/manifest.json': generateSimpleViteManifest({ - 'index.html': {} - }), - '/index.html': () => new Response('<html><head></head><body>it\'s me</body></html>', { headers: { 'content-type': 'text/html' } }), - '/meta.json': { commitSha: '1234567890' } - } - }) - app = await injectApp() - }) - - it('index.html has version', async function () { - const response = await app.inject({ url: '/index.html' }) - expect(response.statusCode).to.equal(200) - // important here is, that it is different than in the test without meta.json - expect(response.headers.version).to.equal('1487554813') - }) - }) - - describe('with different meta.json', function () { - beforeEach(async function () { - td.reset() - await new RedisMock().flushdb() - - await mockConfig({ baseUrls: ['http://ui-server/'] }) - await mockRedis() - mockFetch({ - 'http://ui-server': { - '/manifest.json': generateSimpleViteManifest({ - 'index.html': {} - }), - '/index.html': () => new Response('<html><head></head><body>it\'s me</body></html>', { headers: { 'content-type': 'text/html' } }), - '/meta.json': { commitSha: '0987654321' } - } - }) - app = await injectApp() - }) - - it('index.html has version', async function () { - const response = await app.inject({ url: '/index.html' }) - expect(response.statusCode).to.equal(200) - // important here is, that it is different than in the test without meta.json - expect(response.headers.version).to.equal('319344871') - }) - }) }) diff --git a/spec/meta_test.js b/spec/meta_test.js index da108c2..0ad1acc 100644 --- a/spec/meta_test.js +++ b/spec/meta_test.js @@ -20,44 +20,39 @@ * */ -import { generateSimpleViteManifest, injectApp, mockConfig, mockFetch, mockRedis } from './util.js' +import { injectApp, mockRedis } from './util.js' import { expect } from 'chai' import * as td from 'testdouble' import RedisMock from 'ioredis-mock' describe('Responses contain custom headers', function () { - let fetchConfig - let config + let client let app beforeEach(async function () { - await mockConfig(config = { baseUrls: ['http://ui-server/'] }) - await mockRedis() - mockFetch(fetchConfig = { - 'http://ui-server': { - '/manifest.json': generateSimpleViteManifest({ - 'example.js': {} - }), - '/example.js': () => new Response('this is example', { headers: { 'content-type': 'application/javascript' } }), - '/meta.json': { name: 'sample-service', version: '1.0' } - } - }) + client = (await mockRedis()).client + await Promise.all([ + client.set('ui-middleware:versionInfo', JSON.stringify({ version: '123456' })), + client.set('ui-middleware:123456:mergedMetadata', JSON.stringify([ + { name: 'sample-service', version: '1.0' }, + { + id: 'ui-middleware', + name: 'UI Middleware', + buildDate: '0123456789', + commitSha: '0123456789abcdef', + version: '4.2' + } + ])) + ]) app = await injectApp() }) afterEach(async function () { await new RedisMock().flushdb() - delete process.env.APP_VERSION - delete process.env.BUILD_TIMESTAMP - delete process.env.CI_COMMIT_SHA td.reset() }) it('has own metadata', async function () { - process.env.APP_VERSION = '4.2' - process.env.BUILD_TIMESTAMP = '0123456789' - process.env.CI_COMMIT_SHA = '0123456789abcdef' - const response = await app.inject({ url: '/meta' }) expect(response.statusCode).to.equal(200) expect(response.json()).to.deep.contain({ @@ -82,57 +77,10 @@ describe('Responses contain custom headers', function () { const response = await app.inject({ url: '/meta' }) expect(response.json()).to.have.length(2) - config.baseUrls = [] - await import('../src/version.js').then(async ({ updateVersionProcessor }) => { - // need to process two times to actually trigger the update - await updateVersionProcessor() - await updateVersionProcessor() - }) - - const response2 = await app.inject({ url: '/meta' }) + client.set('ui-middleware:123457:mergedMetadata', JSON.stringify([ + { name: 'sample-service', version: '2.0' } + ])) + const response2 = await app.inject({ url: '/meta', headers: { version: '123457' } }) expect(response2.json()).to.have.length(1) }) - - describe('without service avaible', function () { - beforeEach(function () { - delete fetchConfig['http://ui-server'] - }) - - it('does not have metadata from ui service when unavailable', async function () { - await import('../src/cache.js').then(({ clear }) => clear()) - const response = await app.inject({ url: '/meta' }) - expect(response.statusCode).to.equal(200) - expect(response.json()).to.not.deep.contain({ - name: 'sample-service', - version: '1.0' - }) - }) - }) - - describe('without redis disabled', function () { - beforeEach(async function () { - td.reset() - await mockConfig({ baseUrls: ['http://ui-server/'] }) - await mockRedis() - mockFetch(fetchConfig = { - 'http://ui-server': { - '/manifest.json': generateSimpleViteManifest({ - 'example.js': {} - }), - '/example.js': () => new Response('this is example', { headers: { 'content-type': 'application/javascript' } }), - '/meta.json': { name: 'sample-service', version: '1.0' } - } - }) - app = await injectApp() - }) - - it('has metadata', async function () { - const response = await app.inject({ url: '/meta' }) - expect(response.statusCode).to.equal(200) - expect(response.json()).to.deep.contain({ - name: 'sample-service', - version: '1.0' - }) - }) - }) }) diff --git a/spec/redis_test.js b/spec/redis_test.js deleted file mode 100644 index 41b4ac7..0000000 --- a/spec/redis_test.js +++ /dev/null @@ -1,63 +0,0 @@ -/* - * - * @copyright Copyright (c) OX Software GmbH, Germany <info@open-xchange.com> - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with OX App Suite. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>. - * - * Any use of the work other than as authorized under this license or copyright law is prohibited. - * - */ - -import { generateSimpleViteManifest, injectApp, mockConfig, mockFetch, mockRedis } from './util.js' -import { expect } from 'chai' -import * as td from 'testdouble' -import sinon from 'sinon' -import RedisMock from 'ioredis-mock' - -const sandbox = sinon.createSandbox() - -describe('Redis', function () { - let app - let spy - - beforeEach(async function () { - await mockConfig({ baseUrls: ['http://ui-server/'] }) - await mockRedis({}, false) - mockFetch({ - 'http://ui-server': { - '/manifest.json': generateSimpleViteManifest({}), - '/example.js': spy = sandbox.spy(() => { - return new Response('this is example', { headers: { 'content-type': 'application/javascript' } }) - }) - } - }) - app = await injectApp() - }) - - afterEach(async function () { - td.reset() - await new RedisMock().flushdb() - }) - - it('use internal cache, when redis is disabled', async function () { - expect(spy.callCount).to.equal(0) - let response = await app.inject({ url: '/example.js' }) - expect(response.statusCode).to.equal(200) - expect(spy.callCount).to.equal(1) - response = await app.inject({ url: '/example.js' }) - expect(response.statusCode).to.equal(200) - expect(spy.callCount).to.equal(1) - }) -}) diff --git a/spec/salt_test.js b/spec/salt_test.js index 4cdeecb..4a755b2 100644 --- a/spec/salt_test.js +++ b/spec/salt_test.js @@ -47,6 +47,14 @@ describe('Salt', function () { }) it('change version when salt changes', async function () { + const pubClient = new RedisMock() + let runUpdate + await import('../src/version.js').then(async ({ updateVersionProcessor }) => { + runUpdate = updateVersionProcessor + }) + await runUpdate(pubClient) + await runUpdate(pubClient) + const response = await app.inject({ url: '/manifests' }) expect(response.statusCode).to.equal(200) expect(response.headers.version).to.equal('1916675216') @@ -54,11 +62,9 @@ describe('Salt', function () { // update salt config.salt = '1' - await import('../src/version.js').then(async ({ updateVersionProcessor }) => { - // need to process two times to actually trigger the update - await updateVersionProcessor() - await updateVersionProcessor() - }) + // need to process two times to actually trigger the update + await runUpdate(pubClient) + await runUpdate(pubClient) const responseAfterUpdate = await app.inject({ url: '/manifests' }) expect(responseAfterUpdate.statusCode).to.equal(200) diff --git a/spec/util.js b/spec/util.js index c7ee1ac..4a67a48 100644 --- a/spec/util.js +++ b/spec/util.js @@ -79,9 +79,8 @@ export async function mockRedis (data = {}, isEnabled = true) { const mock = { isReady () { return Promise.resolve() }, isEnabled () { return isEnabled }, - client: new RedisMock(data), - pubClient: new RedisMock(), - subClient: new RedisMock() + createClient () { return new RedisMock() }, + client: new RedisMock(data) } await td.replaceEsm('../src/redis.js', mock) return mock diff --git a/spec/version_mismatches_test.js b/spec/version_mismatches_test.js index cbb49cb..5438e58 100644 --- a/spec/version_mismatches_test.js +++ b/spec/version_mismatches_test.js @@ -24,13 +24,16 @@ import { generateSimpleViteManifest, injectApp, mockConfig, mockFetch, mockRedis import { expect } from 'chai' import * as td from 'testdouble' import RedisMock from 'ioredis-mock' +import promClient from 'prom-client' describe('version mismatches', function () { let app + let pubClient + let runUpdate beforeEach(async function () { await mockConfig({ baseUrls: ['http://ui-server/'] }) - await mockRedis() + const { createClient } = await mockRedis() mockFetch({ 'http://ui-server': { '/manifest.json': generateSimpleViteManifest({ @@ -49,6 +52,13 @@ describe('version mismatches', function () { ) } }) + pubClient = createClient('pubClient') + for (const prop of Object.getOwnPropertyNames(promClient.register._metrics)) { + delete promClient.register._metrics[prop] + } + const { updateVersionProcessor } = await import('../src/version.js') + runUpdate = updateVersionProcessor + runUpdate(pubClient) app = await injectApp() }) @@ -68,6 +78,9 @@ describe('version mismatches', function () { response = await app.inject({ url: '/bar.js' }) expect(response.statusCode).to.equal(404) + await runUpdate(pubClient) + await runUpdate(pubClient) + // get foo.js again. Since the versions should coincide now, the client should receive the new file response = await app.inject({ url: '/foo.js' }) expect(response.statusCode).to.equal(200) @@ -86,6 +99,9 @@ describe('version mismatches', function () { response = await app.inject({ url: '/whatever.js' }) expect(response.statusCode).to.equal(404) + await runUpdate(pubClient) + await runUpdate(pubClient) + // get foo.js again. Since the versions should coincide now, the client should receive the new file response = await app.inject({ url: '/foo.js' }) expect(response.statusCode).to.equal(200) diff --git a/src/cache.js b/src/cache.js index 179d7bc..b996c40 100644 --- a/src/cache.js +++ b/src/cache.js @@ -20,6 +20,7 @@ * */ +import { NotFoundError } from './errors.js' import logger from './logger.js' import * as redis from './redis.js' import { getRedisKey } from './util.js' @@ -116,6 +117,10 @@ export function getFile ({ name, version }, fallback) { return (cache[key] = { body, ...JSON.parse(meta) }) } + if (!fallback) { + delete cache[key] + throw new NotFoundError(`[Cache] Not found: ${key}`) + } const dataFromServer = await fallback({ version, name }).catch(err => { if (err.status !== 404) delete cache[key] throw err @@ -141,5 +146,6 @@ export function getFile ({ name, version }, fallback) { function expire (key) { logger.debug(`[Cache] Key ${key} has expired.`) + fileCacheSizeGauge.dec() delete cache[key] } diff --git a/src/errors.js b/src/errors.js index e69343e..aa6157a 100644 --- a/src/errors.js +++ b/src/errors.js @@ -21,7 +21,7 @@ */ export class NotFoundError extends Error { - constructor (message, options) { + constructor (message, options = {}) { super(message, options) this.status = options.status } diff --git a/src/files.js b/src/files.js index 161f68c..615728e 100644 --- a/src/files.js +++ b/src/files.js @@ -28,7 +28,7 @@ import { configMap } from './config_map.js' import { NotFoundError, VersionMismatchError, isVersionMismatchError } from './errors.js' import logger from './logger.js' import { getCSSDependenciesFor, getViteManifests } from './manifests.js' -import { getVersionInfo, updateVersionProcessor } from './version.js' +import { getVersionInfo } from './version.js' const gzip = promisify(zlib.gzip) const brotliCompress = promisify(zlib.brotliCompress) @@ -95,35 +95,31 @@ export async function fetchFileWithHeaders ({ path, version }) { return Promise.any(configMap.urls.map(baseUrl => fetchFileWithHeadersFromBaseUrl({ path, baseUrl, version }))) } -export function getFile ({ version, path }) { +export function getFile ({ version, path, fetchFiles = true }) { return cache.getFile({ name: path, version }, ({ name: path, version }) => { + if (!fetchFiles) throw new Error('[File] Not found in cache: ' + 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 }) }) } -export async function warmCache ({ version }) { +export async function warmCache ({ version, fetchFiles = false }) { const start = +new Date() logger.info('[File] start warming up the cache') - const viteManifests = await getViteManifests({ version }) - - for (const key of Object.keys(viteManifests)) { - const path = `/${viteManifests[key].file}` - if (!path) continue - 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}`) - } + const viteManifests = {} + try { + Object.assign(viteManifests, await getViteManifests({ version })) + } catch (err) {} + const paths = Object.keys(viteManifests).map(key => `/${viteManifests[key].file}`) + + while (paths.length > 0) { + await Promise.all(paths.splice(0, 20).map(path => { + return getFile({ version, path, fetchFiles }) + })) } logger.info(`[File] finished warming up the cache in ${Math.floor((+new Date() - start) / 1000)}s`) diff --git a/src/index.js b/src/index.js index 346be77..be93348 100644 --- a/src/index.js +++ b/src/index.js @@ -31,23 +31,32 @@ import logger from './logger.js' import fastify from 'fastify' import autoLoad from '@fastify/autoload' -import { getLatestVersion } from './version.js' +import { getLatestVersion, registerLatestVersionListener } from './version.js' import { configMap } from './config_map.js' -import * as redis from './redis.js' import { warmCache } from './files.js' +import { createClient, isReady, client } from './redis.js' import lightship from './lightship.js' const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) +const subClient = createClient('sub client') // Load env vars from .env and .env.defaults files // Note: actual env vars supersede .env file and .env file supersedes .env.defaults file config() -lightship.queueBlockingTask(redis.isReady()) +async function waitForVersionAvailable () { + let version = await getLatestVersion() + while (!version) { + version = await getLatestVersion() + logger.info(`[Health] Check latest version on startup. Found ${version}`) + await new Promise(resolve => setTimeout(resolve, 1000)) + } +} + +lightship.queueBlockingTask(isReady()) lightship.queueBlockingTask(configMap.load()) -lightship.queueBlockingTask(getLatestVersion() - .then(() => logger.info('[Health] Check latest version on startup.'))) +lightship.queueBlockingTask(waitForVersionAvailable()) // Create a Fastify server const app = fastify({ @@ -71,11 +80,13 @@ const autoLoadOptions = { dir: join(__dirname, 'routes'), autoHooks: true } if (process.env.APP_ROOT !== '/') autoLoadOptions.options = { prefix: String(process.env.APP_ROOT).replace(/\/$/, '') } app.register(autoLoad, autoLoadOptions) -app.addHook('onReady', () => { - // don't block the onReady hook +lightship.whenFirstReady().then(async () => { + // don't block getLatestVersion() .then(version => warmCache({ version })) .catch(err => logger.error(err)) + + registerLatestVersionListener(subClient) }) // This hook is used to signal lightship that the service is ready to receive requests @@ -92,9 +103,8 @@ try { lightship.registerShutdownHandler(async () => { logger.info('[Service] Shutting down...') await Promise.all([ - redis.client.quit(), - redis.pubClient.quit(), - redis.subClient.quit() + client.quit(), + subClient.quit() ]) await app.close() }) diff --git a/src/lightship.js b/src/lightship.js index d523db5..eeed213 100644 --- a/src/lightship.js +++ b/src/lightship.js @@ -23,7 +23,9 @@ import { createLightship } from 'lightship' import logger from './logger.js' -const lightship = await createLightship() +const lightship = await createLightship({ + port: Number(process.env.LIGHTSHIP_PORT) +}) // This is a graceful shutdown handler in case of uncaught exceptions process.on('uncaughtException', async err => { diff --git a/src/redis.js b/src/redis.js index 993cf54..66f8b46 100644 --- a/src/redis.js +++ b/src/redis.js @@ -22,8 +22,6 @@ import Redis from 'ioredis' import logger from './logger.js' -import Queue from 'bull' -import { registerLatestVersionListener, updateVersionProcessor } from './version.js' const commonQueueOptions = { enableReadyCheck: false, maxRetriesPerRequest: null } @@ -32,7 +30,7 @@ const hosts = (process.env.REDIS_HOSTS || '').split(',').map(host => { return { host: hostname, port: Number(port) } }) -function createClient (id, options = {}) { +export function createClient (id, options = commonQueueOptions) { options = { db: Number(process.env.REDIS_DB), password: process.env.REDIS_PASSWORD, @@ -69,69 +67,6 @@ export async function isReady () { } export const client = createClient('common client', { maxRetriesPerRequest: 1 }) -export const pubClient = createClient('pub client') -export const subClient = createClient('sub client', commonQueueOptions) - -/* - * Bull specific things are below - */ - -client.on('ready', () => { - const updateVersionQueue = getQueue('update-version') - try { - updateVersionQueue.process(updateVersionProcessor) - logger.debug('[Redis] Register version update processor.') - } catch (err) { - logger.debug('[Redis] Update version processor is already registered.') - } - updateVersionQueue.add({}, { - jobId: 'update-version-job', - repeat: { every: Number(process.env.CACHE_TTL) }, - removeOnComplete: 10, - removeOnFail: 10, - timeout: 10000 - }) -}) - -registerLatestVersionListener(subClient) - -/* - * queue specific code - */ - -const queues = {} - -export function getQueue (name) { - if (queues[name]) return queues[name] - return (queues[name] = new Queue(name, { - prefix: process.env.REDIS_PREFIX, - createClient: function (type) { - switch (type) { - case 'client': - return client.duplicate() - case 'subscriber': - return subClient.duplicate() - default: - return client.duplicate(commonQueueOptions) - } - } - })) -} - -export function getQueues () { - return Object.values(queues) -} - -export async function closeQueue (name) { - if (!queues[name]) throw new Error(`No such queue "${name}" to close.`) - const queue = queues[name] - delete queues[name] - const jobs = await queue.getRepeatableJobs() - for (const job of jobs) { - queue.removeRepeatableByKey(job.key) - } - return queue.close() -} export function isEnabled () { return !!process.env.REDIS_HOST diff --git a/src/routes/metadata.js b/src/routes/metadata.js index e4b6792..fadceea 100644 --- a/src/routes/metadata.js +++ b/src/routes/metadata.js @@ -20,37 +20,12 @@ * */ -import { configMap } from '../config_map.js' import * as cache from '../cache.js' import { getRedisKey } from '../util.js' -async function fetchMergedMetadata () { - const metadata = await Promise.all(configMap.urls.map(async url => { - const { origin } = new URL(url) - try { - const response = await fetch(new URL('meta.json', origin), { cache: 'no-store' }) - if (!response.ok) return - return response.json() - } catch (e) { - // unhandled - } - })) - - metadata.push({ - id: 'ui-middleware', - name: 'UI Middleware', - buildDate: process.env.BUILD_TIMESTAMP, - commitSha: process.env.CI_COMMIT_SHA, - version: process.env.APP_VERSION - }) - - // only return when contains data - return metadata.filter(Boolean) -} - export default async function metadataPlugin (fastify) { fastify.get('/meta', async (req, res) => { - const mergedMetadata = await cache.get(getRedisKey({ version: res.version, name: 'mergedMetadata' }), async () => [await fetchMergedMetadata()]) + const mergedMetadata = await cache.get(getRedisKey({ version: res.version, name: 'mergedMetadata' })) res.send(mergedMetadata) }) } diff --git a/src/updater.js b/src/updater.js new file mode 100644 index 0000000..0361019 --- /dev/null +++ b/src/updater.js @@ -0,0 +1,69 @@ +/* + * + * @copyright Copyright (c) OX Software GmbH, Germany <info@open-xchange.com> + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with OX App Suite. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>. + * + * Any use of the work other than as authorized under this license or copyright law is prohibited. + * + */ + +import { config } from 'dotenv-defaults' +import { createClient, client, isReady } from './redis.js' +import logger from './logger.js' +import fastify from 'fastify' +import autoLoad from '@fastify/autoload' +import { fileURLToPath } from 'node:url' +import { join, dirname } from 'node:path' +import lightship from './lightship.js' +import { updateVersionProcessor } from './version.js' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) +const pubClient = createClient('pub client') + +config() + +async function runUpdate () { + try { + await updateVersionProcessor(pubClient) + } catch (err) {} finally { + setTimeout(() => runUpdate(), Number(process.env.CACHE_TTL)) + } +} +runUpdate() + +lightship.queueBlockingTask(isReady()) + +const app = fastify({}) +app.register(autoLoad, { dir: join(__dirname, 'plugins') }) +app.addHook('onReady', () => { lightship.signalReady() }) + +try { + // Binds and listens for connections on the specified host and port + await app.listen({ host: '::', port: Number(process.env.PORT) }) +} catch (err) { + logger.error(err) + await lightship.shutdown() +} + +lightship.registerShutdownHandler(async () => { + logger.info('[Service] Shutting down...') + await Promise.all([ + client.quit(), + pubClient.quit() + ]) + await app.close() +}) diff --git a/src/version.js b/src/version.js index b7e5b58..9b1f1f5 100644 --- a/src/version.js +++ b/src/version.js @@ -21,30 +21,20 @@ */ import { configMap } from './config_map.js' -import { asyncThrottle, getRedisKey, hash } from './util.js' +import { 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' +import { getViteManifests } from './manifests.js' -const versionInfo = { +export 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({ +export const versionUpdateGauge = new Gauge({ name: 'version_update_event', help: 'Timestamp of a version update event', labelNames: ['version'] @@ -92,21 +82,11 @@ export async function getVersionInfo () { 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 } @@ -144,7 +124,45 @@ export function registerLatestVersionListener (client) { }) } -export const updateVersionProcessor = asyncThrottle(async function updateVersionProcessor ({ immediate = false } = {}) { +// only observe the version update event, no need to store the reference +// eslint-disable-next-line no-new +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'] +}) + +export async function fetchMergedMetadata () { + const metadata = await Promise.all(configMap.urls.map(async url => { + const { origin } = new URL(url) + try { + const response = await fetch(new URL('meta.json', origin), { cache: 'no-store' }) + if (!response.ok) return + return response.json() + } catch (e) { + // unhandled + } + })) + + metadata.push({ + id: 'ui-middleware', + name: 'UI Middleware', + buildDate: process.env.BUILD_TIMESTAMP, + commitSha: process.env.CI_COMMIT_SHA, + version: process.env.APP_VERSION + }) + + // only return when contains data + return metadata.filter(Boolean) +} + +let prevProcessedVersion = null + +export async function updateVersionProcessor (pubClient) { try { logger.info('[Version] Check for new version') await configMap.load() @@ -154,33 +172,31 @@ export const updateVersionProcessor = asyncThrottle(async function updateVersion 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 (prevProcessedVersion && storedVersion === fetchedVersionInfo.version) { + // make sure to limit memory consumption and always check redis + cache.clear() - 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.') + if (!storedVersion || prevProcessedVersion === fetchedVersionInfo.version) { // update local version info Object.assign(versionInfo, fetchedVersionInfo) const stringifiedVersionInfo = JSON.stringify(versionInfo) - redis.pubClient.publish(getRedisKey({ name: 'updateVersionInfo' }), stringifiedVersionInfo) + cache.clear() + await warmCache({ version: versionInfo.version, fetchFiles: true }) await redis.client.set(getRedisKey({ name: 'versionInfo' }), stringifiedVersionInfo) + await cache.get(getRedisKey({ version: versionInfo.version, name: 'mergedMetadata' }), async () => [await fetchMergedMetadata()]) versionUpdateGauge.setToCurrentTime({ version: versionInfo.version }) + logger.info('[Version] publish update to other nodes.') + pubClient.publish(getRedisKey({ name: 'updateVersionInfo' }), stringifiedVersionInfo) } 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) + prevProcessedVersion = fetchedVersionInfo.version } - - return versionInfo.version } catch (err) { logger.error(`[Version] comparing version is not possible. Error: ${err.message}`) } -}) +} -- GitLab