expose federatedInstanceCache for access to bulk cache APIs

This commit is contained in:
Hazelnoot 2025-08-14 01:20:30 -04:00
parent fbf6fc0c54
commit 4b37bb23a2

View file

@ -5,37 +5,72 @@
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 { In } from 'typeorm';
import type { InstancesRepository, MiMeta } from '@/models/_.js'; import type { InstancesRepository, MiMeta } from '@/models/_.js';
import type { MiInstance } from '@/models/Instance.js'; import type { MiInstance } from '@/models/Instance.js';
import { MemoryKVCache } 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 type { GlobalEvents } from '@/core/GlobalEventService.js'; import { diffArraysSimple } from '@/misc/diff-arrays.js';
import { Serialized } from '@/types.js'; import { QuantumKVCache } from '@/misc/QuantumKVCache.js';
import { diffArrays, diffArraysSimple } from '@/misc/diff-arrays.js'; import { InternalEventService } from '@/core/InternalEventService.js';
import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
@Injectable() @Injectable()
export class FederatedInstanceService implements OnApplicationShutdown { export class FederatedInstanceService implements OnApplicationShutdown {
private readonly federatedInstanceCache: MemoryKVCache<MiInstance | null>; public readonly federatedInstanceCache: QuantumKVCache<MiInstance>;
constructor( constructor(
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
@Inject(DI.instancesRepository) @Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository, private instancesRepository: InstancesRepository,
@Inject(DI.meta)
private readonly meta: MiMeta,
private utilityService: UtilityService, private utilityService: UtilityService,
private idService: IdService, private idService: IdService,
private readonly internalEventService: InternalEventService,
) { ) {
this.federatedInstanceCache = new MemoryKVCache(1000 * 60 * 3); // 3m this.federatedInstanceCache = new QuantumKVCache(this.internalEventService, 'federatedInstance', {
this.redisForSub.on('message', this.onMessage); lifetime: 1000 * 60 * 3, // 3 minutes
fetcher: async key => {
const host = this.utilityService.toPuny(key);
let instance = await this.instancesRepository.findOneBy({ host });
if (instance == null) {
await this.instancesRepository.createQueryBuilder('instance')
.insert()
.values({
id: this.idService.gen(),
host,
firstRetrievedAt: new Date(),
isBlocked: this.utilityService.isBlockedHost(host),
isSilenced: this.utilityService.isSilencedHost(host),
isMediaSilenced: this.utilityService.isMediaSilencedHost(host),
isAllowListed: this.utilityService.isAllowListedHost(host),
isBubbled: this.utilityService.isBubbledHost(host),
})
.orIgnore()
.execute();
instance = await this.instancesRepository.findOneByOrFail({ host });
}
return instance;
},
bulkFetcher: async keys => {
const hosts = keys.map(key => this.utilityService.toPuny(key));
const instances = await this.instancesRepository.findBy({ host: In(hosts) });
return new Map(instances.map(i => [i.host, i]));
},
});
this.internalEventService.on('metaUpdated', this.onMetaUpdated);
} }
@bindThis @bindThis
public async fetchOrRegister(host: string): Promise<MiInstance> { public async fetchOrRegister(host: string): Promise<MiInstance> {
return this.federatedInstanceCache.fetch(host);
/*
host = this.utilityService.toPuny(host); host = this.utilityService.toPuny(host);
const cached = this.federatedInstanceCache.get(host); const cached = this.federatedInstanceCache.get(host);
@ -61,12 +96,15 @@ export class FederatedInstanceService implements OnApplicationShutdown {
index = await this.instancesRepository.findOneByOrFail({ host }); index = await this.instancesRepository.findOneByOrFail({ host });
} }
this.federatedInstanceCache.set(host, index); await this.federatedInstanceCache.set(host, index);
return index; return index;
*/
} }
@bindThis @bindThis
public async fetch(host: string): Promise<MiInstance | null> { public async fetch(host: string): Promise<MiInstance> {
return this.federatedInstanceCache.fetch(host);
/*
host = this.utilityService.toPuny(host); host = this.utilityService.toPuny(host);
const cached = this.federatedInstanceCache.get(host); const cached = this.federatedInstanceCache.get(host);
@ -75,29 +113,54 @@ export class FederatedInstanceService implements OnApplicationShutdown {
const index = await this.instancesRepository.findOneBy({ host }); const index = await this.instancesRepository.findOneBy({ host });
if (index == null) { if (index == null) {
this.federatedInstanceCache.set(host, null); await this.federatedInstanceCache.set(host, null);
return null; return null;
} else { } else {
this.federatedInstanceCache.set(host, index); await this.federatedInstanceCache.set(host, index);
return index; return index;
} }
*/
} }
@bindThis @bindThis
public async update(id: MiInstance['id'], data: Partial<MiInstance>): Promise<void> { public async update(id: MiInstance['id'], data: QueryDeepPartialEntity<MiInstance>): Promise<MiInstance> {
const result = await this.instancesRepository.createQueryBuilder().update() const result = await this.instancesRepository.createQueryBuilder().update()
.set(data) .set(data)
.where('id = :id', { id }) .where('id = :id', { id })
.returning('*') .returning('*')
.execute() .execute()
.then((response) => { .then((response) => {
return response.raw[0]; return response.raw[0] as MiInstance;
}); });
this.federatedInstanceCache.set(result.host, result); await this.federatedInstanceCache.set(result.host, result);
return result;
} }
private syncCache(before: Serialized<MiMeta | undefined>, after: Serialized<MiMeta>): void { /**
* Gets all instances in the allowlist (meta.federationHosts).
*/
@bindThis
public async getAllowList(): Promise<MiInstance[]> {
const allowedHosts = new Set(this.meta.federationHosts);
this.meta.blockedHosts.forEach(h => allowedHosts.delete(h));
const instances = await this.federatedInstanceCache.fetchMany(this.meta.federationHosts);
return instances.map(i => i[1]);
}
/**
* Gets all instances in the denylist (meta.blockedHosts).
*/
@bindThis
public async getDenyList(): Promise<MiInstance[]> {
const instances = await this.federatedInstanceCache.fetchMany(this.meta.blockedHosts);
return instances.map(i => i[1]);
}
// This gets fired *in each process* so don't do anything to trigger cache notifications!
private syncCache(before: MiMeta | undefined, after: MiMeta): void {
const changed = const changed =
diffArraysSimple(before?.blockedHosts, after.blockedHosts) || diffArraysSimple(before?.blockedHosts, after.blockedHosts) ||
diffArraysSimple(before?.silencedHosts, after.silencedHosts) || diffArraysSimple(before?.silencedHosts, after.silencedHosts) ||
@ -112,20 +175,13 @@ export class FederatedInstanceService implements OnApplicationShutdown {
} }
@bindThis @bindThis
private async onMessage(_: string, data: string): Promise<void> { private async onMetaUpdated(body: { before?: MiMeta; after: MiMeta; }) {
const obj = JSON.parse(data); this.syncCache(body.before, body.after);
if (obj.channel === 'internal') {
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
if (type === 'metaUpdated') {
this.syncCache(body.before, body.after);
}
}
} }
@bindThis @bindThis
public dispose(): void { public dispose(): void {
this.redisForSub.off('message', this.onMessage); this.internalEventService.off('metaUpdated', this.onMetaUpdated);
this.federatedInstanceCache.dispose(); this.federatedInstanceCache.dispose();
} }