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

View file

@ -14,6 +14,7 @@ import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { FeaturedService } from '@/core/FeaturedService.js';
import { MiInstance } from '@/models/Instance.js';
import { diffArrays } from '@/misc/diff-arrays.js';
import type { MetasRepository } from '@/models/_.js';
import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable()
@ -28,6 +29,9 @@ export class MetaService implements OnApplicationShutdown {
@Inject(DI.db)
private db: DataSource,
@Inject(DI.metasRepository)
private readonly metasRepository: MetasRepository,
private featuredService: FeaturedService,
private globalEventService: GlobalEventService,
) {
@ -69,35 +73,31 @@ export class MetaService implements OnApplicationShutdown {
public async fetch(noCache = false): Promise<MiMeta> {
if (!noCache && this.cache) return this.cache;
return await this.db.transaction(async transactionalEntityManager => {
// 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する
const metas = await transactionalEntityManager.find(MiMeta, {
let meta = await this.metasRepository.findOne({
order: {
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;
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