Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@uppy/companion: switch from node-redis to ioredis #4623

Merged
merged 12 commits into from May 13, 2024

Conversation

dschmidt
Copy link
Contributor

Followup for #4597

This replaces node-redis with the (apparently) more modern ioredis. I haven't found much resources on whether one should pick one or the other, but ioredis supports Redis Sentinel natively (which I need) while node-redis does not.

It's supposed to be faster for certain use cases:
https://ably.com/blog/migrating-from-node-redis-to-ioredis
https://www.reddit.com/r/node/comments/uymb7w/redis_vs_ioredis/
https://github.com/poppinlp/node_redis-vs-ioredis

https://docs.redis.com/latest/rs/references/client_references/client_ioredis/
Both libs are "community recommended" ... for whatever that means.

This is a breaking change as config options are not fully compatible.
If someone passes in redisOptions in middleware mode (in standalone mode it wasn't possible until recently, see #4597) the behavior might change/break...
If you want to have this, it's probably for the next major release

Not sure it would be worth introducing an abstraction and support both. connect-redis does some sort of basic abstraction, but they would not accept patches to abstract more api away (I've asked)

Closes #4571

@socket-security

This comment was marked as resolved.

@arturi arturi added the 4.0 For the 4.0 major version label Aug 14, 2023
@dschmidt
Copy link
Contributor Author

@arturi Is there any roadmap for 4.0?
Any rough eta on when you want to release that? Depending on that it might actually be worth to support ioredis and node-redis at the same time for now...

@arturi
Copy link
Contributor

arturi commented Aug 14, 2023

@dschmidt we were just discussing it, and it could be that new major for Companion will happen in around 2 months.

@arturi arturi mentioned this pull request Oct 25, 2023
35 tasks
@aduh95 aduh95 changed the base branch from main to 4.x April 16, 2024 15:12
@aduh95 aduh95 added the safe to test Add this label on trustworthy PRs to spawn the e2e test suite label May 9, 2024
Copy link
Contributor

github-actions bot commented May 9, 2024

Diff output files
diff --git a/packages/@uppy/companion/lib/server/Uploader.d.ts b/packages/@uppy/companion/lib/server/Uploader.d.ts
index 5d9a222..ae38c05 100644
--- a/packages/@uppy/companion/lib/server/Uploader.d.ts
+++ b/packages/@uppy/companion/lib/server/Uploader.d.ts
@@ -23,7 +23,7 @@ declare class Uploader {
         size: any;
         companionOptions: any;
         pathPrefix: string;
-        storage: any;
+        storage: import("ioredis").default;
         s3: {
             client: any;
             options: any;
@@ -69,6 +69,8 @@ declare class Uploader {
         useFormData?: boolean;
         chunkSize?: number;
     });
+    /** @type {import('ioredis').Redis} */
+    storage: import('ioredis').Redis;
     options: {
         endpoint: string;
         uploadUrl?: string;
@@ -89,7 +91,6 @@ declare class Uploader {
     fileName: string;
     size: number;
     uploadFileName: any;
-    storage: any;
     downloadedBytes: number;
     readStream: import("stream").Readable | fs.ReadStream;
     abortReadStream(err: any): void;
diff --git a/packages/@uppy/companion/lib/server/Uploader.js b/packages/@uppy/companion/lib/server/Uploader.js
index aed14e0..27a21f1 100644
--- a/packages/@uppy/companion/lib/server/Uploader.js
+++ b/packages/@uppy/companion/lib/server/Uploader.js
@@ -134,6 +134,8 @@ class StreamableBlob {
   }
 }
 class Uploader {
+  /** @type {import('ioredis').Redis} */
+  storage;
   /**
    * Uploads file to destination based on the supplied protocol (tus, s3-multipart, multipart)
    * For tus uploads, the deferredLength option is enabled, because file size value can be unreliable
@@ -401,9 +403,7 @@ class Uploader {
     // https://github.com/transloadit/uppy/issues/3748
     const keyExpirySec = 60 * 60 * 24;
     const redisKey = `${Uploader.STORAGE_PREFIX}:${this.token}`;
-    this.storage.set(redisKey, jsonStringify(state), {
-      EX: keyExpirySec,
-    });
+    this.storage.set(redisKey, jsonStringify(state), "EX", keyExpirySec);
   }
   throttledEmitProgress = throttle(
     (dataToEmit) => {
diff --git a/packages/@uppy/companion/lib/server/emitter/redis-emitter.d.ts b/packages/@uppy/companion/lib/server/emitter/redis-emitter.d.ts
index ba7a709..0d502d9 100644
--- a/packages/@uppy/companion/lib/server/emitter/redis-emitter.d.ts
+++ b/packages/@uppy/companion/lib/server/emitter/redis-emitter.d.ts
@@ -1,4 +1,4 @@
-declare function _exports(redisClient: any, redisPubSubScope: any): {
+declare function _exports(redisClient: import('ioredis').Redis, redisPubSubScope: string): {
     on: (eventName: string, handler: any) => void | EventEmitter<[never]>;
     off: (eventName: string, handler: any) => Promise<void> | EventEmitter<[never]>;
     once: (eventName: string, handler: any) => void | EventEmitter<[never]>;
@@ -7,3 +7,4 @@ declare function _exports(redisClient: any, redisPubSubScope: any): {
     removeAllListeners: (eventName: string) => Promise<void> | EventEmitter<[never]>;
 };
 export = _exports;
+import { EventEmitter } from "events";
diff --git a/packages/@uppy/companion/lib/server/emitter/redis-emitter.js b/packages/@uppy/companion/lib/server/emitter/redis-emitter.js
index 5612124..aa4f768 100644
--- a/packages/@uppy/companion/lib/server/emitter/redis-emitter.js
+++ b/packages/@uppy/companion/lib/server/emitter/redis-emitter.js
@@ -6,16 +6,21 @@ const logger = require("../logger");
  * This module simulates the builtin events.EventEmitter but with the use of redis.
  * This is useful for when companion is running on multiple instances and events need
  * to be distributed across.
+ *
+ * @param {import('ioredis').Redis} redisClient
+ * @param {string} redisPubSubScope
+ * @returns
  */
 module.exports = (redisClient, redisPubSubScope) => {
   const prefix = redisPubSubScope ? `${redisPubSubScope}:` : "";
   const getPrefixedEventName = (eventName) => `${prefix}${eventName}`;
-  const publisher = redisClient.duplicate();
-  publisher.on("error", err => logger.error("publisher redis error", err));
+  const publisher = redisClient.duplicate({ lazyConnect: true });
+  publisher.on("error", err => logger.error("publisher redis error", err.toString()));
+  /** @type {import('ioredis').Redis} */
   let subscriber;
   const connectedPromise = publisher.connect().then(() => {
     subscriber = publisher.duplicate();
-    subscriber.on("error", err => logger.error("subscriber redis error", err));
+    subscriber.on("error", err => logger.error("subscriber redis error", err.toString()));
     return subscriber.connect();
   });
   const handlersByEvent = new Map();
@@ -53,11 +58,20 @@ module.exports = (redisClient, redisPubSubScope) => {
       if (handlersByThisEventName.size === 0) {
         handlersByEvent.delete(eventName);
       }
-      return subscriber.pUnsubscribe(getPrefixedEventName(eventName), actualHandler);
+      subscriber.off("pmessage", actualHandler);
+      return subscriber.punsubscribe(getPrefixedEventName(eventName));
     });
   }
+  /**
+   * @param {string} eventName
+   * @param {*} handler
+   * @param {*} _once
+   */
   function addListener(eventName, handler, _once = false) {
-    function actualHandler(message) {
+    function actualHandler(pattern, channel, message) {
+      if (pattern !== getPrefixedEventName(eventName)) {
+        return;
+      }
       if (_once) {
         removeListener(eventName, handler);
       }
@@ -65,9 +79,10 @@ module.exports = (redisClient, redisPubSubScope) => {
       try {
         args = JSON.parse(message);
       } catch (ex) {
-        return handleError(new Error(`Invalid JSON received! Channel: ${eventName} Message: ${message}`));
+        handleError(new Error(`Invalid JSON received! Channel: ${eventName} Message: ${message}`));
+        return;
       }
-      return handler(...args);
+      handler(...args);
     }
     let handlersByThisEventName = handlersByEvent.get(eventName);
     if (handlersByThisEventName == null) {
@@ -75,7 +90,10 @@ module.exports = (redisClient, redisPubSubScope) => {
       handlersByEvent.set(eventName, handlersByThisEventName);
     }
     handlersByThisEventName.set(handler, actualHandler);
-    runWhenConnected(() => subscriber.pSubscribe(getPrefixedEventName(eventName), actualHandler));
+    runWhenConnected(() => {
+      subscriber.on("pmessage", actualHandler);
+      return subscriber.psubscribe(getPrefixedEventName(eventName));
+    });
   }
   /**
    * Add an event listener
@@ -129,7 +147,7 @@ module.exports = (redisClient, redisPubSubScope) => {
     }
     return runWhenConnected(() => {
       handlersByEvent.delete(eventName);
-      return subscriber.pUnsubscribe(getPrefixedEventName(eventName));
+      return subscriber.punsubscribe(getPrefixedEventName(eventName));
     });
   }
   return {
diff --git a/packages/@uppy/companion/lib/server/redis.d.ts b/packages/@uppy/companion/lib/server/redis.d.ts
index ba5aa6f..b4637d1 100644
--- a/packages/@uppy/companion/lib/server/redis.d.ts
+++ b/packages/@uppy/companion/lib/server/redis.d.ts
@@ -1 +1,6 @@
-export function client(companionOptions: any): any;
+export function client({ redisUrl, redisOptions }?: {
+    redisUrl: any;
+    redisOptions: any;
+}): Redis;
+import Redis_1 = require("ioredis/built/Redis");
+import Redis = Redis_1.default;
diff --git a/packages/@uppy/companion/lib/server/redis.js b/packages/@uppy/companion/lib/server/redis.js
index 14e804f..13ebd5a 100644
--- a/packages/@uppy/companion/lib/server/redis.js
+++ b/packages/@uppy/companion/lib/server/redis.js
@@ -1,37 +1,30 @@
 "use strict";
 Object.defineProperty(exports, "__esModule", { value: true });
-const redis = require("redis");
+const Redis = require("ioredis").default;
 const logger = require("./logger");
+/** @type {import('ioredis').Redis} */
 let redisClient;
 /**
  * A Singleton module that provides a single redis client through out
  * the lifetime of the server
  *
- * @param {{ redisUrl?: string, redisOptions?: Record<string, any> }} [companionOptions] options
+ * @param {string} [redisUrl] ioredis url
+ * @param {Record<string, any>} [redisOptions] ioredis client options
  */
-function createClient(companionOptions) {
+function createClient(redisUrl, redisOptions) {
   if (!redisClient) {
-    const { redisUrl, redisOptions } = companionOptions;
-    redisClient = redis.createClient({
-      ...redisOptions,
-      ...(redisUrl && { url: redisUrl }),
-    });
-    redisClient.on("error", err => logger.error("redis error", err));
-    (async () => {
-      try {
-        // fire and forget.
-        // any requests made on the client before connection is established will be auto-queued by node-redis
-        await redisClient.connect();
-      } catch (err) {
-        logger.error(err.message, "redis.error");
-      }
-    })();
+    if (redisUrl) {
+      redisClient = new Redis(redisUrl, redisOptions);
+    } else {
+      redisClient = new Redis(redisOptions);
+    }
+    redisClient.on("error", err => logger.error("redis error", err.toString()));
   }
   return redisClient;
 }
-module.exports.client = (companionOptions) => {
-  if (!companionOptions?.redisUrl && !companionOptions?.redisOptions) {
+module.exports.client = ({ redisUrl, redisOptions } = { redisUrl: undefined, redisOptions: undefined }) => {
+  if (!redisUrl && !redisOptions) {
     return redisClient;
   }
-  return createClient(companionOptions);
+  return createClient(redisUrl, redisOptions);
 };
diff --git a/packages/@uppy/companion/lib/standalone/helper.js b/packages/@uppy/companion/lib/standalone/helper.js
index 9c21b17..7731990 100644
--- a/packages/@uppy/companion/lib/standalone/helper.js
+++ b/packages/@uppy/companion/lib/standalone/helper.js
@@ -143,9 +143,9 @@ const getConfigFromEnv = () => {
       ? parseInt(process.env.COMPANION_PERIODIC_PING_COUNT, 10)
       : undefined,
     filePath: process.env.COMPANION_DATADIR,
-    redisUrl: process.env.COMPANION_REDIS_URL,
     redisPubSubScope: process.env.COMPANION_REDIS_PUBSUB_SCOPE,
-    //  redisOptions refers to https://www.npmjs.com/package/redis#options-object-properties
+    redisUrl: process.env.COMPANION_REDIS_URL,
+    //  redisOptions refers to https://redis.github.io/ioredis/index.html#RedisOptions
     redisOptions: (() => {
       try {
         if (!process.env.COMPANION_REDIS_OPTIONS) {

@github-actions github-actions bot removed the safe to test Add this label on trustworthy PRs to spawn the e2e test suite label May 9, 2024
Copy link

socket-security bot commented May 9, 2024

👍 Dependency issues cleared. Learn more about Socket for GitHub ↗︎

This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored.

View full report↗︎

probably from borked merge
@mifi mifi added the safe to test Add this label on trustworthy PRs to spawn the e2e test suite label May 9, 2024
@github-actions github-actions bot removed the safe to test Add this label on trustworthy PRs to spawn the e2e test suite label May 9, 2024
@mifi
Copy link
Contributor

mifi commented May 9, 2024

@dschmidt is this a typo?

      subscriber.off('message', actualHandler)

shouldn't it instead be

      subscriber.off('pmessage', actualHandler)

@dschmidt
Copy link
Contributor Author

dschmidt commented May 9, 2024

I don't recall from the top of my head, but it pretty much looks like

@mifi mifi added the safe to test Add this label on trustworthy PRs to spawn the e2e test suite label May 9, 2024
@github-actions github-actions bot removed pending end-to-end tests safe to test Add this label on trustworthy PRs to spawn the e2e test suite labels May 9, 2024
@mifi mifi requested a review from Murderlon May 9, 2024 16:40
Copy link
Member

@Murderlon Murderlon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any docs changes needed? Do we have e2e tests covering this?

@mifi
Copy link
Contributor

mifi commented May 13, 2024

end to end tests initially broke and I then fixed the bug, so i would say that the e2e tests cover redis if that's what you were thinking about.

COMPANION_REDIS_OPTIONS is different now, because the options are passed to a different library. but I see that it's not even documented before this PR. I will add it

@mifi mifi added the safe to test Add this label on trustworthy PRs to spawn the e2e test suite label May 13, 2024
@github-actions github-actions bot removed the safe to test Add this label on trustworthy PRs to spawn the e2e test suite label May 13, 2024
@Murderlon Murderlon changed the title Switch to ioredis @uppy/companion: switch from node-redis to ioredis May 13, 2024
@mifi mifi merged commit 4890281 into transloadit:4.x May 13, 2024
23 of 24 checks passed
@dschmidt
Copy link
Contributor Author

🎉 🎉 🎉

Awesome! Thank you so much for bringing this over the finish line, @mifi <3

@mifi
Copy link
Contributor

mifi commented May 13, 2024

thank you too!

github-actions bot added a commit that referenced this pull request May 14, 2024
| Package                |      Version | Package                |      Version |
| ---------------------- | ------------ | ---------------------- | ------------ |
| @uppy/companion        | 5.0.0-beta.6 | @uppy/status-bar       | 4.0.0-beta.7 |
| @uppy/companion-client | 4.0.0-beta.6 | @uppy/unsplash         | 4.0.0-beta.6 |
| @uppy/compressor       | 2.0.0-beta.7 | @uppy/url              | 4.0.0-beta.6 |
| @uppy/core             | 4.0.0-beta.7 | @uppy/utils            | 6.0.0-beta.6 |
| @uppy/dashboard        | 4.0.0-beta.7 | @uppy/webcam           | 4.0.0-beta.6 |
| @uppy/dropbox          | 4.0.0-beta.6 | @uppy/xhr-upload       | 4.0.0-beta.4 |
| @uppy/image-editor     | 3.0.0-beta.4 | uppy                   | 4.0.0-beta.7 |
| @uppy/screen-capture   | 4.0.0-beta.5 |                        |              |

- @uppy/companion: switch from `node-redis` to `ioredis` (Dominik Schmidt / #4623)
- meta: Fix headings in xhr.mdx (Merlijn Vos)
- @uppy/xhr-upload: introduce hooks similar to tus (Merlijn Vos / #5094)
- @uppy/core: close->destroy, clearUploadedFiles->clear (Merlijn Vos / #5154)
- @uppy/companion-client,@uppy/dropbox,@uppy/screen-capture,@uppy/unsplash,@uppy/url,@uppy/webcam: Use `title` consistently from locales (Merlijn Vos / #5134)




| Package            | Version | Package            | Version |
| ------------------ | ------- | ------------------ | ------- |
| @uppy/core         |  3.11.3 | uppy               |  3.25.3 |
| @uppy/image-editor |   2.4.6 |                    |         |

- @uppy/image-editor: fix tooltips (Avneet Singh Malhotra / #5156)
- meta: Remove redundant `plugins` prop from examples (Merlijn Vos / #5145)
- @uppy/image-editor: Remove `target` option from examples and document consistently (Merlijn Vos / #5146)
- @uppy/core: make getObjectOfFilesPerState more efficient (Merlijn Vos / #5155)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
4.0 For the 4.0 major version
Projects
None yet
Development

Successfully merging this pull request may close these issues.

@uppy/companion: support redis sentinel
5 participants