From 31ebd77e8a449b3c021f9393e83c70c6521967ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?= =?UTF-8?q?=E3=81=AB=E3=82=85?= <17376330+u1-liquid@users.noreply.github.com> Date: Mon, 1 Apr 2024 18:44:44 +0900 Subject: [PATCH] =?UTF-8?q?fix(backend):=20=E5=87=A6=E7=90=86=E3=81=AB?= =?UTF-8?q?=E5=A4=B1=E6=95=97=E3=81=99=E3=82=8B=E3=81=A8idempotent?= =?UTF-8?q?=E3=82=AD=E3=83=BC=E3=82=92=E5=89=8A=E9=99=A4=E3=81=97=E3=81=A6?= =?UTF-8?q?=E3=81=97=E3=81=BE=E3=81=84=E3=81=9D=E3=81=AE=E5=BE=8C=E3=81=AE?= =?UTF-8?q?=E3=83=AA=E3=82=AF=E3=82=A8=E3=82=B9=E3=83=88=E3=81=8C=E9=80=9A?= =?UTF-8?q?=E3=81=A3=E3=81=A6=E3=81=97=E3=81=BE=E3=81=86=E5=95=8F=E9=A1=8C?= =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3=20(MisskeyIO#591)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/@types/ioredis.d.ts | 29 ++++++++++++ packages/backend/src/GlobalModule.ts | 46 ++++++++++++++++++- .../api/endpoints/drive/files/create.ts | 4 +- .../endpoints/drive/files/upload-from-url.ts | 4 +- .../src/server/api/endpoints/notes/create.ts | 4 +- 5 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 packages/backend/src/@types/ioredis.d.ts diff --git a/packages/backend/src/@types/ioredis.d.ts b/packages/backend/src/@types/ioredis.d.ts new file mode 100644 index 0000000000..b168bc369f --- /dev/null +++ b/packages/backend/src/@types/ioredis.d.ts @@ -0,0 +1,29 @@ +import { Result, Callback } from 'ioredis'; + +declare module 'ioredis' { + interface RedisCommander { + /* + * Set value if key has the specified value. + * + * lua script: + * if redis.call('GET', KEYS[1]) == ARGV[1] then + * return redis.call('SET', KEYS[1], ARGV[2]) + * else + * return 0 + * end + */ + setIf(key: string, value: string, newValue: string, callback?: Callback): Result; + + /* + * Unlink key if key has the specified value. + * + * lua script: + * if redis.call('GET', KEYS[1]) == ARGV[1] then + * return redis.call('UNLINK', KEYS[1]) + * else + * return 0 + * end + */ + unlinkIf(key: string, value: string, callback?: Callback): Result; + } +} diff --git a/packages/backend/src/GlobalModule.ts b/packages/backend/src/GlobalModule.ts index a09221e650..5669c8f071 100644 --- a/packages/backend/src/GlobalModule.ts +++ b/packages/backend/src/GlobalModule.ts @@ -47,7 +47,7 @@ const $meilisearch: Provider = { const $redis: Provider = { provide: DI.redis, useFactory: (config: Config) => { - return new Redis.Redis({ + const redis = new Redis.Redis({ ...config.redis, reconnectOnError: (err: Error) => { if ( err.message.includes('READONLY') @@ -57,6 +57,27 @@ const $redis: Provider = { return 1; }, }); + redis.defineCommand('setIf', { + numberOfKeys: 1, + lua: ` + if redis.call('GET', KEYS[1]) == ARGV[1] then + return redis.call('SET', KEYS[1], ARGV[2]) + else + return 0 + end + `, + }); + redis.defineCommand('unlinkIf', { + numberOfKeys: 1, + lua: ` + if redis.call('GET', KEYS[1]) == ARGV[1] then + return redis.call('UNLINK', KEYS[1]) + else + return 0 + end + `, + }); + return redis; }, inject: [DI.config], }; @@ -101,7 +122,7 @@ const $redisForSub: Provider = { const $redisForTimelines: Provider = { provide: DI.redisForTimelines, useFactory: (config: Config) => { - return new Redis.Redis({ + const redis = new Redis.Redis({ ...config.redisForTimelines, reconnectOnError: (err: Error) => { if ( err.message.includes('READONLY') @@ -111,6 +132,27 @@ const $redisForTimelines: Provider = { return 1; }, }); + redis.defineCommand('setIf', { + numberOfKeys: 1, + lua: ` + if redis.call('GET', KEYS[1]) == ARGV[1] then + return redis.call('SET', KEYS[1], ARGV[2]) + else + return 0 + end + `, + }); + redis.defineCommand('unlinkIf', { + numberOfKeys: 1, + lua: ` + if redis.call('GET', KEYS[1]) == ARGV[1] then + return redis.call('UNLINK', KEYS[1]) + else + return 0 + end + `, + }); + return redis; }, inject: [DI.config], }; diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts index 9083ef15f5..6de619b432 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts @@ -173,8 +173,8 @@ export default class extends Endpoint { // eslint- logger.info('Successfully created drive file.', { fileId: driveFile.id }); return await this.driveFileEntityService.pack(driveFile, me, { self: true }); } catch (e) { - // エラーが発生した場合、リクエストの処理結果を削除 - await this.redisClient.unlink(`drive:files:create:idempotent:${me.id}:${hash}`); + // エラーが発生した場合、まだ処理中として記録されている場合はリクエストの処理結果を削除 + await this.redisClient.unlinkIf(`drive:files:create:idempotent:${me.id}:${hash}`, '_'); logger.error('Failed to create drive file.', { error: e }); diff --git a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts index cecd63a478..46475b452d 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts @@ -117,8 +117,8 @@ export default class extends Endpoint { // eslint- }); }, async err => { - // エラーが発生した場合、リクエストの処理結果を削除 - await this.redisClient.unlink(`drive:files:upload-from-url:idempotent:${me.id}:${hash}`); + // エラーが発生した場合、まだ処理中として記録されている場合はリクエストの処理結果を削除 + await this.redisClient.unlinkIf(`drive:files:upload-from-url:idempotent:${me.id}:${hash}`, '_'); logger.error('Failed to upload from URL.', { error: err }); }, ); diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 2e47eb89e3..9c42c0af31 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -445,8 +445,8 @@ export default class extends Endpoint { // eslint- createdNote: await this.noteEntityService.pack(note, me), }; } catch (err) { - // エラーが発生した場合、リクエストの処理結果を削除 - await this.redisForTimelines.unlink(`note:idempotent:${me.id}:${hash}`); + // エラーが発生した場合、まだ処理中として記録されている場合はリクエストの処理結果を削除 + await this.redisForTimelines.unlinkIf(`note:idempotent:${me.id}:${hash}`, '_'); logger.error('Failed to create a note.', { error: err });