From 1394a250e67332fcc112a7ea79fc10d942d04434 Mon Sep 17 00:00:00 2001
From: Dominik Hebeler <dominik@hebeler.club>
Date: Sun, 5 Mar 2023 21:37:30 +0100
Subject: [PATCH] added cronjob

---
 chart/templates/deployment.yaml | 26 +++++++++++++++++++
 docker-compose.yml              | 12 +++++++++
 pass/app.js                     | 45 +++++++++++++++++++++++----------
 pass/bin/cron                   | 33 +++++++++++++++---------
 4 files changed, 91 insertions(+), 25 deletions(-)

diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml
index a68d247..58dd2e0 100644
--- a/chart/templates/deployment.yaml
+++ b/chart/templates/deployment.yaml
@@ -63,6 +63,32 @@ spec:
               port: http
           resources:
             {{- toYaml .Values.resources | nindent 12 }}
+        - name: {{ .Chart.Name }}-cron
+          securityContext:
+            {{- toYaml .Values.securityContext | nindent 12 }}
+          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
+          imagePullPolicy: {{ .Values.image.pullPolicy }}
+          command: ["/usr/local/bin/node"]
+          args: ["./bin/cron"]
+          volumeMounts:
+            - name: order-data
+              mountPath: /data
+          {{- if .Values.application.secretName }}
+            - name: application-secret
+              readOnly: true
+              mountPath: /app/config/production.json
+              subPath: production.json
+          {{- end }}
+          livenessProbe:
+            httpGet:
+              path: /healthz/cron
+              port: 3000
+          readinessProbe:
+            httpGet:
+              path: /healthz/cron
+              port: 3000
+          resources:
+            {{- toYaml .Values.resources | nindent 12 }}
       {{- with .Values.nodeSelector }}
       nodeSelector:
         {{- toYaml . | nindent 8 }}
diff --git a/docker-compose.yml b/docker-compose.yml
index 9afce28..efd54ee 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -27,6 +27,18 @@ services:
     ports:
       - 8085:3000
       - 9229:9229
+  express_cron:
+    build:
+      context: ./build/pass
+      target: development
+    entrypoint: "/usr/local/bin/node"
+    command: "./bin/cron"
+    networks:
+      - metager
+    working_dir: /app
+    volumes:
+      - ./pass:/app
+      - mgpassdata:/data
   express_redis:
     build:
       context: ./build/redis
diff --git a/pass/app.js b/pass/app.js
index 1aeec96..4beb019 100644
--- a/pass/app.js
+++ b/pass/app.js
@@ -5,6 +5,7 @@ var cookieParser = require("cookie-parser");
 var logger = require("morgan");
 
 var indexRouter = require("./routes/index");
+const RedisClient = require("./app/RedisClient");
 
 var app = express();
 
@@ -17,9 +18,13 @@ if (process.env.NODE_ENV === "development") {
   app.use(logger("dev"));
 } else {
   // Do not log successful requests in production
-  app.use(logger("dev", {
-    skip: (req, res) => { return res.statusCode < 400 }
-  }));
+  app.use(
+    logger("dev", {
+      skip: (req, res) => {
+        return res.statusCode < 400;
+      },
+    })
+  );
 }
 app.use(express.json());
 app.use(express.urlencoded({ extended: false, limit: "1mb" }));
@@ -28,24 +33,38 @@ app.use(cookieParser());
 // Healthcheck URL
 app.get("/healthz", (req, res) => {
   res.status(200);
-  res.header({ "Content-Type": "application/json" })
-  res.send(JSON.stringify({ "status": "OK" }));
-})
+  res.header({ "Content-Type": "application/json" });
+  res.send(JSON.stringify({ status: "OK" }));
+});
+
+app.get("/healthz/cron", async (req, res) => {
+  let redis_client = RedisClient.CLIENT();
+  let live = await redis_client.get("cron_liveness");
+
+  if (live !== null) {
+    res.json({ message: "OK" });
+  } else {
+    res.status(404).json({ message: "DOWN" });
+  }
+});
 
 app.use((req, res, next) => {
   res.locals.baseDir = "";
   let subPath = req.url.match(/^((\/.*)?\/keys)/);
-  let allowed_hosts = [
-    "localhost",
-    "metager.de",
-    "metager.org"
-  ];
+  let allowed_hosts = ["localhost", "metager.de", "metager.org"];
 
-  if (allowed_hosts.includes(req.hostname) || (process.env.NODE_ENV === "development" && req.hostname.match(/^(localhost|.*\.ngrok\.io|.*\.review\.metager\.de)$/))) {
+  if (
+    allowed_hosts.includes(req.hostname) ||
+    (process.env.NODE_ENV === "development" &&
+      req.hostname.match(/^(localhost|.*\.ngrok\.io|.*\.review\.metager\.de)$/))
+  ) {
     let proto = req.get("x-forwarded-proto") ?? req.protocol;
     let host = req.get("x-forwarded-host") ?? req.get("host");
     let port = req.get("x-forwarded-port");
-    res.locals.baseDir = `${proto}://${host}` + (port ? `:${port}` : '') + (subPath ? subPath[1] : "");
+    res.locals.baseDir =
+      `${proto}://${host}` +
+      (port ? `:${port}` : "") +
+      (subPath ? subPath[1] : "");
   }
   next();
 });
diff --git a/pass/bin/cron b/pass/bin/cron
index 81e89bc..763c829 100644
--- a/pass/bin/cron
+++ b/pass/bin/cron
@@ -1,6 +1,7 @@
+const dayjs = require("dayjs");
 const Order = require("../app/Order");
 const RedisClient = require("../app/RedisClient");
-
+let redis_client;
 /**
  * Will call every cron script every minute.
  * The cron script itself will make sure it executes in the desired
@@ -10,19 +11,30 @@ const RedisClient = require("../app/RedisClient");
  */
 
 let cronjobs = async () => {
-  await writeLogsToOrder();
+  let now = dayjs();
+  console.log(`[${now.format("YYYY-MM-DD HH:mm:ss")}] Start`);
+  redis_client = RedisClient.CLIENT();
+  await redis_client.setex("cron_liveness", 70, 1);
+  let written_logs = await writeLogsToOrder();
+  console.log(
+    `[${now.format(
+      "YYYY-MM-DD HH:mm:ss"
+    )}] Written ${written_logs} key changes.`
+  );
+  await redis_client.quit();
+  console.log(`[${now.format("YYYY-MM-DD HH:mm:ss")}] Finish`);
 };
+console.log("Start");
 let interval = setInterval(cronjobs, 60000);
 cronjobs();
 
 async function writeLogsToOrder() {
-  let redis_client = RedisClient.CLIENT();
   let redis_lock_key = "cron:writeLogsToOrder";
   let interval_seconds = 60; // Will execute every minute
 
   let lock = await tryToGetLock(redis_lock_key, interval_seconds);
   if (!lock) {
-    return;
+    return 0;
   }
   // Start gathering changes in keys; do it in packets of 1000
   return redis_client
@@ -45,24 +57,25 @@ async function writeLogsToOrder() {
       return key_changes;
     })
     .then(async (key_changes) => {
+      let written_changes = 0;
       for (let order_id in key_changes) {
         /**
          * @type {Order}
          */
         let loaded_order = await Order.LOAD_ORDER_FROM_ID(order_id);
         await loaded_order.getWriteLock();
+        written_changes += key_changes[order_id].length;
         loaded_order.addOrderKeyChanges(key_changes[order_id]);
         await loaded_order.save();
         await loaded_order.releaseWriteLock().catch((reason) => {
           console.error(reason);
         });
       }
-    })
-    .then(() => redis_client.quit());
+      return written_changes;
+    });
 }
 
 async function tryToGetLock(redis_lock_key, interval_seconds) {
-  let redis_client = RedisClient.CLIENT();
   let redis_lock = await redis_client
     .pipeline()
     .setnx(redis_lock_key, 1)
@@ -76,19 +89,15 @@ async function tryToGetLock(redis_lock_key, interval_seconds) {
     redis_lock[2][0] !== null
   ) {
     console.error("Error while getting redis lock for cronjob");
-    await redis_client.quit();
     return false;
   }
 
   if (redis_lock[0][1] === 1) {
     // Got Lock successfully
-    await redis_client.quit();
     return true;
   } else {
     // Did not get lock. Reset expiretime to previous value
-    await redis_client
-      .expireat(redis_lock_key, redis_lock[1][1])
-      .then(() => redis_client.quit());
+    await redis_client.expireat(redis_lock_key, redis_lock[1][1]);
     return false;
   }
 }
-- 
GitLab