diff --git a/.npmrc b/.npmrc index cffe8cd..43c97e7 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1 @@ -save-exact=true +package-lock=false diff --git a/README.md b/README.md index e534129..9e98c10 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,6 @@ Monthly download on NPM - - Vulnerabilities on Snyk -

@@ -40,7 +37,7 @@ ## 🚦 Current Status -This package is currently under development and should be consider **ALPHA** in terms of state. I/We are currently accepting contributions and/or dedicated contributors to help develop and maintain this package. +This package is currently maintained and should be considered **Stable/GA** in terms of state. I/We are currently accepting contributions and/or dedicated contributors to help develop and maintain this package. For more information on contributing please see [the contrib message below](#contributing). @@ -48,13 +45,14 @@ For more information on contributing please see [the contrib message below](#con This package's lead maintainer is an employee of Strapi however this package is not officially maintained by Strapi Solutions SAS nor Strapi, Inc. and is currently maintained in the free time of the lead maintainer. -**Absolutely no part of this code should be considered covered under any agreement you have with Strapi proper** including but not limited to any Enterprise Agreement you have with Strapi. +**Absolutely no part of this code should be considered covered under any agreement you have with Strapi proper** including but not limited to any Enterprise and/or Cloud Agreement you have with Strapi. ## ✨ Features -This plugin utilizes 1 core package: +This plugin utilizes 2 core packages: -- [ioredis](https://github.com/luin/ioredis) - for all connection management +- [ioredis](https://github.com/luin/ioredis) - for all connection management to any Redis or Redis-compatible database +- [redlock](https://github.com/mike-marcacci/node-redlock) - for distributed locks related to Strapi's built in cron-tasks system These are the primary features that are finished or currently being worked on: @@ -62,6 +60,7 @@ These are the primary features that are finished or currently being worked on: - [x] Redis Replica + Sentinel Support - [ ] Redis Sharding Support (assumed working, no config samples) - [x] Multiple connections/databases +- [x] Redlock capabilities with Strapi's built-in cron tasks ## 🤔 Motivation @@ -72,42 +71,28 @@ A few examples of where Redis could be used within a Strapi application: - LRU-based response cache for REST - Apollo server GraphQL cache - IP Rate-limiting using something like [koa2-ratelimit](https://www.npmjs.com/package/koa2-ratelimit) -- Distributed Redis locks for Strapi clusters (useful for clustered usage of cron tasks) +- Server-side user session storage - So much more If you are currently using this package in your plugin and would like to be featured, please feel free to submit an issue to have your plugin added to the list below: - [strapi-plugin-rest-cache](https://www.npmjs.com/package/strapi-plugin-rest-cache) - via: [strapi-provider-rest-cache-redis](https://www.npmjs.com/package/strapi-provider-rest-cache-redis) -- [strapi-plugin-redcron](https://www.npmjs.com/package/strapi-plugin-redcron) - More plugins coming soon! +Note the following packages used to use this package with Strapi v4 but have since been merged into this package: + +- [strapi-plugin-redcron](https://www.npmjs.com/package/strapi-plugin-redcron) + ## 🖐 Requirements Supported Strapi Versions: -| Strapi Version | Supported | Tested On | -| -------------- | --------- | ------------- | -| v3 | ❌ | N/A | -| v4.0.x | ✅ | July 2022 | -| v4.1.x | ✅ | July 2022 | -| v4.2.x | ✅ | July 2022 | -| v4.3.x | ✅ | December 2022 | -| v4.4.x | ✅ | December 2022 | -| v4.5.x | ✅ | December 2022 | -| v4.6.x | ✅ | January 2024 | -| v4.7.x | ✅ | January 2024 | -| v4.8.x | ✅ | January 2024 | -| v4.9.x | ✅ | January 2024 | -| v4.10.x | ✅ | January 2024 | -| v4.11.x | ✅ | January 2024 | -| v4.12.x | ✅ | January 2024 | -| v4.13.x | ✅ | January 2024 | -| v4.14.x | ✅ | January 2024 | -| v4.15.x | ✅ | January 2024 | -| v4.16.x | ✅ | January 2024 | -| v4.17.x | ✅ | January 2024 | -| v4.19.x | ✅ | January 2024 | +| Strapi Version | Plugin Version | Supported | Tested On | +|----------------|----------------|-----------|-----------| +| v3.x.x | N/A | ❌ | N/A | +| v4.x.x | 1.1.0 | ✅ | Sept 2024 | +| v5.x.x | 2.0.0 | ✅ | Sept 2024 | **This plugin will not work with Strapi v3 projects as it utilizes APIs that don't exist in the v3!** @@ -115,6 +100,9 @@ Supported Strapi Versions: Install the plugin in your Strapi project or your Strapi plugin. +**Warning** +For Strapi 4 projects you should use the `1.x.x` version of this plugin, for Strapi 5 projects you should use the `2.x.x` version of this plugin. + ```bash # Using Yarn (Recommended) yarn add strapi-plugin-redis diff --git a/package.json b/package.json index cba4563..ca29cbd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "strapi-plugin-redis", - "version": "1.1.0", + "version": "2.0.0", "description": "Plugin used to centralize management of Redis connections in Strapi", "strapi": { "displayName": "Redis", @@ -11,8 +11,12 @@ }, "dependencies": { "chalk": "4.1.2", - "debug": "4.3.4", - "ioredis": "5.2.4" + "debug": "4.3.5", + "ioredis": "5.4.1", + "redlock": "5.0.0-beta.2" + }, + "peerDependencies": { + "@strapi/strapi": "^5.0.0" }, "scripts": {}, "author": { @@ -30,6 +34,9 @@ "email": "derrickmehaffy@gmail.com", "url": "https://github.com/derrickmehaffy", "lead": true + }, + { + "name": "Excl Networks Inc." } ], "bugs": { diff --git a/server/config/index.js b/server/config/index.js index 071e218..c76932c 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -5,6 +5,15 @@ module.exports = { settings: { debug: false, debugIORedis: false, + redlockConfig: { + driftFactor: 0.01, + retryCount: 10, + retryDelay: 200, + retryJitter: 200, + }, + enableRedlock: false, + lockDelay: null, + lockTTL: 5000, }, connections: { default: { diff --git a/server/register.js b/server/register.js index e00dd74..d4f56ac 100644 --- a/server/register.js +++ b/server/register.js @@ -1,19 +1,20 @@ -'use strict' +'use strict'; -const debug = require('debug') +const debug = require('debug'); +const { default: Redlock } = require('redlock'); module.exports = async ({ strapi }) => { // Load plugin Config - const coreConfig = strapi.config.get('plugin.redis'); + const coreConfig = strapi.config.get('plugin::redis'); // Configure plugin debug if (coreConfig.settings.debug === true) { - debug.enable('strapi:strapi-plugin-redis') + debug.enable('strapi:strapi-plugin-redis'); } // Allow plugin + ioredis debug if (coreConfig.settings.debug === true && coreConfig.settings.debugIORedis === true) { - debug.enable('strapi:strapi-plugin-redis,ioredis:*') + debug.enable('strapi:strapi-plugin-redis,ioredis:*'); } // Construct Redis API @@ -25,15 +26,63 @@ module.exports = async ({ strapi }) => { // Build Redis database connections await strapi.plugin('redis').service('connection').buildAll(coreConfig); - // Construct Admin Permissions - const actions = [ - { - section: 'settings', - category: 'redis', - displayName: 'Access the Redis Overview page', - uid: 'settings.read', - pluginName: 'redis', - }, - ]; - await strapi.admin.services.permission.actionProvider.registerMany(actions); + // Configure Redlock + if (coreConfig.settings.enableRedlock === true) { + const originalAdd = strapi.cron.add; + const redlockConfig = coreConfig.settings.redlockConfig; + + strapi.cron.add = (tasks) => { + const generateRedlockFunction = (originalFunction, name) => { + return async (...args) => { + const connections = Object.keys(strapi.redis.connections).map((key) => { + return strapi.redis.connections[key].client; + }); + const redlock = new Redlock(connections, redlockConfig); + + let lock; + try { + lock = await redlock.acquire([name], coreConfig.settings.lockTTL); + debug(`Job ${name} acquired lock`); + await originalFunction(...args); + } catch (e) { + debug(`Job ${name} failed to acquire lock`); + } finally { + // wait some time so other processes will lose the lock + let lockDelay = coreConfig.settings.lockDelay + ? coreConfig.settings.lockDelay + : coreConfig.settings.redlockConfig.retryCount * + (coreConfig.settings.redlockConfig.retryDelay + + coreConfig.settings.redlockConfig.retryJitter); + debug(`Job ${name} waiting ${lockDelay}ms before releasing lock`); + await new Promise((resolve) => setTimeout(resolve, lockDelay)); + if (lock) { + debug(`Job ${name} releasing lock`); + try { + await lock.release(); + } catch (e) { + debug(`Job ${name} failed to release lock ${e}`); + } + } + } + }; + }; + Object.keys(tasks).forEach((key) => { + const taskValue = tasks[key]; + if (typeof taskValue === 'function') { + strapi.log.info('redlock requires tasks to use the object format'); + return; + } else if ( + typeof taskValue === 'object' && + taskValue && + typeof taskValue.task === 'function' && + taskValue.bypassRedlock !== true + ) { + // fallback to key if no name is provided + const taskName = taskValue.name || key; + taskValue.task = generateRedlockFunction(taskValue.task, 'redlock:' + taskName); + } + }); + originalAdd(tasks); + }; + } }; diff --git a/server/services/connection.js b/server/services/connection.js index 1ec416b..728e3ca 100644 --- a/server/services/connection.js +++ b/server/services/connection.js @@ -24,7 +24,7 @@ module.exports = ({ strapi }) => ({ debug(`${chalk.red('Failed to build')} ${name} connection - ${chalk.blue('cluster')}`); } - // Check for sentinel config + // Check for sentinel config } else if (nameConfig.connection.sentinels) { delete nameConfig.connection.host; delete nameConfig.connection.port; @@ -37,7 +37,7 @@ module.exports = ({ strapi }) => ({ debug(`${chalk.red('Failed to build')} ${name} connection - ${chalk.yellow('sentinel')}`); } - // Check for regular single connection + // Check for regular single connection } else { try { strapi.redis.connections[name] = { diff --git a/yarn.lock b/yarn.lock index f696346..a994484 100644 --- a/yarn.lock +++ b/yarn.lock @@ -39,14 +39,21 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -debug@4.3.4, debug@^4.3.4: +debug@4.3.5: + version "4.3.5" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" + integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== + dependencies: + ms "2.1.2" + +debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" -denque@^2.0.1: +denque@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== @@ -56,15 +63,15 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -ioredis@5.2.4: - version "5.2.4" - resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.2.4.tgz#9e262a668bc29bae98f2054c1e0d7efd86996b96" - integrity sha512-qIpuAEt32lZJQ0XyrloCRdlEdUUNGG9i0UOk6zgzK6igyudNWqEBxfH6OlbnOOoBBvr1WB02mm8fR55CnikRng== +ioredis@5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.4.1.tgz#1c56b70b759f01465913887375ed809134296f40" + integrity sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA== dependencies: "@ioredis/commands" "^1.1.1" cluster-key-slot "^1.1.0" debug "^4.3.4" - denque "^2.0.1" + denque "^2.1.0" lodash.defaults "^4.2.0" lodash.isarguments "^3.1.0" redis-errors "^1.2.0" @@ -86,6 +93,11 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +node-abort-controller@^3.0.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548" + integrity sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ== + redis-errors@^1.0.0, redis-errors@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" @@ -98,6 +110,13 @@ redis-parser@^3.0.0: dependencies: redis-errors "^1.0.0" +redlock@5.0.0-beta.2: + version "5.0.0-beta.2" + resolved "https://registry.yarnpkg.com/redlock/-/redlock-5.0.0-beta.2.tgz#a629c07e07d001c0fdd9f2efa614144c4416fe44" + integrity sha512-2RDWXg5jgRptDrB1w9O/JgSZC0j7y4SlaXnor93H/UJm/QyDiFgBKNtrh0TI6oCXqYSaSoXxFh6Sd3VtYfhRXw== + dependencies: + node-abort-controller "^3.0.1" + standard-as-callback@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45"