Skip to content

Commit 0a08fe1

Browse files
committed
Cached importJwk and data host data to improve cpu usage
ref https://linear.app/ghost/issue/BER-3241
1 parent 049047a commit 0a08fe1

File tree

2 files changed

+76
-22
lines changed

2 files changed

+76
-22
lines changed

src/dispatchers.ts

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,17 @@ export const actorDispatcher = (hostDataContextLoader: HostDataContextLoader) =>
104104
export const keypairDispatcher = (
105105
accountService: AccountService,
106106
hostDataContextLoader: HostDataContextLoader,
107-
) =>
108-
async function keypairDispatcher(ctx: FedifyContext, identifier: string) {
107+
) => {
108+
const MAX_CACHE_SIZE = 1000;
109+
const cryptoKeyCache = new Map<
110+
number,
111+
{ publicKey: CryptoKey; privateKey: CryptoKey }
112+
>();
113+
114+
return async function keypairDispatcher(
115+
ctx: FedifyContext,
116+
identifier: string,
117+
) {
109118
const hostData = await hostDataContextLoader.loadDataForHost(ctx.host);
110119

111120
if (isError(hostData)) {
@@ -145,6 +154,11 @@ export const keypairDispatcher = (
145154

146155
const { account } = getValue(hostData);
147156

157+
const cached = cryptoKeyCache.get(account.id);
158+
if (cached) {
159+
return [cached];
160+
}
161+
148162
const keyPair = await accountService.getKeyPair(account.id);
149163

150164
if (isError(keyPair)) {
@@ -176,18 +190,22 @@ export const keypairDispatcher = (
176190
const { publicKey, privateKey } = getValue(keyPair);
177191

178192
try {
179-
return [
180-
{
181-
publicKey: await importJwk(
182-
JSON.parse(publicKey) as JsonWebKey,
183-
'public',
184-
),
185-
privateKey: await importJwk(
186-
JSON.parse(privateKey) as JsonWebKey,
187-
'private',
188-
),
189-
},
190-
];
193+
const imported = {
194+
publicKey: await importJwk(
195+
JSON.parse(publicKey) as JsonWebKey,
196+
'public',
197+
),
198+
privateKey: await importJwk(
199+
JSON.parse(privateKey) as JsonWebKey,
200+
'private',
201+
),
202+
};
203+
204+
if (cryptoKeyCache.size < MAX_CACHE_SIZE) {
205+
cryptoKeyCache.set(account.id, imported);
206+
}
207+
208+
return [imported];
191209
} catch (error) {
192210
ctx.data.logger.error(
193211
'Could not parse keypair for {host} (identifier: {identifier}): {error}',
@@ -200,6 +218,7 @@ export const keypairDispatcher = (
200218
return [];
201219
}
202220
};
221+
};
203222

204223
export function createAcceptHandler(accountService: AccountService) {
205224
return async function handleAccept(ctx: FedifyContext, accept: Accept) {

src/http/host-data-context-loader.ts

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,55 @@ import type { Result } from '@/core/result';
66
import { error, ok } from '@/core/result';
77
import type { Site } from '@/site/site.service';
88

9+
type HostDataResult = Result<
10+
{ site: Site; account: Account },
11+
'site-not-found' | 'account-not-found' | 'multiple-users-for-site'
12+
>;
13+
14+
interface CacheEntry {
15+
result: HostDataResult;
16+
expiry: number;
17+
}
18+
919
export class HostDataContextLoader {
20+
private static readonly CACHE_TTL_MS = 60_000;
21+
private static readonly MAX_CACHE_SIZE = 1000;
22+
private static readonly CACHE_ENABLED =
23+
process.env.NODE_ENV === 'production';
24+
private readonly cache = new Map<string, CacheEntry>();
25+
1026
constructor(
1127
readonly db: Knex,
1228
readonly accountRepository: KnexAccountRepository,
1329
) {}
1430

15-
async loadDataForHost(
16-
host: string,
17-
): Promise<
18-
Result<
19-
{ site: Site; account: Account },
20-
'site-not-found' | 'account-not-found' | 'multiple-users-for-site'
21-
>
22-
> {
31+
async loadDataForHost(host: string): Promise<HostDataResult> {
32+
if (!HostDataContextLoader.CACHE_ENABLED) {
33+
return this.queryDataForHost(host);
34+
}
35+
36+
const now = Date.now();
37+
const cached = this.cache.get(host);
38+
39+
if (cached && cached.expiry > now) {
40+
return cached.result;
41+
}
42+
43+
const result = await this.queryDataForHost(host);
44+
45+
if (this.cache.size >= HostDataContextLoader.MAX_CACHE_SIZE) {
46+
this.cache.delete(this.cache.keys().next().value!);
47+
}
48+
49+
this.cache.set(host, {
50+
result,
51+
expiry: now + HostDataContextLoader.CACHE_TTL_MS,
52+
});
53+
54+
return result;
55+
}
56+
57+
private async queryDataForHost(host: string): Promise<HostDataResult> {
2358
const results = await this.db('sites')
2459
.leftJoin('users', 'users.site_id', 'sites.id')
2560
.leftJoin('accounts', 'accounts.id', 'users.account_id')

0 commit comments

Comments
 (0)