no more infinity caches

This commit is contained in:
Namekuji 2023-07-02 22:10:33 -04:00
parent 9d26e08032
commit 284c0db1fd
15 changed files with 89 additions and 51 deletions

View file

@ -6,8 +6,8 @@ export class Cache<T> {
private ttl: number; private ttl: number;
private prefix: string; private prefix: string;
constructor(prefix: string, ttl: number) { constructor(prefix: string, ttlSeconds: number) {
this.ttl = ttl; this.ttl = ttlSeconds;
this.prefix = `cache:${prefix}`; this.prefix = `cache:${prefix}`;
} }
@ -15,26 +15,28 @@ export class Cache<T> {
return key ? `${this.prefix}:${key}` : this.prefix; return key ? `${this.prefix}:${key}` : this.prefix;
} }
public async set(key: string | null, value: T, transaction?: ChainableCommander): Promise<void> { public async set(
key: string | null,
value: T,
transaction?: ChainableCommander,
): Promise<void> {
const _key = this.prefixedKey(key); const _key = this.prefixedKey(key);
const _value = Buffer.from(encode(value)); const _value = Buffer.from(encode(value));
const commander = transaction ?? redisClient; const commander = transaction ?? redisClient;
if (this.ttl === Infinity) { await commander.set(_key, _value, "EX", this.ttl);
await commander.set(_key, _value);
} else {
await commander.set(_key, _value, "PX", this.ttl);
}
} }
public async get(key: string | null): Promise<T | undefined> { public async get(key: string | null, renew = false): Promise<T | undefined> {
const _key = this.prefixedKey(key); const _key = this.prefixedKey(key);
const cached = await redisClient.getBuffer(_key); const cached = await redisClient.getBuffer(_key);
if (cached === null) return undefined; if (cached === null) return undefined;
if (renew) await redisClient.expire(_key, this.ttl);
return decode(cached) as T; return decode(cached) as T;
} }
public async getAll(): Promise<Map<string, T>> { public async getAll(renew = false): Promise<Map<string, T>> {
const keys = await redisClient.keys(`${this.prefix}*`); const keys = await redisClient.keys(`${this.prefix}*`);
const map = new Map<string, T>(); const map = new Map<string, T>();
if (keys.length === 0) { if (keys.length === 0) {
@ -49,6 +51,14 @@ export class Cache<T> {
} }
} }
if (renew) {
const trans = redisClient.multi();
for (const key of map.keys()) {
trans.expire(key, this.ttl);
}
await trans.exec();
}
return map; return map;
} }
@ -66,9 +76,10 @@ export class Cache<T> {
public async fetch( public async fetch(
key: string | null, key: string | null,
fetcher: () => Promise<T>, fetcher: () => Promise<T>,
renew = false,
validator?: (cachedValue: T) => boolean, validator?: (cachedValue: T) => boolean,
): Promise<T> { ): Promise<T> {
const cachedValue = await this.get(key); const cachedValue = await this.get(key, renew);
if (cachedValue !== undefined) { if (cachedValue !== undefined) {
if (validator) { if (validator) {
if (validator(cachedValue)) { if (validator(cachedValue)) {
@ -94,9 +105,10 @@ export class Cache<T> {
public async fetchMaybe( public async fetchMaybe(
key: string | null, key: string | null,
fetcher: () => Promise<T | undefined>, fetcher: () => Promise<T | undefined>,
renew = false,
validator?: (cachedValue: T) => boolean, validator?: (cachedValue: T) => boolean,
): Promise<T | undefined> { ): Promise<T | undefined> {
const cachedValue = await this.get(key); const cachedValue = await this.get(key, renew);
if (cachedValue !== undefined) { if (cachedValue !== undefined) {
if (validator) { if (validator) {
if (validator(cachedValue)) { if (validator(cachedValue)) {

View file

@ -11,7 +11,7 @@ import * as Acct from "@/misc/acct.js";
import type { Packed } from "./schema.js"; import type { Packed } from "./schema.js";
import { Cache } from "./cache.js"; import { Cache } from "./cache.js";
const blockingCache = new Cache<User["id"][]>("blocking", 1000 * 60 * 5); const blockingCache = new Cache<User["id"][]>("blocking", 60 * 5);
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている // NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている

View file

@ -11,7 +11,7 @@ export type Size = {
height: number; height: number;
}; };
const cache = new Cache<boolean>("emojiMeta",1000 * 60 * 10); // once every 10 minutes for the same url const cache = new Cache<boolean>("emojiMeta", 60 * 10); // once every 10 minutes for the same url
const logger = new Logger("emoji"); const logger = new Logger("emoji");
export async function getEmojiSize(url: string): Promise<Size> { export async function getEmojiSize(url: string): Promise<Size> {

View file

@ -3,10 +3,12 @@ import type { User } from "@/models/entities/user.js";
import type { UserKeypair } from "@/models/entities/user-keypair.js"; import type { UserKeypair } from "@/models/entities/user-keypair.js";
import { Cache } from "./cache.js"; import { Cache } from "./cache.js";
const cache = new Cache<UserKeypair>("keypairStore", Infinity); const cache = new Cache<UserKeypair>("keypairStore", 60 * 30);
export async function getUserKeypair(userId: User["id"]): Promise<UserKeypair> { export async function getUserKeypair(userId: User["id"]): Promise<UserKeypair> {
return await cache.fetch(userId, () => return await cache.fetch(
UserKeypairs.findOneByOrFail({ userId: userId }), userId,
() => UserKeypairs.findOneByOrFail({ userId: userId }),
true,
); );
} }

View file

@ -9,7 +9,7 @@ import config from "@/config/index.js";
import { query } from "@/prelude/url.js"; import { query } from "@/prelude/url.js";
import { redisClient } from "@/db/redis.js"; import { redisClient } from "@/db/redis.js";
const cache = new Cache<Emoji | null>("populateEmojis", 1000 * 60 * 60 * 12); const cache = new Cache<Emoji | null>("populateEmojis", 60 * 60 * 12);
/** /**
* *

View file

@ -1,4 +1,3 @@
import { URL } from "url";
import { In, Not } from "typeorm"; import { In, Not } from "typeorm";
import Ajv from "ajv"; import Ajv from "ajv";
import type { ILocalUser, IRemoteUser } from "@/models/entities/user.js"; import type { ILocalUser, IRemoteUser } from "@/models/entities/user.js";
@ -40,7 +39,10 @@ import {
} from "../index.js"; } from "../index.js";
import type { Instance } from "../entities/instance.js"; import type { Instance } from "../entities/instance.js";
const userInstanceCache = new Cache<Instance | null>("userInstance", 1000 * 60 * 60 * 3); const userInstanceCache = new Cache<Instance | null>(
"userInstance",
60 * 60 * 3,
);
type IsUserDetailed<Detailed extends boolean> = Detailed extends true type IsUserDetailed<Detailed extends boolean> = Detailed extends true
? Packed<"UserDetailed"> ? Packed<"UserDetailed">

View file

@ -5,7 +5,6 @@ import type {
CacheableRemoteUser, CacheableRemoteUser,
CacheableUser, CacheableUser,
} from "@/models/entities/user.js"; } from "@/models/entities/user.js";
import { User, IRemoteUser } from "@/models/entities/user.js";
import type { UserPublickey } from "@/models/entities/user-publickey.js"; import type { UserPublickey } from "@/models/entities/user-publickey.js";
import type { MessagingMessage } from "@/models/entities/messaging-message.js"; import type { MessagingMessage } from "@/models/entities/messaging-message.js";
import { import {
@ -20,8 +19,11 @@ import type { IObject } from "./type.js";
import { getApId } from "./type.js"; import { getApId } from "./type.js";
import { resolvePerson } from "./models/person.js"; import { resolvePerson } from "./models/person.js";
const publicKeyCache = new Cache<UserPublickey | null>("publicKey", Infinity); const publicKeyCache = new Cache<UserPublickey | null>("publicKey", 60 * 30);
const publicKeyByUserIdCache = new Cache<UserPublickey | null>("publicKeyByUserId", Infinity); const publicKeyByUserIdCache = new Cache<UserPublickey | null>(
"publicKeyByUserId",
60 * 30,
);
export type UriParseResult = export type UriParseResult =
| { | {
@ -123,17 +125,23 @@ export default class DbResolver {
if (parsed.type !== "users") return null; if (parsed.type !== "users") return null;
return ( return (
(await userByIdCache.fetchMaybe(parsed.id, () => (await userByIdCache.fetchMaybe(
parsed.id,
() =>
Users.findOneBy({ Users.findOneBy({
id: parsed.id, id: parsed.id,
}).then((x) => x ?? undefined), }).then((x) => x ?? undefined),
true,
)) ?? null )) ?? null
); );
} else { } else {
return await uriPersonCache.fetch(parsed.uri, () => return await uriPersonCache.fetch(
parsed.uri,
() =>
Users.findOneBy({ Users.findOneBy({
uri: parsed.uri, uri: parsed.uri,
}), }),
true,
); );
} }
} }
@ -156,14 +164,17 @@ export default class DbResolver {
return key; return key;
}, },
true,
(key) => key != null, (key) => key != null,
); );
if (key == null) return null; if (key == null) return null;
return { return {
user: (await userByIdCache.fetch(key.userId, () => user: (await userByIdCache.fetch(
Users.findOneByOrFail({ id: key.userId }), key.userId,
() => Users.findOneByOrFail({ id: key.userId }),
true,
)) as CacheableRemoteUser, )) as CacheableRemoteUser,
key, key,
}; };
@ -183,6 +194,7 @@ export default class DbResolver {
const key = await publicKeyByUserIdCache.fetch( const key = await publicKeyByUserIdCache.fetch(
user.id, user.id,
() => UserPublickeys.findOneBy({ userId: user.id }), () => UserPublickeys.findOneBy({ userId: user.id }),
true,
(v) => v != null, (v) => v != null,
); );

View file

@ -135,7 +135,7 @@ export async function fetchPerson(
): Promise<CacheableUser | null> { ): Promise<CacheableUser | null> {
if (typeof uri !== "string") throw new Error("uri is not string"); if (typeof uri !== "string") throw new Error("uri is not string");
const cached = await uriPersonCache.get(uri); const cached = await uriPersonCache.get(uri, true);
if (cached) return cached; if (cached) return cached;
// Fetch from the database if the URI points to this server // Fetch from the database if the URI points to this server

View file

@ -9,7 +9,7 @@ import {
localUserByNativeTokenCache, localUserByNativeTokenCache,
} from "@/services/user-cache.js"; } from "@/services/user-cache.js";
const appCache = new Cache<App>("app", Infinity); const appCache = new Cache<App>("app", 60 * 30);
export class AuthenticationError extends Error { export class AuthenticationError extends Error {
constructor(message: string) { constructor(message: string) {
@ -49,6 +49,7 @@ export default async (
const user = await localUserByNativeTokenCache.fetch( const user = await localUserByNativeTokenCache.fetch(
token, token,
() => Users.findOneBy({ token }) as Promise<ILocalUser | null>, () => Users.findOneBy({ token }) as Promise<ILocalUser | null>,
true,
); );
if (user == null) { if (user == null) {
@ -82,11 +83,14 @@ export default async (
Users.findOneBy({ Users.findOneBy({
id: accessToken.userId, id: accessToken.userId,
}) as Promise<ILocalUser>, }) as Promise<ILocalUser>,
true,
); );
if (accessToken.appId) { if (accessToken.appId) {
const app = await appCache.fetch(accessToken.appId, () => const app = await appCache.fetch(
Apps.findOneByOrFail({ id: accessToken.appId! }), accessToken.appId,
() => Apps.findOneByOrFail({ id: accessToken.appId! }),
true,
); );
return [ return [

View file

@ -100,7 +100,10 @@ const nodeinfo2 = async () => {
}; };
}; };
const cache = new Cache<Awaited<ReturnType<typeof nodeinfo2>>>("nodeinfo", 1000 * 60 * 10); const cache = new Cache<Awaited<ReturnType<typeof nodeinfo2>>>(
"nodeinfo",
60 * 10,
);
router.get(nodeinfo2_1path, async (ctx) => { router.get(nodeinfo2_1path, async (ctx) => {
const base = await cache.fetch(null, () => nodeinfo2()); const base = await cache.fetch(null, () => nodeinfo2());

View file

@ -6,10 +6,10 @@ import { IsNull } from "typeorm";
const ACTOR_USERNAME = "instance.actor" as const; const ACTOR_USERNAME = "instance.actor" as const;
const cache = new Cache<ILocalUser>("instanceActor", Infinity); const cache = new Cache<ILocalUser>("instanceActor", 60 * 30);
export async function getInstanceActor(): Promise<ILocalUser> { export async function getInstanceActor(): Promise<ILocalUser> {
const cached = await cache.get(null); const cached = await cache.get(null, true);
if (cached) return cached; if (cached) return cached;
const user = (await Users.findOneBy({ const user = (await Users.findOneBy({

View file

@ -29,17 +29,14 @@ import {
Notes, Notes,
Instances, Instances,
UserProfiles, UserProfiles,
Antennas,
Followings,
MutedNotes, MutedNotes,
Channels, Channels,
ChannelFollowings, ChannelFollowings,
Blockings,
NoteThreadMutings, NoteThreadMutings,
} from "@/models/index.js"; } from "@/models/index.js";
import type { DriveFile } from "@/models/entities/drive-file.js"; import type { DriveFile } from "@/models/entities/drive-file.js";
import type { App } from "@/models/entities/app.js"; import type { App } from "@/models/entities/app.js";
import { Not, In, IsNull } from "typeorm"; import { Not, In } from "typeorm";
import type { User, ILocalUser, IRemoteUser } from "@/models/entities/user.js"; import type { User, ILocalUser, IRemoteUser } from "@/models/entities/user.js";
import { genId } from "@/misc/gen-id.js"; import { genId } from "@/misc/gen-id.js";
import { import {
@ -73,7 +70,7 @@ import { Mutex } from "redis-semaphore";
const mutedWordsCache = new Cache< const mutedWordsCache = new Cache<
{ userId: UserProfile["userId"]; mutedWords: UserProfile["mutedWords"] }[] { userId: UserProfile["userId"]; mutedWords: UserProfile["mutedWords"] }[]
>("mutedWords", 1000 * 60 * 5); >("mutedWords", 60 * 5);
type NotificationType = "reply" | "renote" | "quote" | "mention"; type NotificationType = "reply" | "renote" | "quote" | "mention";

View file

@ -4,7 +4,7 @@ import { genId } from "@/misc/gen-id.js";
import { toPuny } from "@/misc/convert-host.js"; import { toPuny } from "@/misc/convert-host.js";
import { Cache } from "@/misc/cache.js"; import { Cache } from "@/misc/cache.js";
const cache = new Cache<Instance>("registerOrFetchInstanceDoc", 1000 * 60 * 60); const cache = new Cache<Instance>("registerOrFetchInstanceDoc", 60 * 60);
export async function registerOrFetchInstanceDoc( export async function registerOrFetchInstanceDoc(
host: string, host: string,

View file

@ -15,7 +15,7 @@ import { createSystemUser } from "./create-system-user.js";
const ACTOR_USERNAME = "relay.actor" as const; const ACTOR_USERNAME = "relay.actor" as const;
const relaysCache = new Cache<Relay[]>("relay", 1000 * 60 * 10); const relaysCache = new Cache<Relay[]>("relay", 60 * 10);
export async function getRelayActor(): Promise<ILocalUser> { export async function getRelayActor(): Promise<ILocalUser> {
const user = await Users.findOneBy({ const user = await Users.findOneBy({

View file

@ -7,13 +7,19 @@ import { Users } from "@/models/index.js";
import { Cache } from "@/misc/cache.js"; import { Cache } from "@/misc/cache.js";
import { redisClient, subscriber } from "@/db/redis.js"; import { redisClient, subscriber } from "@/db/redis.js";
export const userByIdCache = new Cache<CacheableUser>("userById", Infinity); export const userByIdCache = new Cache<CacheableUser>("userById", 60 * 30);
export const localUserByNativeTokenCache = new Cache<CacheableLocalUser | null>( export const localUserByNativeTokenCache = new Cache<CacheableLocalUser | null>(
"localUserByNativeToken", "localUserByNativeToken",
Infinity, 60 * 30,
);
export const localUserByIdCache = new Cache<CacheableLocalUser>(
"localUserByIdCache",
60 * 30,
);
export const uriPersonCache = new Cache<CacheableUser | null>(
"uriPerson",
60 * 30,
); );
export const localUserByIdCache = new Cache<CacheableLocalUser>("localUserByIdCache", Infinity);
export const uriPersonCache = new Cache<CacheableUser | null>("uriPerson", Infinity);
subscriber.on("message", async (_, data) => { subscriber.on("message", async (_, data) => {
const obj = JSON.parse(data); const obj = JSON.parse(data);