diff --git a/app/gen-server/lib/DocWorkerMap.ts b/app/gen-server/lib/DocWorkerMap.ts index 6943bb6663..20215f85af 100644 --- a/app/gen-server/lib/DocWorkerMap.ts +++ b/app/gen-server/lib/DocWorkerMap.ts @@ -3,15 +3,11 @@ import * as version from 'app/common/version'; import {DocStatus, DocWorkerInfo, IDocWorkerMap} from 'app/server/lib/DocWorkerMap'; import log from 'app/server/lib/log'; import {checkPermitKey, formatPermitKey, IPermitStore, Permit} from 'app/server/lib/Permit'; -import {promisifyAll} from 'bluebird'; import mapValues = require('lodash/mapValues'); -import {createClient, Multi, RedisClient} from 'redis'; +import {createClient, RedisClientType} from 'redis'; import Redlock from 'redlock'; import {v4 as uuidv4} from 'uuid'; -promisifyAll(RedisClient.prototype); -promisifyAll(Multi.prototype); - // Max time for which we will hold a lock, by default. In milliseconds. const LOCK_TIMEOUT = 3000; @@ -184,37 +180,39 @@ class DummyDocWorkerMap implements IDocWorkerMap { * "default". */ export class DocWorkerMap implements IDocWorkerMap { - private _client: RedisClient; - private _clients: RedisClient[]; + private _client: RedisClientType; + private _clients: RedisClientType[]; private _redlock: Redlock; // Optional deploymentKey argument supplies a key unique to the deployment (this is important // for maintaining groups across redeployments only) - constructor(_clients?: RedisClient[], private _deploymentKey?: string, private _options?: { + constructor(_clients?: RedisClientType[], private _deploymentKey?: string, private _options?: { permitMsec?: number }) { this._deploymentKey = this._deploymentKey || version.version; - this._clients = _clients || [createClient(process.env.REDIS_URL)]; + this._clients = _clients || [createClient({ + "url":process.env.REDIS_URL + })]; this._redlock = new Redlock(this._clients); this._client = this._clients[0]!; - this._client.on('error', (err) => log.warn(`DocWorkerMap: redisClient error`, String(err))); + this._client.on('error', (err: any) => log.warn(`DocWorkerMap: redisClient error`, String(err))); this._client.on('end', () => log.warn(`DocWorkerMap: redisClient connection closed`)); this._client.on('reconnecting', () => log.warn(`DocWorkerMap: redisClient reconnecting`)); } public async addWorker(info: DocWorkerInfo): Promise { log.info(`DocWorkerMap.addWorker ${info.id}`); - const lock = await this._redlock.lock('workers-lock', LOCK_TIMEOUT); + const lock = await this._redlock.acquire(['workers-lock'], LOCK_TIMEOUT); try { // Make a worker-{workerId} key with contact info. - await this._client.hmsetAsync(`worker-${info.id}`, info); + await this._client.hSet(`worker-${info.id}`, {...info}); // Add this worker to set of workers (but don't make it available for work yet). - await this._client.saddAsync('workers', info.id); + await this._client.sAdd('workers', info.id); if (info.group) { // Accept work only for a specific group. // Do not accept work not associated with the specified group. - await this._client.setAsync(`worker-${info.id}-group`, info.group); + await this._client.set(`worker-${info.id}-group`, info.group); } else { // Figure out if worker should belong to a group via elections. // Be careful: elections happen within a single deployment, so are somewhat @@ -222,111 +220,111 @@ export class DocWorkerMap implements IDocWorkerMap { // but there is no worker available for that group, it may open on any worker. // And if a worker is assigned to a group, it may still end up assigned work // not associated with that group if it is the only worker available. - const groups = await this._client.hgetallAsync('groups'); + const groups = await this._client.hGetAll('groups'); if (groups) { - const elections = await this._client.hgetallAsync(`elections-${this._deploymentKey}`) || {}; + const elections = await this._client.hGetAll(`elections-${this._deploymentKey}`) || {}; for (const group of Object.keys(groups).sort()) { const count = parseInt(groups[group], 10) || 0; if (count < 1) { continue; } const elected: string[] = JSON.parse(elections[group] || '[]'); if (elected.length >= count) { continue; } elected.push(info.id); - await this._client.setAsync(`worker-${info.id}-group`, group); - await this._client.hsetAsync(`elections-${this._deploymentKey}`, group, JSON.stringify(elected)); + await this._client.set(`worker-${info.id}-group`, group); + await this._client.hSet(`elections-${this._deploymentKey}`, group, JSON.stringify(elected)); break; } } } } finally { - await lock.unlock(); + await lock.release(); } } public async removeWorker(workerId: string): Promise { log.info(`DocWorkerMap.removeWorker ${workerId}`); - const lock = await this._redlock.lock('workers-lock', LOCK_TIMEOUT); + const lock = await this._redlock.acquire(['workers-lock'], LOCK_TIMEOUT); try { // Drop out of available set first. - await this._client.sremAsync('workers-available', workerId); - const group = await this._client.getAsync(`worker-${workerId}-group`) || DEFAULT_GROUP; - await this._client.sremAsync(`workers-available-${group}`, workerId); + await this._client.sRem('workers-available', workerId); + const group = await this._client.get(`worker-${workerId}-group`) || DEFAULT_GROUP; + await this._client.sRem(`workers-available-${group}`, workerId); // At this point, this worker should no longer be receiving new doc assignments, though // clients may still be directed to the worker. // If we were elected for anything, back out. - const elections = await this._client.hgetallAsync(`elections-${this._deploymentKey}`); + const elections = await this._client.hGetAll(`elections-${this._deploymentKey}`); if (elections) { if (group in elections) { const elected: string[] = JSON.parse(elections[group]); const newElected = elected.filter(worker => worker !== workerId); if (elected.length !== newElected.length) { if (newElected.length > 0) { - await this._client.hsetAsync(`elections-${this._deploymentKey}`, group, + await this._client.hSet(`elections-${this._deploymentKey}`, group, JSON.stringify(newElected)); } else { - await this._client.hdelAsync(`elections-${this._deploymentKey}`, group); + await this._client.hDel(`elections-${this._deploymentKey}`, group); delete elections[group]; } } // We're the last one involved in elections - remove the key entirely. if (Object.keys(elected).length === 0) { - await this._client.delAsync(`elections-${this._deploymentKey}`); + await this._client.del(`elections-${this._deploymentKey}`); } } } // Now, we start removing the assignments. - const assignments = await this._client.smembersAsync(`worker-${workerId}-docs`); + const assignments = await this._client.sMembers(`worker-${workerId}-docs`); if (assignments) { const op = this._client.multi(); for (const doc of assignments) { op.del(`doc-${doc}`); } - await op.execAsync(); + await op.exec(); } // Now remove worker-{workerId}* keys. - await this._client.delAsync(`worker-${workerId}-docs`); - await this._client.delAsync(`worker-${workerId}-group`); - await this._client.delAsync(`worker-${workerId}`); + await this._client.del(`worker-${workerId}-docs`); + await this._client.del(`worker-${workerId}-group`); + await this._client.del(`worker-${workerId}`); // Forget about this worker completely. - await this._client.sremAsync('workers', workerId); + await this._client.sRem('workers', workerId); } finally { - await lock.unlock(); + await lock.release(); } } public async setWorkerAvailability(workerId: string, available: boolean): Promise { log.info(`DocWorkerMap.setWorkerAvailability ${workerId} ${available}`); - const group = await this._client.getAsync(`worker-${workerId}-group`) || DEFAULT_GROUP; + const group = await this._client.get(`worker-${workerId}-group`) || DEFAULT_GROUP; if (available) { - const docWorker = await this._client.hgetallAsync(`worker-${workerId}`) as DocWorkerInfo|null; + const docWorker = await this._client.hGetAll(`worker-${workerId}`) as unknown as DocWorkerInfo|null; if (!docWorker) { throw new Error('no doc worker contact info available'); } - await this._client.saddAsync(`workers-available-${group}`, workerId); + await this._client.sAdd(`workers-available-${group}`, workerId); // If we're not assigned exclusively to a group, add this worker also to the general // pool of workers. if (!docWorker.group) { - await this._client.saddAsync('workers-available', workerId); + await this._client.sAdd('workers-available', workerId); } } else { - await this._client.sremAsync('workers-available', workerId); - await this._client.sremAsync(`workers-available-${group}`, workerId); + await this._client.sRem('workers-available', workerId); + await this._client.sRem(`workers-available-${group}`, workerId); } } public async isWorkerRegistered(workerInfo: DocWorkerInfo): Promise { const group = workerInfo.group || DEFAULT_GROUP; - return Boolean(await this._client.sismemberAsync(`workers-available-${group}`, workerInfo.id)); + return Boolean(await this._client.sIsMember(`workers-available-${group}`, workerInfo.id)); } public async releaseAssignment(workerId: string, docId: string): Promise { const op = this._client.multi(); op.del(`doc-${docId}`); - op.srem(`worker-${workerId}-docs`, docId); - await op.execAsync(); + op.sRem(`worker-${workerId}-docs`, docId); + await op.exec(); } public async getAssignments(workerId: string): Promise { - return this._client.smembersAsync(`worker-${workerId}-docs`); + return this._client.sMembers(`worker-${workerId}-docs`); } /** @@ -362,11 +360,11 @@ export class DocWorkerMap implements IDocWorkerMap { */ public async assignDocWorker(docId: string, workerId?: string): Promise { if (docId === 'import') { - const lock = await this._redlock.lock(`workers-lock`, LOCK_TIMEOUT); + const lock = await this._redlock.acquire([`workers-lock`], LOCK_TIMEOUT); try { - const _workerId = await this._client.srandmemberAsync(`workers-available-${DEFAULT_GROUP}`); + const _workerId = await this._client.sRandMember(`workers-available-${DEFAULT_GROUP}`); if (!_workerId) { throw new Error('no doc worker available'); } - const docWorker = await this._client.hgetallAsync(`worker-${_workerId}`) as DocWorkerInfo|null; + const docWorker = await this._client.hGetAll(`worker-${_workerId}`) as unknown as DocWorkerInfo|null; if (!docWorker) { throw new Error('no doc worker contact info available'); } return { docMD5: null, @@ -374,7 +372,7 @@ export class DocWorkerMap implements IDocWorkerMap { isActive: false }; } finally { - await lock.unlock(); + await lock.release(); } } @@ -384,7 +382,7 @@ export class DocWorkerMap implements IDocWorkerMap { if (docStatus) { return docStatus; } // No assignment yet, so let's lock and set an assignment up. - const lock = await this._redlock.lock(`workers-lock`, LOCK_TIMEOUT); + const lock = await this._redlock.acquire([`workers-lock`], LOCK_TIMEOUT); try { // Now that we've locked, recheck that the worker hasn't been reassigned @@ -395,11 +393,11 @@ export class DocWorkerMap implements IDocWorkerMap { if (!workerId) { // Check if document has a preferred worker group set. - const group = await this._client.getAsync(`doc-${docId}-group`) || DEFAULT_GROUP; + const group = await this._client.get(`doc-${docId}-group`) || DEFAULT_GROUP; // Let's start off by assigning documents to available workers randomly. // TODO: use a smarter algorithm. - workerId = await this._client.srandmemberAsync(`workers-available-${group}`) || undefined; + workerId = await this._client.sRandMember(`workers-available-${group}`) || undefined; if (!workerId) { // No workers available in the desired worker group. Rather than refusing to // open the document, we fall back on assigning a worker from any of the workers @@ -408,17 +406,17 @@ export class DocWorkerMap implements IDocWorkerMap { // or not starting enough workers). It has the downside of potentially disguising // problems, so we log a warning. log.warn(`DocWorkerMap.assignDocWorker ${docId} found no workers for group ${group}`); - workerId = await this._client.srandmemberAsync('workers-available') || undefined; + workerId = await this._client.sRandMember('workers-available') || undefined; } if (!workerId) { throw new Error('no doc workers available'); } } else { - if (!await this._client.sismemberAsync('workers-available', workerId)) { + if (!await this._client.sIsMember('workers-available', workerId)) { throw new Error(`worker ${workerId} not known or not available`); } } // Look up how to contact the worker. - const docWorker = await this._client.hgetallAsync(`worker-${workerId}`) as DocWorkerInfo|null; + const docWorker = await this._client.hGetAll(`worker-${workerId}`) as unknown as DocWorkerInfo|null; if (!docWorker) { throw new Error('no doc worker contact info available'); } // We can now construct a DocStatus, preserving any existing checksum. @@ -427,17 +425,17 @@ export class DocWorkerMap implements IDocWorkerMap { // We add the assignment to worker-{workerId}-docs and save doc-{docId}. const result = await this._client.multi() - .sadd(`worker-${workerId}-docs`, docId) - .hmset(`doc-${docId}`, { + .sAdd(`worker-${workerId}-docs`, docId) + .hSet(`doc-${docId}`, { docWorker: JSON.stringify(docWorker), // redis can't store nested objects, strings only isActive: JSON.stringify(true) // redis can't store booleans, strings only }) - .setex(`doc-${docId}-checksum`, CHECKSUM_TTL_MSEC / 1000.0, checksum || 'null') - .execAsync(); + .setEx(`doc-${docId}-checksum`, CHECKSUM_TTL_MSEC / 1000.0, checksum || 'null') + .exec(); if (!result) { throw new Error('failed to store new assignment'); } return newDocStatus; } finally { - await lock.unlock(); + await lock.release(); } } @@ -457,11 +455,11 @@ export class DocWorkerMap implements IDocWorkerMap { } public async updateChecksum(family: string, key: string, checksum: string) { - await this._client.setexAsync(`${family}-${key}-checksum`, CHECKSUM_TTL_MSEC / 1000.0, checksum); + await this._client.setEx(`${family}-${key}-checksum`, CHECKSUM_TTL_MSEC / 1000.0, checksum); } public async getChecksum(family: string, key: string) { - const checksum = await this._client.getAsync(`${family}-${key}-checksum`); + const checksum = await this._client.get(`${family}-${key}-checksum`); return checksum === 'null' ? null : checksum; } @@ -473,17 +471,17 @@ export class DocWorkerMap implements IDocWorkerMap { const key = formatPermitKey(uuidv4(), prefix); // seems like only integer seconds are supported? const duration = ttlMs || permitMsec; - await client.setexAsync(key, Math.ceil(duration / 1000.0), JSON.stringify(permit)); + await client.setEx(key, Math.ceil(duration / 1000.0), JSON.stringify(permit)); return key; }, async getPermit(key: string): Promise { if (!checkPermitKey(key, prefix)) { throw new Error('permit could not be read'); } - const result = await client.getAsync(key); + const result = await client.get(key); return result && JSON.parse(result); }, async removePermit(key: string): Promise { if (!checkPermitKey(key, prefix)) { throw new Error('permit could not be read'); } - await client.delAsync(key); + await client.del(key); }, async close() { // nothing to do @@ -496,7 +494,7 @@ export class DocWorkerMap implements IDocWorkerMap { public async close(): Promise { for (const cli of this._clients || []) { - await cli.quitAsync(); + await cli.quit(); } } @@ -505,50 +503,50 @@ export class DocWorkerMap implements IDocWorkerMap { // favoring redlock: // https://redis.io/commands/setnx#design-pattern-locking-with-codesetnxcode const redisKey = `nomination-${name}`; - const lock = await this._redlock.lock(`${redisKey}-lock`, LOCK_TIMEOUT); + const lock = await this._redlock.acquire([`${redisKey}-lock`], LOCK_TIMEOUT); try { - if (await this._client.getAsync(redisKey) !== null) { return null; } + if (await this._client.get(redisKey) !== null) { return null; } const electionKey = uuidv4(); // seems like only integer seconds are supported? - await this._client.setexAsync(redisKey, Math.ceil(durationInMs / 1000.0), electionKey); + await this._client.setEx(redisKey, Math.ceil(durationInMs / 1000.0), electionKey); return electionKey; } finally { - await lock.unlock(); + await lock.release(); } } public async removeElection(name: string, electionKey: string): Promise { const redisKey = `nomination-${name}`; - const lock = await this._redlock.lock(`${redisKey}-lock`, LOCK_TIMEOUT); + const lock = await this._redlock.acquire([`${redisKey}-lock`], LOCK_TIMEOUT); try { - const current = await this._client.getAsync(redisKey); + const current = await this._client.get(redisKey); if (current === electionKey) { - await this._client.delAsync(redisKey); + await this._client.del(redisKey); } else if (current !== null) { throw new Error('could not remove election'); } } finally { - await lock.unlock(); + await lock.release(); } } public async getWorkerGroup(workerId: string): Promise { - return this._client.getAsync(`worker-${workerId}-group`); + return this._client.get(`worker-${workerId}-group`); } public async getDocGroup(docId: string): Promise { - return this._client.getAsync(`doc-${docId}-group`); + return this._client.get(`doc-${docId}-group`); } public async updateDocGroup(docId: string, docGroup: string): Promise { - await this._client.setAsync(`doc-${docId}-group`, docGroup); + await this._client.set(`doc-${docId}-group`, docGroup); } public async removeDocGroup(docId: string): Promise { - await this._client.delAsync(`doc-${docId}-group`); + await this._client.del(`doc-${docId}-group`); } - public getRedisClient(): RedisClient { + public getRedisClient(): RedisClientType { return this._client; } @@ -562,9 +560,9 @@ export class DocWorkerMap implements IDocWorkerMap { }> { // Fetch the various elements that go into making a DocStatus const props = await this._client.multi() - .hgetall(`doc-${docId}`) + .hGetAll(`doc-${docId}`) .get(`doc-${docId}-checksum`) - .execAsync() as [{[key: string]: any}|null, string|null]|null; + .exec() as [{[key: string]: any}|null, string|null]|null; // Fields are JSON encoded since redis cannot store them directly. const doc = props?.[0] ? mapValues(props[0], (val) => JSON.parse(val)) as DocStatus : null; // Redis cannot store a null value, so we encode it as 'null', which does diff --git a/app/server/devServerMain.ts b/app/server/devServerMain.ts index 57b069023d..cad5baf8b7 100644 --- a/app/server/devServerMain.ts +++ b/app/server/devServerMain.ts @@ -22,12 +22,10 @@ import {updateDb} from 'app/server/lib/dbUtils'; import {FlexServer} from 'app/server/lib/FlexServer'; import log from 'app/server/lib/log'; import {MergedServer} from 'app/server/MergedServer'; -import {promisifyAll} from 'bluebird'; import * as fse from 'fs-extra'; import * as path from 'path'; -import {createClient, RedisClient} from 'redis'; +import {createClient} from 'redis'; -promisifyAll(RedisClient.prototype); function getPort(envVarName: string, fallbackPort: number): number { const val = process.env[envVarName]; @@ -61,7 +59,7 @@ export async function main() { const {createInitialDb} = require('test/gen-server/seed'); await createInitialDb(); if (process.env.REDIS_URL) { - await createClient(process.env.REDIS_URL).flushdbAsync(); + await createClient({url:process.env.REDIS_URL}).flushDb(); } } else { await updateDb(); diff --git a/app/server/lib/AccessTokens.ts b/app/server/lib/AccessTokens.ts index f57c90a19e..9ba5f1a34f 100644 --- a/app/server/lib/AccessTokens.ts +++ b/app/server/lib/AccessTokens.ts @@ -4,7 +4,7 @@ import { KeyedMutex } from 'app/common/KeyedMutex'; import { AccessTokenOptions } from 'app/plugin/GristAPI'; import { makeId } from 'app/server/lib/idUtils'; import * as jwt from 'jsonwebtoken'; -import { RedisClient } from 'redis'; +import { RedisClientType } from 'redis'; export const Deps = { // Signed tokens expire after this length of time. @@ -87,7 +87,7 @@ export class AccessTokens implements IAccessTokens { // tokens must be honored. Cache is of a list of secrets. It is important to allow multiple // secrets so we can change the secret we are signing with and still honor tokens signed with // a previous secret. - constructor(cli: RedisClient|null, private _factor: number = 10) { + constructor(cli: RedisClientType|null, private _factor: number = 10) { this._store = cli ? new RedisAccessTokenSignerStore(cli) : new InMemoryAccessTokenSignerStore(); this._dtMsec = Deps.TOKEN_TTL_MSECS; this._reads = new MapWithTTL(this._dtMsec * _factor * 0.5); @@ -247,15 +247,15 @@ export class InMemoryAccessTokenSignerStore implements IAccessTokenSignerStore { // Redis based implementation of IAccessTokenSignerStore, for multi process/instance // Grist. export class RedisAccessTokenSignerStore implements IAccessTokenSignerStore { - constructor(private _cli: RedisClient) { } + constructor(private _cli: RedisClientType) { } public async getSigners(docId: string): Promise { - const keys = await this._cli.getAsync(this._getKey(docId)); + const keys = await this._cli.get(this._getKey(docId)); return keys?.split(',') || []; } public async setSigners(docId: string, secrets: string[], ttlMsec: number): Promise { - await this._cli.setexAsync(this._getKey(docId), ttlMsec, secrets.join(',')); + await this._cli.setEx(this._getKey(docId), ttlMsec, secrets.join(',')); } public async close() { diff --git a/app/server/lib/ActiveDoc.ts b/app/server/lib/ActiveDoc.ts index 32b2050eb4..33866de01e 100644 --- a/app/server/lib/ActiveDoc.ts +++ b/app/server/lib/ActiveDoc.ts @@ -131,7 +131,7 @@ import {IMessage, MsgType} from 'grain-rpc'; import imageSize from 'image-size'; import * as moment from 'moment-timezone'; import fetch from 'node-fetch'; -import {createClient, RedisClient} from 'redis'; +import {createClient, RedisClientType} from 'redis'; import tmp from 'tmp'; import {ActionHistory} from './ActionHistory'; @@ -270,7 +270,7 @@ export class ActiveDoc extends EventEmitter { private _attachmentFileManager: AttachmentFileManager; // Client watching for 'product changed' event published by Billing to update usage - private _redisSubscriber?: RedisClient; + private _redisSubscriber?: RedisClientType; // Timer for shutting down the ActiveDoc a bit after all clients are gone. private _inactivityTimer = new InactivityTimer(() => { @@ -339,19 +339,20 @@ export class ActiveDoc extends EventEmitter { if (process.env.REDIS_URL && billingAccount) { const prefix = getPubSubPrefix(); const channel = `${prefix}-billingAccount-${billingAccount.id}-product-changed`; - this._redisSubscriber = createClient(process.env.REDIS_URL); - this._redisSubscriber.subscribe(channel); - this._redisSubscriber.on("message", async () => { + const listener = async (message: string, chan: string) => { + this._log.debug(null, `Reload ActiveDoc: ${message}, ${chan}`); // A product change has just happened in Billing. // Reload the doc (causing connected clients to reload) to ensure everyone sees the effect of the change. this._log.debug(null, 'reload after product change'); await this.reloadDoc(); - }); - this._redisSubscriber.on("error", async (error) => { + }; + this._redisSubscriber = createClient({url:process.env.REDIS_URL}); + this._redisSubscriber.SUBSCRIBE(channel, listener) + .catch(err=>{ this._log.error( null, `encountered error while subscribed to channel ${channel}`, - error + err ); }); } @@ -2126,7 +2127,7 @@ export class ActiveDoc extends EventEmitter { this._triggers.shutdown(); - this._redisSubscriber?.quitAsync() + this._redisSubscriber?.quit() .catch(e => this._log.warn(docSession, "Failed to quit redis subscriber", e)); // Clear the MapWithTTL to remove all timers from the event loop. diff --git a/app/server/lib/DocApi.ts b/app/server/lib/DocApi.ts index 28652552d9..387e740568 100644 --- a/app/server/lib/DocApi.ts +++ b/app/server/lib/DocApi.ts @@ -1871,10 +1871,10 @@ export class DocWorkerApi { const expiry = 2 * 24 * 60 * 60 / period.periodsPerDay; multi.incr(key).expire(key, expiry); } - multi.execAsync().then(result => { + multi.exec().then(result => { for (let i = 0; i < keys.length; i++) { const key = keys[i]; - const newCount = Number(result![i * 2]); // incrs are at even positions, expires at odd positions + const newCount = Number(result[i * 2]); // incrs are at even positions, expires at odd positions // Theoretically this could be overwritten by a lower count that was requested earlier // but somehow arrived after. // This doesn't really matter, and the count on redis will still increase reliably. diff --git a/app/server/lib/DocWorkerMap.ts b/app/server/lib/DocWorkerMap.ts index 922a8d1f79..74d529eb4d 100644 --- a/app/server/lib/DocWorkerMap.ts +++ b/app/server/lib/DocWorkerMap.ts @@ -6,7 +6,7 @@ import { IChecksumStore } from 'app/server/lib/IChecksumStore'; import { IElectionStore } from 'app/server/lib/IElectionStore'; import { IPermitStores } from 'app/server/lib/Permit'; -import { RedisClient } from 'redis'; +import { RedisClientType } from 'redis'; export interface DocWorkerInfo { id: string; @@ -75,5 +75,5 @@ export interface IDocWorkerMap extends IPermitStores, IElectionStore, IChecksumS removeDocGroup(docId: string): Promise; - getRedisClient(): RedisClient|null; + getRedisClient(): RedisClientType|null; } diff --git a/app/server/lib/FlexServer.ts b/app/server/lib/FlexServer.ts index 2263cd9ebf..d2a048a070 100644 --- a/app/server/lib/FlexServer.ts +++ b/app/server/lib/FlexServer.ts @@ -532,14 +532,14 @@ export class FlexServer implements GristServer { checks.set('db', asyncCheck(this._dbManager.connection.query('SELECT 1'))); } if (isParameterOn(req.query.redis)) { - checks.set('redis', asyncCheck(this._docWorkerMap.getRedisClient()?.pingAsync())); + checks.set('redis', asyncCheck(this._docWorkerMap.getRedisClient()?.ping())); } if (isParameterOn(req.query.docWorkerRegistered) && this.worker) { // Only check whether the doc worker is registered if we have a worker. // The Redis client may not be connected, but in this case this has to // be checked with the 'redis' parameter (the user may want to avoid // removing workers when connection is unstable). - if (this._docWorkerMap.getRedisClient()?.connected) { + if (this._docWorkerMap.getRedisClient()?.isReady) { checks.set('docWorkerRegistered', asyncCheck( this._docWorkerMap.isWorkerRegistered(this.worker).then(isRegistered => { if (!isRegistered) { throw new Error('doc worker not registered'); } diff --git a/app/server/lib/Triggers.ts b/app/server/lib/Triggers.ts index 81af4c428e..d1b453ac0f 100644 --- a/app/server/lib/Triggers.ts +++ b/app/server/lib/Triggers.ts @@ -23,13 +23,10 @@ import {proxyAgent} from 'app/server/lib/ProxyAgent'; import {matchesBaseDomain} from 'app/server/lib/requestUtils'; import {delayAbort} from 'app/server/lib/serverUtils'; import {LogSanitizer} from "app/server/utils/LogSanitizer"; -import {promisifyAll} from 'bluebird'; import * as _ from 'lodash'; import {AbortController, AbortSignal} from 'node-abort-controller'; import fetch from 'node-fetch'; -import {createClient, Multi, RedisClient} from 'redis'; - -promisifyAll(RedisClient.prototype); +import {createClient, RedisClientType} from 'redis'; // Only owners can manage triggers, but any user's activity can trigger them // and the corresponding actions get the full values @@ -126,7 +123,7 @@ export class DocTriggers { // Client lazily initiated by _redisClient getter, since most documents don't have triggers // and therefore don't need a redis connection. - private _redisClientField: RedisClient | undefined; + private _redisClientField: RedisClientType | undefined; // Promise which resolves after we finish fetching the backup queue from redis on startup. private _getRedisQueuePromise: Promise | undefined; @@ -142,7 +139,7 @@ export class DocTriggers { if (redisUrl) { // We create a transient client just for this purpose because it makes it easy // to quit it afterwards and avoid keeping a client open for documents without triggers. - this._getRedisQueuePromise = this._getRedisQueue(createClient(redisUrl)); + this._getRedisQueuePromise = this._getRedisQueue(createClient({url:redisUrl})); } this._stats = new WebhookStatistics(this._docId, _activeDoc, () => this._redisClient ?? null); } @@ -151,7 +148,7 @@ export class DocTriggers { this._shuttingDown = true; this._loopAbort?.abort(); if (!this._sending) { - void(this._redisClientField?.quitAsync()); + void(this._redisClientField?.quit()); } } @@ -346,7 +343,7 @@ export class DocTriggers { // NOTE: this is subject to a race condition, currently it is not possible, but any future modification probably // will require some kind of locking over the queue (or a rewrite) if (removed && this._redisClient) { - await this._redisClient.multi().del(this._redisQueueKey).execAsync(); + await this._redisClient.multi().del(this._redisQueueKey).exec(); } await this._stats.clear(); this._log("Webhook queue cleared", {numRemoved: removed}); @@ -375,9 +372,9 @@ export class DocTriggers { // Re-add all the remaining events to the queue. if (this._webHookEventQueue.length) { const strings = this._webHookEventQueue.map(e => JSON.stringify(e)); - multi.rpush(this._redisQueueKey, ...strings); + multi.rPush(this._redisQueueKey, strings); } - await multi.execAsync(); + await multi.exec(); } await this._stats.clear(); this._log("Single webhook queue cleared", {numRemoved: removed, webhookId}); @@ -444,7 +441,7 @@ export class DocTriggers { private async _pushToRedisQueue(events: WebHookEvent[]) { const strings = events.map(e => JSON.stringify(e)); try { - await this._redisClient?.rpushAsync(this._redisQueueKey, ...strings); + await this._redisClient?.rPush(this._redisQueueKey, strings); } catch(e){ // It's very hard to test this with integration tests, because it requires a redis failure. @@ -454,15 +451,15 @@ export class DocTriggers { } } - private async _getRedisQueue(redisClient: RedisClient) { - const strings = await redisClient.lrangeAsync(this._redisQueueKey, 0, -1); + private async _getRedisQueue(redisClient: RedisClientType) { + const strings = await redisClient.lRange(this._redisQueueKey, 0, -1); if (strings.length) { this._log("Webhook events found on redis queue", {numEvents: strings.length}); const events = strings.map(s => JSON.parse(s)); this._webHookEventQueue.unshift(...events); this._startSendLoop(); } - await redisClient.quitAsync(); + await redisClient.quit(); } private _getRecordDeltas(tableDelta: TableDelta): RecordDeltas { @@ -715,10 +712,9 @@ export class DocTriggers { this._webHookEventQueue.splice(0, batch.length); - let multi: Multi | null = null; - if (this._redisClient) { - multi = this._redisClient.multi(); - multi.ltrim(this._redisQueueKey, batch.length, -1); + const multi = this._redisClient?.multi() || null; + if (multi) { + multi.lTrim(this._redisQueueKey, batch.length, -1); } if (!success) { @@ -729,8 +725,9 @@ export class DocTriggers { this._webHookEventQueue.push(...batch); if (multi) { const strings = batch.map(e => JSON.stringify(e)); - multi.rpush(this._redisQueueKey, ...strings); + multi.rPush(this._redisQueueKey, strings); } + // We are postponed, so mark that. await this._stats.logStatus(id, 'postponed'); } else { @@ -764,12 +761,12 @@ export class DocTriggers { } } - await multi?.execAsync(); + await multi?.exec(); } this._log("Ended _sendLoop"); - this._redisClient?.quitAsync().catch(e => + this._redisClient?.quit().catch(e => // Catch error to prevent sendLoop being restarted this._log("Error quitting redis: " + e, {level: 'warn'}) ); @@ -782,7 +779,7 @@ export class DocTriggers { const redisUrl = process.env.REDIS_URL; if (redisUrl) { this._log("Creating redis client"); - this._redisClientField = createClient(redisUrl); + this._redisClientField = createClient({url:redisUrl}); } return this._redisClientField; } @@ -908,7 +905,7 @@ class PersistedStore { constructor( docId: string, private _activeDoc: ActiveDoc, - private _redisClientDep: () => RedisClient | null + private _redisClientDep: () => RedisClientType | null ) { this._redisKey = `webhooks:${docId}:statistics`; } @@ -916,7 +913,7 @@ class PersistedStore { public async clear() { this._statsCache.clear(); if (this._redisClient) { - await this._redisClient.delAsync(this._redisKey).catch(() => {}); + await this._redisClient.del(this._redisKey).catch(() => {}); } } @@ -928,10 +925,10 @@ class PersistedStore { if (this._redisClient) { const multi = this._redisClient.multi(); for (const [key, value] of keyValues) { - multi.hset(this._redisKey, `${id}:${key}`, value); + multi.hSet(this._redisKey, `${id}:${key}`, value); multi.expire(this._redisKey, WEBHOOK_STATS_CACHE_TTL); } - await multi.execAsync(); + await multi.exec(); } else { for (const [key, value] of keyValues) { this._statsCache.set(`${id}:${key}`, value); @@ -941,7 +938,7 @@ class PersistedStore { protected async get(id: string, keys: Keys[]): Promise<[Keys, string][]> { if (this._redisClient) { - const values = (await this._redisClient.hgetallAsync(this._redisKey)) || {}; + const values = (await this._redisClient.hGetAll(this._redisKey)) || {}; return keys.map(key => [key, values[`${id}:${key}`] || '']); } else { return keys.map(key => [key, this._statsCache.get(`${id}:${key}`) || '']); diff --git a/app/server/lib/gristSessions.ts b/app/server/lib/gristSessions.ts index a5555ba1b3..30b35e524a 100644 --- a/app/server/lib/gristSessions.ts +++ b/app/server/lib/gristSessions.ts @@ -3,12 +3,13 @@ import {parseSubdomain} from 'app/common/gristUrls'; import {isNumber} from 'app/common/gutil'; import {RequestWithOrg} from 'app/server/lib/extractOrg'; import {GristServer} from 'app/server/lib/GristServer'; +import log from 'app/server/lib/log'; import {fromCallback} from 'app/server/lib/serverUtils'; import {Sessions} from 'app/server/lib/Sessions'; import {promisifyAll} from 'bluebird'; import * as crypto from 'crypto'; import * as express from 'express'; -import assignIn = require('lodash/assignIn'); +import assignIn from 'lodash/assignIn'; import * as path from 'path'; @@ -55,17 +56,25 @@ export interface IGristSession { function createSessionStoreFactory(sessionsDB: string): () => SessionStore { if (process.env.REDIS_URL) { // Note that ./build excludes this module from the electron build. - const RedisStore = require('connect-redis')(session); + const redis = require('redis'); + const {RedisStore} = require('connect-redis'); promisifyAll(RedisStore.prototype); return () => { - const store = new RedisStore({ + const client = redis.createClient({ url: process.env.REDIS_URL, }); + client.unref(); + client.on('error', (err: any) => { + log.warn(`[Redis Client]: ${err}`); + }); + const store = new RedisStore({ + client + }); return assignIn(store, { async close() { // Quit the client, so that it doesn't attempt to reconnect (which matters for some // tests), and so that node becomes close-able. - await fromCallback(cb => store.client.quit(cb)); + await fromCallback(cb => client.quit(cb)); }}); }; } else { diff --git a/package.json b/package.json index 58e2e53cf1..819f1eec49 100644 --- a/package.json +++ b/package.json @@ -134,7 +134,7 @@ "color-convert": "2.0.1", "commander": "9.3.0", "components-jqueryui": "1.12.1", - "connect-redis": "3.4.0", + "connect-redis": "8.0.1", "cookie": "0.7.0", "cookie-parser": "1.4.7", "csv": "6.3.8", @@ -149,6 +149,7 @@ "exceljs": "4.2.1", "express": "4.21.2", "express-rate-limit": "7.2.0", + "express-session": "^1.18.1", "file-type": "16.5.4", "fs-extra": "7.0.0", "grain-rpc": "0.1.7", @@ -189,8 +190,8 @@ "prom-client": "14.2.0", "qrcode": "1.5.0", "randomcolor": "0.5.3", - "redis": "3.1.1", - "redlock": "3.1.2", + "redis": "4.7.0", + "redlock": "5.0.0-beta.2", "saml2-js": "4.0.2", "scimmy": "1.2.4", "scimmy-routers": "1.2.2", @@ -207,6 +208,7 @@ }, "resolutions": { "**/ip": "https://registry.npmjs.org/neoip/-/neoip-2.1.0.tgz", + "express-session": "https://registry.npmjs.org/@gristlabs/express-session/-/express-session-1.18.1-grist1.tgz", "jquery": "3.5.0", "ts-interface-checker": "1.0.2", "@gristlabs/sqlite3": "5.1.4-grist.8" diff --git a/stubs/app/server/declarations.d.ts b/stubs/app/server/declarations.d.ts index 5290318491..7bd08c3bce 100644 --- a/stubs/app/server/declarations.d.ts +++ b/stubs/app/server/declarations.d.ts @@ -24,61 +24,61 @@ declare module "@gristlabs/sqlite3" { // Add declarations of the promisified methods of redis. // This is not exhaustive, there are a *lot* of methods. -declare module "redis" { - function createClient(url?: string): RedisClient; +// declare module "redis" { +// function createClient(url?: string): RedisClient; - class RedisClient { - public readonly connected: boolean; - public eval(args: any[], callback?: (err: Error | null, res: any) => void): any; +// class RedisClient { +// public readonly connected: boolean; +// public eval(args: any[], callback?: (err: Error | null, res: any) => void): any; - public subscribe(channel: string): void; - public on(eventType: string, callback: (...args: any[]) => void): void; - public publishAsync(channel: string, message: string): Promise; +// public subscribe(channel: string): void; +// public on(eventType: string, callback: (...args: any[]) => void): void; +// public publishAsync(channel: string, message: string): Promise; - public delAsync(key: string): Promise<'OK'>; - public flushdbAsync(): Promise; - public getAsync(key: string): Promise; - public hdelAsync(key: string, field: string): Promise; - public hgetallAsync(key: string): Promise<{[field: string]: any}|null>; - public hkeysAsync(key: string): Promise; - public hmsetAsync(key: string, val: {[field: string]: any}): Promise<'OK'>; - public hsetAsync(key: string, field: string, val: string): Promise<1|0>; - public keysAsync(pattern: string): Promise; - public multi(): Multi; - public quitAsync(): Promise; - public saddAsync(key: string, val: string): Promise<'OK'>; - public selectAsync(db: number): Promise; - public setAsync(key: string, val: string): Promise<'OK'>; - public setexAsync(key: string, ttl: number, val: string): Promise<'OK'>; - public sismemberAsync(key: string, val: string): Promise<0|1>; - public smembersAsync(key: string): Promise; - public srandmemberAsync(key: string): Promise; - public sremAsync(key: string, val: string): Promise<'OK'>; - public ttlAsync(key: string): Promise; - public unwatchAsync(): Promise<'OK'>; - public watchAsync(key: string): Promise; - public lrangeAsync(key: string, start: number, end: number): Promise; - public rpushAsync(key: string, ...vals: string[]): Promise; - public pingAsync(): Promise; - } +// public delAsync(key: string): Promise<'OK'>; +// public flushdbAsync(): Promise; +// public getAsync(key: string): Promise; +// public hdelAsync(key: string, field: string): Promise; +// public hgetallAsync(key: string): Promise<{[field: string]: any}|null>; +// public hkeysAsync(key: string): Promise; +// public hmsetAsync(key: string, val: {[field: string]: any}): Promise<'OK'>; +// public hsetAsync(key: string, field: string, val: string): Promise<1|0>; +// public keysAsync(pattern: string): Promise; +// public multi(): Multi; +// public quitAsync(): Promise; +// public saddAsync(key: string, val: string): Promise<'OK'>; +// public selectAsync(db: number): Promise; +// public setAsync(key: string, val: string): Promise<'OK'>; +// public setexAsync(key: string, ttl: number, val: string): Promise<'OK'>; +// public sismemberAsync(key: string, val: string): Promise<0|1>; +// public smembersAsync(key: string): Promise; +// public srandmemberAsync(key: string): Promise; +// public sremAsync(key: string, val: string): Promise<'OK'>; +// public ttlAsync(key: string): Promise; +// public unwatchAsync(): Promise<'OK'>; +// public watchAsync(key: string): Promise; +// public lrangeAsync(key: string, start: number, end: number): Promise; +// public rpushAsync(key: string, ...vals: string[]): Promise; +// public pingAsync(): Promise; +// } - class Multi { - public del(key: string): Multi; - public execAsync(): Promise; - public get(key: string): Multi; - public hgetall(key: string): Multi; - public hmset(key: string, val: {[field: string]: any}): Multi; - public hset(key: string, field: string, val: string): Multi; - public sadd(key: string, val: string): Multi; - public set(key: string, val: string): Multi; - public setex(key: string, ttl: number, val: string): Multi; - public ttl(key: string): Multi; - public smembers(key: string): Multi; - public srandmember(key: string): Multi; - public srem(key: string, val: string): Multi; - public rpush(key: string, ...vals: string[]): Multi; - public ltrim(key: string, start: number, end: number): Multi; - public incr(key: string): Multi; - public expire(key: string, seconds: number): Multi; - } -} +// class Multi { +// public del(key: string): Multi; +// public execAsync(): Promise; +// public get(key: string): Multi; +// public hgetall(key: string): Multi; +// public hmset(key: string, val: {[field: string]: any}): Multi; +// public hset(key: string, field: string, val: string): Multi; +// public sadd(key: string, val: string): Multi; +// public set(key: string, val: string): Multi; +// public setex(key: string, ttl: number, val: string): Multi; +// public ttl(key: string): Multi; +// public smembers(key: string): Multi; +// public srandmember(key: string): Multi; +// public srem(key: string, val: string): Multi; +// public rpush(key: string, ...vals: string[]): Multi; +// public ltrim(key: string, start: number, end: number): Multi; +// public incr(key: string): Multi; +// public expire(key: string, seconds: number): Multi; +// } +// } diff --git a/test/gen-server/lib/DocWorkerMap.ts b/test/gen-server/lib/DocWorkerMap.ts index 9bcf5c4a52..7b12be7146 100644 --- a/test/gen-server/lib/DocWorkerMap.ts +++ b/test/gen-server/lib/DocWorkerMap.ts @@ -3,39 +3,38 @@ import {DocStatus, DocWorkerInfo, IDocWorkerMap} from 'app/server/lib/DocWorkerM import {FlexServer} from 'app/server/lib/FlexServer'; import {Permit} from 'app/server/lib/Permit'; import {MergedServer} from "app/server/MergedServer"; -import {delay, promisifyAll} from 'bluebird'; +import {delay} from 'bluebird'; import {assert, expect} from 'chai'; import {countBy, values} from 'lodash'; -import {createClient, RedisClient} from 'redis'; +import {createClient, RedisClientType} from 'redis'; import {TestSession} from 'test/gen-server/apiUtils'; import {createInitialDb, removeConnection, setUpDB} from 'test/gen-server/seed'; import sinon from 'sinon'; import * as testUtils from 'test/server/testUtils'; -promisifyAll(RedisClient.prototype); describe('DocWorkerMap', function() { - let cli: RedisClient; + let cli: RedisClientType; testUtils.setTmpLogLevel('error'); before(async function() { if (!process.env.TEST_REDIS_URL) { this.skip(); } - cli = createClient(process.env.TEST_REDIS_URL); - await cli.flushdbAsync(); + cli = createClient({url:process.env.TEST_REDIS_URL}); + await cli.flushDb(); }); after(async function() { - if (cli) { await cli.quitAsync(); } + if (cli) { await cli.quit(); } }); beforeEach(async function() { - if (cli) { await cli.delAsync('groups'); } + if (cli) { await cli.del('groups'); } }); afterEach(async function() { - if (cli) { await cli.flushdbAsync(); } + if (cli) { await cli.flushDb(); } }); it('can assign a worker when available', async function() { @@ -158,25 +157,25 @@ describe('DocWorkerMap', function() { this.timeout(5000); // Say we want one worker reserved for "blizzard" and two for "funkytown" - await cli.hmsetAsync('groups', { + await cli.hSet('groups', { blizzard: 1, funkytown: 2, }); for (let i = 0; i < 20; i++) { - await cli.setAsync(`doc-blizzard${i}-group`, 'blizzard'); - await cli.setAsync(`doc-funkytown${i}-group`, 'funkytown'); + await cli.set(`doc-blizzard${i}-group`, 'blizzard'); + await cli.set(`doc-funkytown${i}-group`, 'funkytown'); } let workers = new DocWorkerMap([cli], 'ver1'); for (let i = 0; i < 5; i++) { await workers.addWorker({id: `worker${i}`, internalUrl: 'internal', publicUrl: 'public'}); await workers.setWorkerAvailability(`worker${i}`, true); } - let elections = await cli.hgetallAsync('elections-ver1'); + let elections = await cli.hGetAll('elections-ver1'); assert.deepEqual(elections, { blizzard: '["worker0"]', funkytown: '["worker1","worker2"]' }); - assert.sameMembers(await cli.smembersAsync('workers-available-blizzard'), ['worker0']); - assert.sameMembers(await cli.smembersAsync('workers-available-funkytown'), ['worker1', 'worker2']); - assert.sameMembers(await cli.smembersAsync('workers-available-default'), ['worker3', 'worker4']); - assert.sameMembers(await cli.smembersAsync('workers-available'), + assert.sameMembers(await cli.sMembers('workers-available-blizzard'), ['worker0']); + assert.sameMembers(await cli.sMembers('workers-available-funkytown'), ['worker1', 'worker2']); + assert.sameMembers(await cli.sMembers('workers-available-default'), ['worker3', 'worker4']); + assert.sameMembers(await cli.sMembers('workers-available'), ['worker0', 'worker1', 'worker2', 'worker3', 'worker4']); for (let i = 0; i < 20; i++) { const assignment = await workers.assignDocWorker(`blizzard${i}`); @@ -215,13 +214,13 @@ describe('DocWorkerMap', function() { await workers.addWorker({id: `worker${i}_v2`, internalUrl: 'internal', publicUrl: 'public'}); await workers.setWorkerAvailability(`worker${i}_v2`, true); } - assert.sameMembers(await cli.smembersAsync('workers-available-blizzard'), + assert.sameMembers(await cli.sMembers('workers-available-blizzard'), ['worker5', 'worker0_v2']); - assert.sameMembers(await cli.smembersAsync('workers-available-funkytown'), + assert.sameMembers(await cli.sMembers('workers-available-funkytown'), ['worker2', 'worker6', 'worker1_v2', 'worker2_v2']); - assert.sameMembers(await cli.smembersAsync('workers-available-default'), + assert.sameMembers(await cli.sMembers('workers-available-default'), ['worker3', 'worker4', 'worker3_v2', 'worker4_v2']); - assert.sameMembers(await cli.smembersAsync('workers-available'), + assert.sameMembers(await cli.sMembers('workers-available'), ['worker2', 'worker3', 'worker4', 'worker5', 'worker6', 'worker0_v2', 'worker1_v2', 'worker2_v2', 'worker3_v2', 'worker4_v2']); @@ -233,13 +232,13 @@ describe('DocWorkerMap', function() { // check everything looks as expected workers = new DocWorkerMap([cli], 'ver2'); - elections = await cli.hgetallAsync('elections-ver2'); + elections = await cli.hGetAll('elections-ver2'); assert.deepEqual(elections, { blizzard: '["worker0_v2"]', funkytown: '["worker1_v2","worker2_v2"]' }); - assert.sameMembers(await cli.smembersAsync('workers-available-blizzard'), ['worker0_v2']); - assert.sameMembers(await cli.smembersAsync('workers-available-funkytown'), ['worker1_v2', 'worker2_v2']); - assert.sameMembers(await cli.smembersAsync('workers-available-default'), ['worker3_v2', 'worker4_v2']); - assert.sameMembers(await cli.smembersAsync('workers-available'), + assert.sameMembers(await cli.sMembers('workers-available-blizzard'), ['worker0_v2']); + assert.sameMembers(await cli.sMembers('workers-available-funkytown'), ['worker1_v2', 'worker2_v2']); + assert.sameMembers(await cli.sMembers('workers-available-default'), ['worker3_v2', 'worker4_v2']); + assert.sameMembers(await cli.sMembers('workers-available'), ['worker0_v2', 'worker1_v2', 'worker2_v2', 'worker3_v2', 'worker4_v2']); for (let i = 0; i < 20; i++) { const assignment = await workers.assignDocWorker(`blizzard${i}`); @@ -255,7 +254,7 @@ describe('DocWorkerMap', function() { } // check everything about previous deployment got cleaned up - assert.equal(await cli.hgetallAsync('elections-ver1'), null); + assert.equal(await cli.hGetAll('elections-ver1'), null); }); it('can assign workers to groups', async function() { @@ -274,21 +273,21 @@ describe('DocWorkerMap', function() { await workers.setWorkerAvailability('worker_secondary', true); // Check that worker lists look sane. - assert.sameMembers(await cli.smembersAsync('workers'), + assert.sameMembers(await cli.sMembers('workers'), ['worker0', 'worker1', 'worker2', 'worker_secondary']); - assert.sameMembers(await cli.smembersAsync('workers-available'), + assert.sameMembers(await cli.sMembers('workers-available'), ['worker0', 'worker1', 'worker2']); - assert.sameMembers(await cli.smembersAsync('workers-available-default'), + assert.sameMembers(await cli.sMembers('workers-available-default'), ['worker0', 'worker1', 'worker2']); - assert.sameMembers(await cli.smembersAsync('workers-available-secondary'), + assert.sameMembers(await cli.sMembers('workers-available-secondary'), ['worker_secondary']); // Check that worker-*-group keys are as expected. - assert.equal(await cli.getAsync('worker-worker_secondary-group'), 'secondary'); - assert.equal(await cli.getAsync('worker-worker0-group'), null); + assert.equal(await cli.get('worker-worker_secondary-group'), 'secondary'); + assert.equal(await cli.get('worker-worker0-group'), null); // Check that a doc for the special group is assigned to the correct worker. - await cli.setAsync('doc-funkydoc-group', 'secondary'); + await cli.set('doc-funkydoc-group', 'secondary'); assert.equal((await workers.assignDocWorker('funkydoc')).docWorker.id, 'worker_secondary'); // Check that other docs don't end up on the special group's worker. @@ -448,7 +447,7 @@ describe('DocWorkerMap', function() { assert.equal((await workers.getDocWorker(doc1))?.docWorker.id, 'worker1'); // Set doc to "special" group. - await cli.setAsync(`doc-${doc1}-group`, 'special'); + await cli.set(`doc-${doc1}-group`, 'special'); // Check doc gets reassigned to correct worker. assert.equal(await (await api.testRequest(`${api.getBaseUrl()}/api/docs/${doc1}/assign`, { @@ -458,7 +457,7 @@ describe('DocWorkerMap', function() { assert.equal((await workers.getDocWorker(doc1))?.docWorker.id, 'worker2'); // Set doc to "other" group. - await cli.setAsync(`doc-${doc1}-group`, 'other'); + await cli.set(`doc-${doc1}-group`, 'other'); // Check doc gets reassigned to one of the correct workers. assert.equal(await (await api.testRequest(`${api.getBaseUrl()}/api/docs/${doc1}/assign`, { @@ -468,7 +467,7 @@ describe('DocWorkerMap', function() { assert.oneOf((await workers.getDocWorker(doc1))?.docWorker.id, ['worker3', 'worker4']); // Remove doc from groups. - await cli.delAsync(`doc-${doc1}-group`); + await cli.del(`doc-${doc1}-group`); assert.equal(await (await api.testRequest(`${api.getBaseUrl()}/api/docs/${doc1}/assign`, { method: 'POST' })).json(), true); diff --git a/test/server/lib/DocApi.ts b/test/server/lib/DocApi.ts index b327cabfb2..991b2a179f 100644 --- a/test/server/lib/DocApi.ts +++ b/test/server/lib/DocApi.ts @@ -29,7 +29,7 @@ import {AbortController} from 'node-abort-controller'; import fetch from 'node-fetch'; import {tmpdir} from 'os'; import * as path from 'path'; -import {createClient, RedisClient} from 'redis'; +import {createClient, RedisClientType} from 'redis'; import * as sinon from 'sinon'; import {configForUser} from 'test/gen-server/testUtils'; import {serveSomething, Serving} from 'test/server/customUtil'; @@ -3780,13 +3780,13 @@ function testDocApi(settings: { }); describe("Daily API Limit", () => { - let redisClient: RedisClient; + let redisClient: RedisClientType; before(async function () { if (!process.env.TEST_REDIS_URL) { this.skip(); } - redisClient = createClient(process.env.TEST_REDIS_URL); + redisClient = createClient({url:process.env.TEST_REDIS_URL}); }); it("limits daily API usage", async function () { @@ -3834,7 +3834,7 @@ function testDocApi(settings: { .set(currentHour, String(used)) .set(nextDay, String(used)) .set(nextHour, String(used)) - .execAsync(); + .exec(); // Make 9 requests. The first 4 should succeed by fitting into the allocation for the minute. // (Free team plans get 5000 requests per day, and 5000/24/60 ~= 3.47 which is rounded up to 4) @@ -3867,7 +3867,7 @@ function testDocApi(settings: { .ttl(minute) .ttl(hour) .ttl(day) - .execAsync(), + .exec(), [ 2 * 60, 2 * 60 * 60, @@ -3881,7 +3881,7 @@ function testDocApi(settings: { .get(minute) .get(hour) .get(day) - .execAsync(), + .exec(), [ String(i), String(used + (first ? 1 : i - 1)), @@ -3942,7 +3942,7 @@ function testDocApi(settings: { after(async function () { if (process.env.TEST_REDIS_URL) { - await redisClient.quitAsync(); + await redisClient.quit(); } }); }); @@ -4174,7 +4174,7 @@ function testDocApi(settings: { }; redisCalls = []; - redisMonitor = createClient(process.env.TEST_REDIS_URL); + redisMonitor = createClient({url:process.env.TEST_REDIS_URL}); redisMonitor.monitor(); redisMonitor.on("monitor", (_time: any, args: any, _rawReply: any) => { redisCalls.push(args); @@ -5436,8 +5436,8 @@ async function flushAuth() { async function flushAllRedis() { // Clear redis test database if redis is in use. if (process.env.TEST_REDIS_URL) { - const cli = createClient(process.env.TEST_REDIS_URL); - await cli.flushdbAsync(); - await cli.quitAsync(); + const cli = createClient({url:process.env.TEST_REDIS_URL}); + await cli.flushDb(); + await cli.quit(); } } diff --git a/test/server/lib/HostedStorageManager.ts b/test/server/lib/HostedStorageManager.ts index 830e8f50ce..dc590c7d43 100644 --- a/test/server/lib/HostedStorageManager.ts +++ b/test/server/lib/HostedStorageManager.ts @@ -31,7 +31,7 @@ import * as bluebird from 'bluebird'; import {assert} from 'chai'; import * as fse from 'fs-extra'; import * as path from 'path'; -import {createClient, RedisClient} from 'redis'; +import {createClient, RedisClientType} from 'redis'; import * as sinon from 'sinon'; import {createInitialDb, removeConnection, setUpDB} from 'test/gen-server/seed'; import {createTmpDir, getGlobalPluginManager} from 'test/server/docTools'; @@ -39,8 +39,6 @@ import {EnvironmentSnapshot, setTmpLogLevel, useFixtureDoc} from 'test/server/te import {waitForIt} from 'test/server/wait'; import {v4 as uuidv4} from 'uuid'; -bluebird.promisifyAll(RedisClient.prototype); - /** * An in-memory store, for testing. */ @@ -404,16 +402,16 @@ describe('HostedStorageManager', function() { let oldEnv: EnvironmentSnapshot; const workerId = 'dw17'; - let cli: RedisClient; + let cli: RedisClientType; let store: TestStore; let workers: DocWorkerMap; let tmpDir: string; before(async function() { if (!process.env.TEST_REDIS_URL) { this.skip(); return; } - cli = createClient(process.env.TEST_REDIS_URL); + cli = createClient({url:process.env.TEST_REDIS_URL}); oldEnv = new EnvironmentSnapshot(); - await cli.flushdbAsync(); + await cli.flushDb(); workers = new DocWorkerMap([cli]); await workers.addWorker({ id: workerId, @@ -468,7 +466,7 @@ describe('HostedStorageManager', function() { after(async function() { await store?.storageManager.testStopOperations(); await workers?.removeWorker(workerId); - await cli?.quitAsync(); + await cli?.quit(); }); beforeEach(function() { @@ -485,18 +483,18 @@ describe('HostedStorageManager', function() { }); async function getRedisChecksum(docId: string): Promise { - return (await cli.getAsync(`doc-${docId}-checksum`)) || ''; + return (await cli.get(`doc-${docId}-checksum`)) || ''; } - async function setRedisChecksum(docId: string, checksum: string): Promise<'OK'> { - return cli.setAsync(`doc-${docId}-checksum`, checksum); + async function setRedisChecksum(docId: string, checksum: string) { + return cli.set(`doc-${docId}-checksum`, checksum); } async function dropAllChecksums() { // `keys` is a potentially slow, unrecommended operation - but ok in test scenario // against a test instance of redis. - for (const key of await cli.keysAsync('*-checksum')) { - await cli.delAsync(key); + for (const key of await cli.keys('*-checksum')) { + await cli.del(key); } } diff --git a/test/server/lib/Webhooks-Proxy.ts b/test/server/lib/Webhooks-Proxy.ts index 505751f2f8..f4f5eb0030 100644 --- a/test/server/lib/Webhooks-Proxy.ts +++ b/test/server/lib/Webhooks-Proxy.ts @@ -32,9 +32,9 @@ let serverUrl: string; let userApi: UserAPIImpl; async function cleanRedisDatabase() { - const cli = createClient(process.env.TEST_REDIS_URL); - await cli.flushdbAsync(); - await cli.quitAsync(); + const cli = createClient({url:process.env.TEST_REDIS_URL}); + await cli.flushDb(); + await cli.quit(); } function backupEnvironmentVariables() { @@ -242,7 +242,7 @@ describe('Webhooks-Proxy', function () { if (process.env.TEST_REDIS_URL) { - redisMonitor = createClient(process.env.TEST_REDIS_URL); + redisMonitor = createClient({url:process.env.TEST_REDIS_URL}); } }); diff --git a/yarn.lock b/yarn.lock index d85315042c..2a52293d9d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -307,7 +307,7 @@ version "0.5.7" resolved "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== - + "@esbuild/aix-ppc64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" @@ -821,6 +821,40 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== +"@redis/bloom@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@redis/bloom/-/bloom-1.2.0.tgz#d3fd6d3c0af3ef92f26767b56414a370c7b63b71" + integrity sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg== + +"@redis/client@1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@redis/client/-/client-1.6.0.tgz#dcf4ae1319763db6fdddd6de7f0af68a352c30ea" + integrity sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg== + dependencies: + cluster-key-slot "1.1.2" + generic-pool "3.9.0" + yallist "4.0.0" + +"@redis/graph@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@redis/graph/-/graph-1.1.1.tgz#8c10df2df7f7d02741866751764031a957a170ea" + integrity sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw== + +"@redis/json@1.0.7": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@redis/json/-/json-1.0.7.tgz#016257fcd933c4cbcb9c49cde8a0961375c6893b" + integrity sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ== + +"@redis/search@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@redis/search/-/search-1.2.0.tgz#50976fd3f31168f585666f7922dde111c74567b8" + integrity sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw== + +"@redis/time-series@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@redis/time-series/-/time-series-1.1.0.tgz#cba454c05ec201bd5547aaf55286d44682ac8eb5" + integrity sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g== + "@sinonjs/commons@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd" @@ -2023,7 +2057,7 @@ block-stream2@^2.1.0: dependencies: readable-stream "^3.4.0" -bluebird@^3.3.3, bluebird@^3.5.0: +bluebird@^3.5.0: version "3.7.2" resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -2634,7 +2668,7 @@ clone@^2.1.2: resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w== -cluster-key-slot@^1.1.0: +cluster-key-slot@1.1.2, cluster-key-slot@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac" integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA== @@ -2763,13 +2797,10 @@ concat-stream@~1.5.0, concat-stream@~1.5.1: readable-stream "~2.0.0" typedarray "~0.0.5" -connect-redis@3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/connect-redis/-/connect-redis-3.4.0.tgz#4040dd3755bddbf93478fb84937a74052c31b965" - integrity sha512-YKPSO9tLwzUr8jzhsGMdSJUxevWrDt0ggXRcTMb+mtnJ/vWGlWV7RC4VUMgqvZv3uTGDFye8Bf7d6No0oSVkOQ== - dependencies: - debug "^4.0.1" - redis "^2.8.0" +connect-redis@8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/connect-redis/-/connect-redis-8.0.1.tgz#83d4d829ee968b9d64da9a6f40fe1ea3046bf835" + integrity sha512-7iOI214/r15ahvu0rqKCHhsgpMdOgyLwqlw/icSTnnAR75xFvMyfxAE+je4M87rZLjDlKzKcTc48XxQXYFsMgA== console-browserify@^1.1.0: version "1.2.0" @@ -3013,7 +3044,7 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@~4.3.1: +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@~4.3.1: version "4.3.7" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== @@ -3095,11 +3126,6 @@ delegates@^1.0.0: resolved "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz" integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= -denque@^1.5.0: - version "1.5.1" - resolved "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz" - integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== - denque@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" @@ -3221,7 +3247,7 @@ dotenv@^16.0.3: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== -double-ended-queue@2.1.0-0, double-ended-queue@^2.1.0-0: +double-ended-queue@2.1.0-0: version "2.1.0-0" resolved "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz" integrity sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw= @@ -3663,6 +3689,33 @@ express-rate-limit@7.2.0: resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-7.2.0.tgz#06ce387dd5388f429cab8263c514fc07bf90a445" integrity sha512-T7nul1t4TNyfZMJ7pKRKkdeVJWa2CqB8NA1P8BwYaoDI5QSBZARv5oMS43J7b7I5P+4asjVXjb7ONuwDKucahg== +express-session@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.18.1.tgz#88d0bbd41878882840f24ec6227493fcb167e8d5" + integrity sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA== + dependencies: + cookie "0.7.2" + cookie-signature "1.0.7" + debug "2.6.9" + depd "~2.0.0" + on-headers "~1.0.2" + parseurl "~1.3.3" + safe-buffer "5.2.1" + uid-safe "~2.1.5" + +"express-session@https://registry.npmjs.org/@gristlabs/express-session/-/express-session-1.18.1-grist1.tgz": + version "1.18.1-grist1" + resolved "https://registry.npmjs.org/@gristlabs/express-session/-/express-session-1.18.1-grist1.tgz#8097d2efa29459534fb4706cfc01432d631f701d" + dependencies: + cookie "0.7.2" + cookie-signature "1.0.7" + debug "2.6.9" + depd "~2.0.0" + on-headers "~1.0.2" + parseurl "~1.3.3" + safe-buffer "5.2.1" + uid-safe "~2.1.5" + express@4.21.2: version "4.21.2" resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" @@ -4045,6 +4098,11 @@ gcp-metadata@^6.1.0: gaxios "^6.0.0" json-bigint "^1.0.0" +generic-pool@3.9.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-3.9.0.tgz#36f4a678e963f4fdb8707eab050823abc4e8f5e4" + integrity sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g== + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -5861,7 +5919,7 @@ node-abort-controller@3.0.1: resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.0.1.tgz#f91fa50b1dee3f909afabb7e261b1e1d6b0cb74e" integrity sha512-/ujIVxthRs+7q6hsdjHMaj8hRG9NuWmwrz+JdRwZ14jdFoKSkm+vDsCbF9PLpnSqjaWQJuTmVtcWHNLr+vrOFw== -node-abort-controller@^3.1.1: +node-abort-controller@^3.0.1, node-abort-controller@^3.1.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== @@ -6720,21 +6778,11 @@ rechoir@^0.7.0: dependencies: resolve "^1.9.0" -redis-commands@^1.2.0, redis-commands@^1.7.0: - version "1.7.0" - resolved "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz" - integrity sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ== - redis-errors@^1.0.0, redis-errors@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz" integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w== -redis-parser@^2.6.0: - version "2.6.0" - resolved "https://registry.npmjs.org/redis-parser/-/redis-parser-2.6.0.tgz" - integrity sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs= - redis-parser@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz" @@ -6742,31 +6790,24 @@ redis-parser@^3.0.0: dependencies: redis-errors "^1.0.0" -redis@3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/redis/-/redis-3.1.1.tgz" - integrity sha512-QhkKhOuzhogR1NDJfBD34TQJz2ZJwDhhIC6ZmvpftlmfYShHHQXjjNspAJ+Z2HH5NwSBVYBVganbiZ8bgFMHjg== +redis@4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/redis/-/redis-4.7.0.tgz#b401787514d25dd0cfc22406d767937ba3be55d6" + integrity sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ== dependencies: - denque "^1.5.0" - redis-commands "^1.7.0" - redis-errors "^1.2.0" - redis-parser "^3.0.0" + "@redis/bloom" "1.2.0" + "@redis/client" "1.6.0" + "@redis/graph" "1.1.1" + "@redis/json" "1.0.7" + "@redis/search" "1.2.0" + "@redis/time-series" "1.1.0" -redis@^2.8.0: - version "2.8.0" - resolved "https://registry.npmjs.org/redis/-/redis-2.8.0.tgz" - integrity sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A== +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: - double-ended-queue "^2.1.0-0" - redis-commands "^1.2.0" - redis-parser "^2.6.0" - -redlock@3.1.2: - version "3.1.2" - resolved "https://registry.npmjs.org/redlock/-/redlock-3.1.2.tgz" - integrity sha512-CKXhOvLd4In5QOfbcF0GIcSsa+pL9JPZd+eKeMk/Sydxh93NUNtwQMTIKFqwPu3PjEgDStwYFJTurVzNA33pZw== - dependencies: - bluebird "^3.3.3" + node-abort-controller "^3.0.1" reflect-metadata@^0.1.13: version "0.1.14" @@ -8455,16 +8496,16 @@ y18n@^5.0.5: resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== +yallist@4.0.0, yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + yallist@^3.0.2: version "3.1.1" resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - yargs-parser@^13.1.2: version "13.1.2" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"