From 7e7e109cfe36c9b9b6d41e475e8d0e30b4a99ea7 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 3 Oct 2024 11:59:42 -0400 Subject: [PATCH] docker-redis - local refactors Signed-off-by: Dave Richer --- server.js | 116 ++++++++++++++++++++++------------- server/utils/redisHelpers.js | 91 ++++++++++++++++----------- 2 files changed, 128 insertions(+), 79 deletions(-) diff --git a/server.js b/server.js index e0bdd0a2e..10182fe18 100644 --- a/server.js +++ b/server.js @@ -1,27 +1,30 @@ -const express = require("express"); const cors = require("cors"); -const bodyParser = require("body-parser"); const path = require("path"); +const http = require("http"); +const Redis = require("ioredis"); +const express = require("express"); +const bodyParser = require("body-parser"); const compression = require("compression"); const cookieParser = require("cookie-parser"); -const http = require("http"); const { Server } = require("socket.io"); -// const { createClient } = require("redis"); -const Redis = require("ioredis"); const { createAdapter } = require("@socket.io/redis-adapter"); -const logger = require("./server/utils/logger"); -const { redisSocketEvents } = require("./server/web-sockets/redisSocketEvents"); -const { instrument, RedisStore } = require("@socket.io/admin-ui"); - +const { instrument } = require("@socket.io/admin-ui"); const { isString, isEmpty } = require("lodash"); + +const logger = require("./server/utils/logger"); const applyRedisHelpers = require("./server/utils/redisHelpers"); const applyIOHelpers = require("./server/utils/ioHelpers"); +const { redisSocketEvents } = require("./server/web-sockets/redisSocketEvents"); // Load environment variables require("dotenv").config({ path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) }); +const CLUSTER_RETRY_BASE_DELAY = 100; +const CLUSTER_RETRY_MAX_DELAY = 5000; +const CLUSTER_RETRY_JITTER = 100; + /** * CORS Origin for Socket.IO * @type {string[][]} @@ -50,11 +53,11 @@ const SOCKETIO_CORS_ORIGIN = [ "https://old.imex.online", "https://www.old.imex.online", "https://wsadmin.imex.online", - "https://www.wsadmin.imex.online", - "http://localhost:3333", - "https://localhost:3333" + "https://www.wsadmin.imex.online" ]; +const SOCKETIO_CORS_ORIGIN_DEV = ["http://localhost:3333", "https://localhost:3333"]; + /** * Middleware for Express app * @param app @@ -103,29 +106,52 @@ const applyRoutes = (app) => { }); }; +/** + * Connect to Redis Cluster + * @returns {Promise} + */ const connectToRedisCluster = () => { - const redisCluster = new Redis.Cluster( - process.env.REDIS_URL - ? JSON.parse(process.env.REDIS_URL) - : [ - { - host: "localhost", - port: 6379 - } - ], - { - clusterRetryStrategy: function (times) { - const delay = Math.min(100 + times * 50, 2000); - logger.log( - `[${process.env.NODE_ENV}] Redis cluster not yet ready. Retrying in ${delay}ms`, - "ERROR", - "redis", - "api" - ); - return delay; - } + if (isEmpty(process.env?.REDIS_URL) || !isString(process.env?.REDIS_URL)) { + logger.log(`[${process.env.NODE_ENV}] No or Malformed REDIS_URL present.`, "ERROR", "redis", "api"); + process.exit(1); + } + + let redisServers; + + try { + redisServers = JSON.parse(process.env.REDIS_URL); + } catch (error) { + logger.log( + `[${process.env.NODE_ENV}] Failed to parse REDIS_URL: ${error.message}. Exiting...`, + "ERROR", + "redis", + "api" + ); + process.exit(1); + } + + const clusterRetryStrategy = (times) => { + const delay = + Math.min(CLUSTER_RETRY_BASE_DELAY + times * 50, CLUSTER_RETRY_MAX_DELAY) + Math.random() * CLUSTER_RETRY_JITTER; + logger.log( + `[${process.env.NODE_ENV}] Redis cluster not yet ready. Retrying in ${delay.toFixed(2)}ms`, + "ERROR", + "redis", + "api" + ); + return delay; + }; + + const redisCluster = new Redis.Cluster(redisServers, { + clusterRetryStrategy, + enableAutoPipelining: true, + enableReadyCheck: true, + redisOptions: { + // connectTimeout: 10000, // Timeout for connecting in ms + // idleTimeoutMillis: 30000, // Close idle connections after 30s + // maxRetriesPerRequest: 5 // Retry a maximum of 5 times per request } - ); + }); return new Promise((resolve, reject) => { redisCluster.on("ready", () => { @@ -171,9 +197,11 @@ const applySocketIO = async (server, app) => { const ioRedis = new Server(server, { path: "/wss", adapter: createAdapter(pubClient, subClient), - store: new RedisStore(pubClient, subClient), cors: { - origin: SOCKETIO_CORS_ORIGIN, + origin: + process.env?.NODE_ENV === "development" + ? [...SOCKETIO_CORS_ORIGIN, ...SOCKETIO_CORS_ORIGIN_DEV] + : SOCKETIO_CORS_ORIGIN, methods: ["GET", "POST"], credentials: true, exposedHeaders: ["set-cookie"] @@ -188,6 +216,7 @@ const applySocketIO = async (server, app) => { username: "admin", password: process.env.REDIS_ADMIN_PASS }, + mode: process.env.REDIS_ADMIN_MODE || "development" }); } @@ -202,19 +231,22 @@ const applySocketIO = async (server, app) => { } }); + const api = { + pubClient, + io, + ioRedis, + redisCluster + }; + app.use((req, res, next) => { - Object.assign(req, { - pubClient, - io, - ioRedis - }); + Object.assign(req, api); next(); }); - Object.assign(module.exports, { io, pubClient, ioRedis }); + Object.assign(module.exports, api); - return { pubClient, io, ioRedis }; + return api; }; /** diff --git a/server/utils/redisHelpers.js b/server/utils/redisHelpers.js index c96e7454f..3ab824be4 100644 --- a/server/utils/redisHelpers.js +++ b/server/utils/redisHelpers.js @@ -1,8 +1,8 @@ -const logger = require("./logger"); /** * Apply Redis helper functions * @param pubClient * @param app + * @param logger */ const applyRedisHelpers = (pubClient, app, logger) => { // Store session data in Redis @@ -60,7 +60,7 @@ const applyRedisHelpers = (pubClient, app, logger) => { // Use Redis multi/pipeline to batch the commands const multi = pubClient.multi(); keyValueArray.forEach(([key, value]) => { - multi.hSet(`socket:${socketId}`, key, JSON.stringify(value)); + multi.hset(`socket:${socketId}`, key, JSON.stringify(value)); }); await multi.exec(); // Execute all queued commands } catch (error) { @@ -143,70 +143,87 @@ const applyRedisHelpers = (pubClient, app, logger) => { next(); }); - // // Demo to show how all the helper functions work + // Demo to show how all the helper functions work // const demoSessionData = async () => { // const socketId = "testSocketId"; // - // // Store session data using setSessionData - // await exports.setSessionData(socketId, "field1", "Hello, Redis!"); - // - // // Retrieve session data using getSessionData - // const field1Value = await exports.getSessionData(socketId, "field1"); + // // 1. Test setSessionData and getSessionData + // await setSessionData(socketId, "field1", "Hello, Redis!"); + // const field1Value = await getSessionData(socketId, "field1"); // console.log("Retrieved single field value:", field1Value); // - // // Store multiple session data using setMultipleSessionData - // await exports.setMultipleSessionData(socketId, { field2: "Second Value", field3: "Third Value" }); - // - // // Retrieve multiple session data using getMultipleSessionData - // const multipleFields = await exports.getMultipleSessionData(socketId, ["field2", "field3"]); + // // 2. Test setMultipleSessionData and getMultipleSessionData + // await setMultipleSessionData(socketId, { field2: "Second Value", field3: "Third Value" }); + // const multipleFields = await getMultipleSessionData(socketId, ["field2", "field3"]); // console.log("Retrieved multiple field values:", multipleFields); // - // // Store multiple session data using setMultipleFromArraySessionData - // await exports.setMultipleFromArraySessionData(socketId, [ + // // 3. Test setMultipleFromArraySessionData + // await setMultipleFromArraySessionData(socketId, [ // ["field4", "Fourth Value"], // ["field5", "Fifth Value"] // ]); // // // Retrieve and log all fields - // const allFields = await exports.getMultipleSessionData(socketId, [ - // "field1", - // "field2", - // "field3", - // "field4", - // "field5" - // ]); + // const allFields = await getMultipleSessionData(socketId, ["field1", "field2", "field3", "field4", "field5"]); // console.log("Retrieved all field values:", allFields); // + // // 4. Test list functions // // Add item to the end of a Redis list - // await exports.addItemToEndOfList(socketId, "logEvents", { event: "Log Event 1", timestamp: new Date() }); - // await exports.addItemToEndOfList(socketId, "logEvents", { event: "Log Event 2", timestamp: new Date() }); + // await addItemToEndOfList(socketId, "logEvents", { event: "Log Event 1", timestamp: new Date() }); + // await addItemToEndOfList(socketId, "logEvents", { event: "Log Event 2", timestamp: new Date() }); // // // Add item to the beginning of a Redis list - // await exports.addItemToBeginningOfList(socketId, "logEvents", { event: "First Log Event", timestamp: new Date() }); + // await addItemToBeginningOfList(socketId, "logEvents", { event: "First Log Event", timestamp: new Date() }); // - // // Retrieve the entire list (using lRange) - // const logEvents = await pubClient.lRange(`socket:${socketId}:logEvents`, 0, -1); - // console.log("Log Events List:", logEvents.map(JSON.parse)); + // // Retrieve the entire list + // const logEventsData = await pubClient.lrange(`socket:${socketId}:logEvents`, 0, -1); + // const logEvents = logEventsData.map((item) => JSON.parse(item)); + // console.log("Log Events List:", logEvents); // - // // **Add the new code below to test clearList** - // - // // Clear the list using clearList - // await exports.clearList(socketId, "logEvents"); + // // 5. Test clearList + // await clearList(socketId, "logEvents"); // console.log("Log Events List cleared."); // // // Retrieve the list after clearing to confirm it's empty - // const logEventsAfterClear = await pubClient.lRange(`socket:${socketId}:logEvents`, 0, -1); + // const logEventsAfterClear = await pubClient.lrange(`socket:${socketId}:logEvents`, 0, -1); // console.log("Log Events List after clearing:", logEventsAfterClear); // Should be an empty array // - // // Clear session data - // await exports.clearSessionData(socketId); + // // 6. Test clearSessionData + // await clearSessionData(socketId); // console.log("Session data cleared."); - // }; // + // // 7. Test room functions + // const roomName = "testRoom"; + // const user1 = { id: 1, name: "Alice" }; + // const user2 = { id: 2, name: "Bob" }; + // + // // Add users to room + // await addUserToRoom(roomName, user1); + // await addUserToRoom(roomName, user2); + // + // // Get users in room + // const usersInRoom = await getUsersInRoom(roomName); + // console.log(`Users in room ${roomName}:`, usersInRoom); + // + // // Remove a user from room + // await removeUserFromRoom(roomName, user1); + // + // // Get users in room after removal + // const usersInRoomAfterRemoval = await getUsersInRoom(roomName); + // console.log(`Users in room ${roomName} after removal:`, usersInRoomAfterRemoval); + // + // // Clean up: remove remaining users from room + // await removeUserFromRoom(roomName, user2); + // + // // Verify room is empty + // const usersInRoomAfterCleanup = await getUsersInRoom(roomName); + // console.log(`Users in room ${roomName} after cleanup:`, usersInRoomAfterCleanup); // Should be empty + // }; // if (process.env.NODE_ENV === "development") { // demoSessionData(); // } - // "th1s1sr3d1s" (BCrypt) + return api; }; module.exports = applyRedisHelpers; +// "th1s1sr3d1s" (BCrypt)