fix(backend): 処理に失敗するとidempotentキーを削除してしまいその後のリクエストが通ってしまう問題を修正 (MisskeyIO#591)

This commit is contained in:
まっちゃとーにゅ 2024-04-01 18:44:44 +09:00 committed by GitHub
parent 92bdc2e054
commit 31ebd77e8a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 79 additions and 8 deletions

View file

@ -0,0 +1,29 @@
import { Result, Callback } from 'ioredis';
declare module 'ioredis' {
interface RedisCommander<Context> {
/*
* 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<string>): Result<string, Context>;
/*
* 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<string>): Result<string, Context>;
}
}

View file

@ -47,7 +47,7 @@ const $meilisearch: Provider = {
const $redis: Provider = { const $redis: Provider = {
provide: DI.redis, provide: DI.redis,
useFactory: (config: Config) => { useFactory: (config: Config) => {
return new Redis.Redis({ const redis = new Redis.Redis({
...config.redis, ...config.redis,
reconnectOnError: (err: Error) => { reconnectOnError: (err: Error) => {
if ( err.message.includes('READONLY') if ( err.message.includes('READONLY')
@ -57,6 +57,27 @@ const $redis: Provider = {
return 1; 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], inject: [DI.config],
}; };
@ -101,7 +122,7 @@ const $redisForSub: Provider = {
const $redisForTimelines: Provider = { const $redisForTimelines: Provider = {
provide: DI.redisForTimelines, provide: DI.redisForTimelines,
useFactory: (config: Config) => { useFactory: (config: Config) => {
return new Redis.Redis({ const redis = new Redis.Redis({
...config.redisForTimelines, ...config.redisForTimelines,
reconnectOnError: (err: Error) => { reconnectOnError: (err: Error) => {
if ( err.message.includes('READONLY') if ( err.message.includes('READONLY')
@ -111,6 +132,27 @@ const $redisForTimelines: Provider = {
return 1; 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], inject: [DI.config],
}; };

View file

@ -173,8 +173,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
logger.info('Successfully created drive file.', { fileId: driveFile.id }); logger.info('Successfully created drive file.', { fileId: driveFile.id });
return await this.driveFileEntityService.pack(driveFile, me, { self: true }); return await this.driveFileEntityService.pack(driveFile, me, { self: true });
} catch (e) { } 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 }); logger.error('Failed to create drive file.', { error: e });

View file

@ -117,8 +117,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}); });
}, },
async err => { 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 }); logger.error('Failed to upload from URL.', { error: err });
}, },
); );

View file

@ -445,8 +445,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
createdNote: await this.noteEntityService.pack(note, me), createdNote: await this.noteEntityService.pack(note, me),
}; };
} catch (err) { } 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 }); logger.error('Failed to create a note.', { error: err });