From 1eeca35683ba57301ba23c21d721e2b160d7f262 Mon Sep 17 00:00:00 2001
From: "richard.petersen" <richard.petersen@open-xchange.com>
Date: Wed, 4 May 2022 14:02:04 +0000
Subject: [PATCH] Add optional flag for redis configuration

---
 .gitlab/autodeploy/values.yaml                |  1 +
 .../templates/deployment.yaml                 |  2 +
 helm/core-ui-middleware/values.yaml           |  1 +
 integration/.mocharc.cjs                      |  1 +
 integration/global-setup.js                   |  1 +
 package.json                                  |  2 +-
 spec/redis_test.js                            | 41 ++++++++++++++++++-
 spec/util.js                                  |  3 +-
 src/cache.js                                  | 25 +++++++++++
 src/create-queues.js                          | 20 +++++----
 src/files.js                                  | 30 ++++----------
 src/manifests.js                              | 14 +++----
 src/redis.js                                  | 12 ++++++
 src/routes/health.js                          |  6 ++-
 src/version.js                                | 36 ++++++++++------
 15 files changed, 140 insertions(+), 55 deletions(-)
 create mode 100644 integration/global-setup.js
 create mode 100644 src/cache.js

diff --git a/.gitlab/autodeploy/values.yaml b/.gitlab/autodeploy/values.yaml
index 36753db..10f48ba 100644
--- a/.gitlab/autodeploy/values.yaml
+++ b/.gitlab/autodeploy/values.yaml
@@ -6,6 +6,7 @@ baseUrls:
   - http://main-core-ui.main-e2e-stack.svc.cluster.local
 
 redis:
+  enabled: true
   host: main-redis-master.main-e2e-stack.svc.cluster.local
   prefix: ${CI_COMMIT_REF_SLUG}-${OX_COMPONENT}
 
diff --git a/helm/core-ui-middleware/templates/deployment.yaml b/helm/core-ui-middleware/templates/deployment.yaml
index a394d48..5bc305a 100644
--- a/helm/core-ui-middleware/templates/deployment.yaml
+++ b/helm/core-ui-middleware/templates/deployment.yaml
@@ -27,6 +27,7 @@ spec:
               value: "{{ .Values.logLevel }}"
             - name: APP_ROOT
               value: "{{ .Values.appRoot }}"
+            {{- if .Values.redis.enabled }}
             - name: REDIS_HOST
               value: "{{ required "redis.host required" .Values.redis.host }}"
             - name: REDIS_PORT
@@ -37,6 +38,7 @@ spec:
               value: "{{ .Values.redis.password }}"
             - name: REDIS_PREFIX
               value: "{{ .Values.redis.prefix }}"
+            {{- end }}
           ports:
             - name: http
               containerPort: {{ .Values.containerPort | default 8080 }}
diff --git a/helm/core-ui-middleware/values.yaml b/helm/core-ui-middleware/values.yaml
index a9bb9fe..315666a 100644
--- a/helm/core-ui-middleware/values.yaml
+++ b/helm/core-ui-middleware/values.yaml
@@ -105,6 +105,7 @@ baseUrls: []
 appRoot: '/'
 
 redis:
+  enabled: false
   host: ''
   port: 6379
   db: 0
diff --git a/integration/.mocharc.cjs b/integration/.mocharc.cjs
index c02759d..d7be27b 100644
--- a/integration/.mocharc.cjs
+++ b/integration/.mocharc.cjs
@@ -1,3 +1,4 @@
 module.exports = {
   spec: ['integration/**/*_test.js'],
+  file: ['integration/global-setup.js']
 }
\ No newline at end of file
diff --git a/integration/global-setup.js b/integration/global-setup.js
new file mode 100644
index 0000000..912f193
--- /dev/null
+++ b/integration/global-setup.js
@@ -0,0 +1 @@
+process.env.REDIS_HOST = 'localhost'
diff --git a/package.json b/package.json
index 0400daf..32b7a0b 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,7 @@
     "start": "node src/index.js",
     "dev": "nodemon index.js",
     "prepare": "husky install",
-    "test": "LOG_LEVEL=error mocha --loader=testdouble --config spec/.mocharc.cjs",
+    "test": "LOG_LEVEL=error mocha --loader=testdouble --config spec/.mocharc.cjs --exit",
     "integration": "LOG_LEVEL=error mocha --loader=testdouble --config integration/.mocharc.cjs --exit",
     "release-chart": "cd helm/core-ui-middleware/ && npx --package=@open-xchange/release-it -- release-it",
     "release-app": "npx --package=@open-xchange/release-it@latest -- release-it-auto-keepachangelog",
diff --git a/spec/redis_test.js b/spec/redis_test.js
index 1fc53a4..c9ce3e8 100644
--- a/spec/redis_test.js
+++ b/spec/redis_test.js
@@ -1,5 +1,42 @@
+import request from 'supertest'
+import { generateSimpleViteManifest, mockApp, mockConfig, mockFetch } from './util.js'
+import { expect } from 'chai'
+import * as td from 'testdouble'
+import { Response } from 'node-fetch'
+import sinon from 'sinon'
+
+const sandbox = sinon.createSandbox()
+
 describe('Redis', function () {
-  it('first instance updates version', async function () {
-    // TODO
+  let app
+  let spy
+
+  beforeEach(async function () {
+    // no redis mock!!
+    await import('../src/create-queues.js').then(({ default: createQueues }) => createQueues())
+    mockConfig({ urls: ['http://ui-server/'] })
+    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 mockApp()
+  })
+
+  afterEach(async function () {
+    td.reset()
+  })
+
+  it('use internal cache, when redis is disabled', async function () {
+    expect(spy.callCount).to.equal(0)
+    let response = await request(app).get('/example.js')
+    expect(response.statusCode).to.equal(200)
+    expect(spy.callCount).to.equal(1)
+    response = await request(app).get('/example.js')
+    expect(response.statusCode).to.equal(200)
+    expect(spy.callCount).to.equal(1)
   })
 })
diff --git a/spec/util.js b/spec/util.js
index f7bb5a9..4667576 100644
--- a/spec/util.js
+++ b/spec/util.js
@@ -42,9 +42,10 @@ export function mockFetch (servers = {}) {
   })
 }
 
-export function mockRedis (data = {}) {
+export function mockRedis (data = {}, isEnabled = true) {
   const mock = {
     isReady () { return Promise.resolve() },
+    isEnabled () { return isEnabled },
     client: new RedisMock({ data }),
     pubClient: new RedisMock(),
     subClient: new RedisMock()
diff --git a/src/cache.js b/src/cache.js
new file mode 100644
index 0000000..523b798
--- /dev/null
+++ b/src/cache.js
@@ -0,0 +1,25 @@
+import * as redis from './redis.js'
+
+const cache = {}
+
+export function set (key, value) {
+  if (cache[key] === value) return
+  cache[key] = value
+  if (redis.isEnabled()) {
+    return redis.client.set(key, value)
+  }
+}
+
+export async function getBuffer (key) {
+  return get(key, { method: 'getBuffer' })
+}
+
+export async function get (key, { method = 'get' } = {}) {
+  if (cache[key]) return cache[key]
+
+  if (redis.isEnabled()) {
+    const result = await redis.client[method]?.(key)
+    cache[key] = result
+    return result
+  }
+}
diff --git a/src/create-queues.js b/src/create-queues.js
index 7c0dfea..6347c6b 100644
--- a/src/create-queues.js
+++ b/src/create-queues.js
@@ -1,14 +1,18 @@
-import { getQueue, subClient } from './redis.js'
+import * as redis from './redis.js'
 import { updateVersionProcessor, registerLatestVersionListener } from './version.js'
 
+const { getQueue, subClient } = redis
+
 export default function createQueues () {
-  const updateVersionQueue = getQueue('update-version')
-  updateVersionQueue.process(updateVersionProcessor)
-  updateVersionQueue.add({}, {
-    jobId: 'update-version-job',
-    repeat: { every: Number(process.env.CACHE_TTL) },
-    removeOnComplete: true
-  })
+  if (redis.isEnabled()) {
+    const updateVersionQueue = getQueue('update-version')
+    updateVersionQueue.process(updateVersionProcessor)
+    updateVersionQueue.add({}, {
+      jobId: 'update-version-job',
+      repeat: { every: Number(process.env.CACHE_TTL) },
+      removeOnComplete: true
+    })
+  }
 
   // not a queue but though, used by redis
   registerLatestVersionListener(subClient)
diff --git a/src/files.js b/src/files.js
index 235fd3f..3cee604 100644
--- a/src/files.js
+++ b/src/files.js
@@ -3,12 +3,10 @@ import crypto from 'crypto'
 import { config } from './config.js'
 import { getRedisKey, isJSFile } from './util.js'
 import { getCSSDependenciesFor, getViteManifests } from './manifests.js'
-import { client } from './redis.js'
+import * as cache from './cache.js'
 import { logger } from './logger.js'
 import { NotFoundError } from './errors.js'
 
-const fileCache = {}
-
 export async function fetchFileWithHeadersFromBaseUrl (path, baseUrl, version) {
   const [response, dependencies] = await Promise.all([
     fetch(new URL(path, baseUrl)),
@@ -56,27 +54,17 @@ export async function fetchFileWithHeaders ({ path, version }) {
 
 export async function saveToCache ({ version, path, body, headers, ...rest }) {
   if (typeof body !== 'string' && !(body instanceof Buffer)) body = JSON.stringify(body)
-  fileCache[version] = fileCache[version] || {}
-  fileCache[version][path] = {
-    body,
-    headers,
-    ...rest
-  }
   return Promise.all([
-    client.set(getRedisKey({ version, name: `${path}:body` }), body),
-    client.set(getRedisKey({ version, name: `${path}:meta` }), JSON.stringify({ headers, ...rest }))
+    cache.set(getRedisKey({ version, name: `${path}:body` }), body),
+    cache.set(getRedisKey({ version, name: `${path}:meta` }), JSON.stringify({ headers, ...rest }))
   ])
 }
 
 export async function loadFromCache ({ version, path }) {
-  if (!fileCache[version]?.[path]) {
-    const [body, meta = '{}'] = await Promise.all([
-      client.getBuffer(getRedisKey({ version, name: `${path}:body` })),
-      client.get(getRedisKey({ version, name: `${path}:meta` }))
-    ])
-    if (!body) return
-    fileCache[version] = fileCache[version] || {}
-    fileCache[version][path] = { ...JSON.parse(meta), body }
-  }
-  return fileCache[version][path]
+  const [body, meta = '{}'] = await Promise.all([
+    cache.getBuffer(getRedisKey({ version, name: `${path}:body` })),
+    cache.get(getRedisKey({ version, name: `${path}:meta` }))
+  ])
+  if (!body) return
+  return { ...JSON.parse(meta), body }
 }
diff --git a/src/manifests.js b/src/manifests.js
index f3f1ca0..87ca688 100644
--- a/src/manifests.js
+++ b/src/manifests.js
@@ -2,10 +2,10 @@ import fetch from 'node-fetch'
 import { config } from './config.js'
 import { getRedisKey, viteManifestToDeps, viteToOxManifest } from './util.js'
 import { logger } from './logger.js'
-import { client } from './redis.js'
+import * as cache from './cache.js'
 
 export async function getViteManifests ({ version }) {
-  const manifests = await client.get(getRedisKey({ version, name: 'viteManifests' }))
+  const manifests = await cache.get(getRedisKey({ version, name: 'viteManifests' }))
   if (manifests) return JSON.parse(manifests)
 
   await config.load()
@@ -30,27 +30,27 @@ export async function getViteManifests ({ version }) {
 
   // combine all manifests by keys. With duplicates, last wins
   const newManifests = viteManifests.reduce((memo, manifest) => Object.assign(memo, manifest), {})
-  await client.set(getRedisKey({ version, name: 'viteManifests' }), JSON.stringify(newManifests))
+  await cache.set(getRedisKey({ version, name: 'viteManifests' }), JSON.stringify(newManifests))
   return newManifests
 }
 
 export async function getOxManifests ({ version }) {
-  const manifests = await client.get(getRedisKey({ version, name: 'oxManifests' }))
+  const manifests = await cache.get(getRedisKey({ version, name: 'oxManifests' }))
   if (manifests) return JSON.parse(manifests)
 
   const viteManifests = await getViteManifests({ version })
   const newManifests = viteToOxManifest(viteManifests)
-  await client.set(getRedisKey({ version, name: 'oxManifests' }), JSON.stringify(newManifests))
+  await cache.set(getRedisKey({ version, name: 'oxManifests' }), JSON.stringify(newManifests))
   return newManifests
 }
 
 export async function getDependencies ({ version }) {
-  const deps = await client.get(getRedisKey({ version, name: 'dependencies' }))
+  const deps = await cache.get(getRedisKey({ version, name: 'dependencies' }))
   if (deps) return JSON.parse(deps)
 
   const viteManifests = await getViteManifests({ version })
   const newDeps = viteManifestToDeps(viteManifests)
-  await client.set(getRedisKey({ version, name: 'dependencies' }), JSON.stringify(newDeps))
+  await cache.set(getRedisKey({ version, name: 'dependencies' }), JSON.stringify(newDeps))
   return newDeps
 }
 
diff --git a/src/redis.js b/src/redis.js
index 8dc4b80..f0c513f 100644
--- a/src/redis.js
+++ b/src/redis.js
@@ -5,6 +5,14 @@ import Queue from 'bull'
 const commonQueueOptions = { enableReadyCheck: false, maxRetriesPerRequest: null }
 
 const createClient = (type, options = {}) => {
+  if (!isEnabled()) {
+    return new Proxy({}, {
+      get () {
+        throw new Error('Redis is disabled. Do not use it.')
+      }
+    })
+  }
+
   const client = new Redis({
     host: process.env.REDIS_HOST,
     port: process.env.REDIS_PORT || 6379,
@@ -64,3 +72,7 @@ export async function closeQueue (name) {
   }
   return queue.close()
 }
+
+export function isEnabled () {
+  return !!process.env.REDIS_HOST
+}
diff --git a/src/routes/health.js b/src/routes/health.js
index f4f26f8..edd0baf 100644
--- a/src/routes/health.js
+++ b/src/routes/health.js
@@ -7,8 +7,10 @@ import { once } from '../util.js'
 const router = Router()
 const healthCheck = new health.HealthChecker()
 
-const redisReady = new health.ReadinessCheck('Redis ready', redis.isReady)
-healthCheck.registerReadinessCheck(redisReady)
+if (redis.isEnabled()) {
+  const redisReady = new health.ReadinessCheck('Redis ready', redis.isReady)
+  healthCheck.registerReadinessCheck(redisReady)
+}
 
 const startupCheck = new health.StartupCheck('check latest version', once(async function () {
   await getLatestVersion()
diff --git a/src/version.js b/src/version.js
index 1c0e38c..4c82230 100644
--- a/src/version.js
+++ b/src/version.js
@@ -2,7 +2,7 @@ import fetch from 'node-fetch'
 import { config } from './config.js'
 import { getRedisKey, hash } from './util.js'
 import { logger } from './logger.js'
-import { client, pubClient } from './redis.js'
+import * as redis from './redis.js'
 
 let latestVersion
 
@@ -34,30 +34,40 @@ export const fetchLatestVersion = async () => {
 export async function getLatestVersion () {
   if (latestVersion) return latestVersion
 
-  const version = await client.get(getRedisKey({ name: 'latestVersion' }))
-  if (version) return (latestVersion = version)
+  if (redis.isEnabled()) {
+    const version = await redis.client.get(getRedisKey({ name: 'latestVersion' }))
+    if (version) return (latestVersion = version)
+  }
 
   const newVersion = await fetchLatestVersion()
-  pubClient.publish(getRedisKey({ name: 'updateLatestVersion' }), newVersion)
-  await client.set(getRedisKey({ name: 'latestVersion' }), newVersion)
+
+  if (redis.isEnabled()) {
+    redis.pubClient.publish(getRedisKey({ name: 'updateLatestVersion' }), newVersion)
+    await redis.client.set(getRedisKey({ name: 'latestVersion' }), newVersion)
+  }
+
   return (latestVersion = newVersion)
 }
 
 export function registerLatestVersionListener (client) {
-  const key = getRedisKey({ name: 'updateLatestVersion' })
-  client.subscribe(key, (errs, count) => logger.info(`Subscribed to ${key}.`))
-  client.on('message', (channel, message) => {
-    if (channel === key) latestVersion = message
-  })
+  if (redis.isEnabled()) {
+    const key = getRedisKey({ name: 'updateLatestVersion' })
+    client.subscribe(key, (errs, count) => logger.info(`Subscribed to ${key}.`))
+    client.on('message', (channel, message) => {
+      if (channel === key) latestVersion = message
+    })
+  } else {
+    setInterval(updateVersionProcessor, Number(process.env.CACHE_TTL))
+  }
 }
 
-export async function updateVersionProcessor (job) {
+export async function updateVersionProcessor () {
   const [storedVersion, fetchedVersion] = await Promise.all([
     getLatestVersion(),
     fetchLatestVersion()
   ])
   if (storedVersion === fetchedVersion) return fetchedVersion
-  pubClient.publish(getRedisKey({ name: 'updateLatestVersion' }), fetchedVersion)
-  await client.set(getRedisKey({ name: 'latestVersion' }), fetchedVersion)
+  redis.pubClient.publish(getRedisKey({ name: 'updateLatestVersion' }), fetchedVersion)
+  await redis.client.set(getRedisKey({ name: 'latestVersion' }), fetchedVersion)
   return fetchedVersion
 }
-- 
GitLab