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 });