avoid race conditions in meta / instance insert

This commit is contained in:
Hazelnoot 2025-05-25 08:44:45 -04:00
parent c0ead9cf11
commit 3e7ab07b3c
2 changed files with 37 additions and 41 deletions

View file

@ -5,15 +5,13 @@
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import { DataSource, QueryFailedError } from 'typeorm';
import type { InstancesRepository } from '@/models/_.js'; import type { InstancesRepository } from '@/models/_.js';
import { MiInstance } from '@/models/Instance.js'; import type { MiInstance } from '@/models/Instance.js';
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js'; import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
@Injectable() @Injectable()
export class FederatedInstanceService implements OnApplicationShutdown { export class FederatedInstanceService implements OnApplicationShutdown {
@ -26,9 +24,6 @@ export class FederatedInstanceService implements OnApplicationShutdown {
@Inject(DI.instancesRepository) @Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository, private instancesRepository: InstancesRepository,
@Inject(DI.db)
private readonly db: DataSource,
private utilityService: UtilityService, private utilityService: UtilityService,
private idService: IdService, private idService: IdService,
) { ) {
@ -58,11 +53,11 @@ export class FederatedInstanceService implements OnApplicationShutdown {
const cached = await this.federatedInstanceCache.get(host); const cached = await this.federatedInstanceCache.get(host);
if (cached) return cached; if (cached) return cached;
return await this.db.transaction(async tem => { let index = await this.instancesRepository.findOneBy({ host });
let index = await tem.findOneBy(MiInstance, { host });
if (index == null) { if (index == null) {
await tem.insert(MiInstance, { await this.instancesRepository.createQueryBuilder('instance')
.insert()
.values({
id: this.idService.gen(), id: this.idService.gen(),
host, host,
firstRetrievedAt: new Date(), firstRetrievedAt: new Date(),
@ -71,14 +66,15 @@ export class FederatedInstanceService implements OnApplicationShutdown {
isMediaSilenced: this.utilityService.isMediaSilencedHost(host), isMediaSilenced: this.utilityService.isMediaSilencedHost(host),
isAllowListed: this.utilityService.isAllowListedHost(host), isAllowListed: this.utilityService.isAllowListedHost(host),
isBubbled: this.utilityService.isBubbledHost(host), isBubbled: this.utilityService.isBubbledHost(host),
}); })
.orIgnore()
.execute();
index = await tem.findOneByOrFail(MiInstance, { host }); index = await this.instancesRepository.findOneByOrFail({ host });
} }
await this.federatedInstanceCache.set(host, index); await this.federatedInstanceCache.set(host, index);
return index; return index;
});
} }
@bindThis @bindThis

View file

@ -14,6 +14,7 @@ import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { FeaturedService } from '@/core/FeaturedService.js'; import { FeaturedService } from '@/core/FeaturedService.js';
import { MiInstance } from '@/models/Instance.js'; import { MiInstance } from '@/models/Instance.js';
import { diffArrays } from '@/misc/diff-arrays.js'; import { diffArrays } from '@/misc/diff-arrays.js';
import type { MetasRepository } from '@/models/_.js';
import type { OnApplicationShutdown } from '@nestjs/common'; import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable() @Injectable()
@ -28,6 +29,9 @@ export class MetaService implements OnApplicationShutdown {
@Inject(DI.db) @Inject(DI.db)
private db: DataSource, private db: DataSource,
@Inject(DI.metasRepository)
private readonly metasRepository: MetasRepository,
private featuredService: FeaturedService, private featuredService: FeaturedService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
) { ) {
@ -69,35 +73,31 @@ export class MetaService implements OnApplicationShutdown {
public async fetch(noCache = false): Promise<MiMeta> { public async fetch(noCache = false): Promise<MiMeta> {
if (!noCache && this.cache) return this.cache; if (!noCache && this.cache) return this.cache;
return await this.db.transaction(async transactionalEntityManager => {
// 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する // 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する
const metas = await transactionalEntityManager.find(MiMeta, { let meta = await this.metasRepository.findOne({
order: { order: {
id: 'DESC', id: 'DESC',
}, },
}); });
const meta = metas[0]; if (!meta) {
await this.metasRepository.createQueryBuilder('meta')
.insert()
.values({
id: 'x',
})
.orIgnore()
.execute();
meta = await this.metasRepository.findOneOrFail({
order: {
id: 'DESC',
},
});
}
if (meta) {
this.cache = meta; this.cache = meta;
return meta; return meta;
} else {
// metaが空のときfetchMetaが同時に呼ばれるとここが同時に呼ばれてしまうことがあるのでフェイルセーフなupsertを使う
const saved = await transactionalEntityManager
.upsert(
MiMeta,
{
id: 'x',
},
['id'],
)
.then((x) => transactionalEntityManager.findOneByOrFail(MiMeta, x.identifiers[0]));
this.cache = saved;
return saved;
}
});
} }
@bindThis @bindThis