perf: prepare scylladb config and use cache for packing user
This commit is contained in:
parent
6025b0ef30
commit
9c65f0078b
11 changed files with 100 additions and 33 deletions
|
@ -67,7 +67,7 @@ redis:
|
||||||
#db: 1
|
#db: 1
|
||||||
#user: default
|
#user: default
|
||||||
|
|
||||||
# ┌─────────────────────────────┐
|
# ┌────────────────────────────┐
|
||||||
#───┘ Cache server configuration └─────────────────────────────────────
|
#───┘ Cache server configuration └─────────────────────────────────────
|
||||||
|
|
||||||
# A Redis-compatible server (DragonflyDB, Keydb, Redis) for caching
|
# A Redis-compatible server (DragonflyDB, Keydb, Redis) for caching
|
||||||
|
@ -81,6 +81,14 @@ redis:
|
||||||
#prefix: example-prefix
|
#prefix: example-prefix
|
||||||
#db: 1
|
#db: 1
|
||||||
|
|
||||||
|
# ┌────────────────────────┐
|
||||||
|
#───┘ ScyllaDB configuration └─────────────────────────────────────
|
||||||
|
|
||||||
|
# scylla:
|
||||||
|
# nodes: ['localhost:9042']
|
||||||
|
# keyspace: calckey
|
||||||
|
# replicationFactor: 1
|
||||||
|
|
||||||
# Please configure either MeiliSearch *or* Sonic.
|
# Please configure either MeiliSearch *or* Sonic.
|
||||||
# If both MeiliSearch and Sonic configurations are present, MeiliSearch will take precedence.
|
# If both MeiliSearch and Sonic configurations are present, MeiliSearch will take precedence.
|
||||||
|
|
||||||
|
|
|
@ -53,6 +53,7 @@
|
||||||
"bull": "4.10.4",
|
"bull": "4.10.4",
|
||||||
"cacheable-lookup": "7.0.0",
|
"cacheable-lookup": "7.0.0",
|
||||||
"calckey-js": "workspace:*",
|
"calckey-js": "workspace:*",
|
||||||
|
"cassandra-driver": "^4.6.4",
|
||||||
"cbor": "8.1.0",
|
"cbor": "8.1.0",
|
||||||
"chalk": "5.3.0",
|
"chalk": "5.3.0",
|
||||||
"chalk-template": "0.4.0",
|
"chalk-template": "0.4.0",
|
||||||
|
|
|
@ -16,6 +16,11 @@ export type Source = {
|
||||||
disableCache?: boolean;
|
disableCache?: boolean;
|
||||||
extra?: { [x: string]: string };
|
extra?: { [x: string]: string };
|
||||||
};
|
};
|
||||||
|
scylla?: {
|
||||||
|
nodes: string[];
|
||||||
|
keyspace: string;
|
||||||
|
replicationFactor: number;
|
||||||
|
},
|
||||||
redis: {
|
redis: {
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
|
|
28
packages/backend/src/db/scylla.ts
Normal file
28
packages/backend/src/db/scylla.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import config from "@/config/index.js";
|
||||||
|
import { Client } from "cassandra-driver";
|
||||||
|
|
||||||
|
function newClient(): Client | null {
|
||||||
|
if (!config.scylla) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Client({
|
||||||
|
contactPoints: config.scylla.nodes,
|
||||||
|
keyspace: config.scylla.keyspace,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const scyllaClient = newClient();
|
||||||
|
|
||||||
|
export const prepared = {
|
||||||
|
timeline: {
|
||||||
|
insert: "",
|
||||||
|
select: "",
|
||||||
|
delete: "",
|
||||||
|
},
|
||||||
|
notification: {
|
||||||
|
insert: "",
|
||||||
|
select: "",
|
||||||
|
delete: "",
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,6 +38,7 @@ import {
|
||||||
UserSecurityKeys,
|
UserSecurityKeys,
|
||||||
} from "../index.js";
|
} from "../index.js";
|
||||||
import type { Instance } from "../entities/instance.js";
|
import type { Instance } from "../entities/instance.js";
|
||||||
|
import { userByIdCache } from "@/services/user-cache.js";
|
||||||
|
|
||||||
const userInstanceCache = new Cache<Instance | null>(
|
const userInstanceCache = new Cache<Instance | null>(
|
||||||
"userInstance",
|
"userInstance",
|
||||||
|
@ -382,23 +383,24 @@ export const UserRepository = db.getRepository(User).extend({
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
let user: User;
|
let id: string;
|
||||||
|
|
||||||
if (typeof src === "object") {
|
if (typeof src === "object") {
|
||||||
user = src;
|
id = src.id;
|
||||||
if (src.avatar === undefined && src.avatarId)
|
|
||||||
src.avatar = (await DriveFiles.findOneBy({ id: src.avatarId })) ?? null;
|
|
||||||
if (src.banner === undefined && src.bannerId)
|
|
||||||
src.banner = (await DriveFiles.findOneBy({ id: src.bannerId })) ?? null;
|
|
||||||
} else {
|
} else {
|
||||||
user = await this.findOneOrFail({
|
id = src;
|
||||||
where: { id: src },
|
}
|
||||||
|
|
||||||
|
const user = await userByIdCache.fetch(id, () =>
|
||||||
|
this.findOneOrFail({
|
||||||
|
where: { id },
|
||||||
relations: {
|
relations: {
|
||||||
avatar: true,
|
avatar: true,
|
||||||
banner: true,
|
banner: true,
|
||||||
},
|
},
|
||||||
});
|
}),
|
||||||
}
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
const meId = me ? me.id : null;
|
const meId = me ? me.id : null;
|
||||||
const isMe = meId === user.id;
|
const isMe = meId === user.id;
|
||||||
|
|
|
@ -128,8 +128,12 @@ export default class DbResolver {
|
||||||
(await userByIdCache.fetchMaybe(
|
(await userByIdCache.fetchMaybe(
|
||||||
parsed.id,
|
parsed.id,
|
||||||
() =>
|
() =>
|
||||||
Users.findOneBy({
|
Users.findOne({
|
||||||
id: parsed.id,
|
where: { id: parsed.id },
|
||||||
|
relations: {
|
||||||
|
avatar: true,
|
||||||
|
banner: true,
|
||||||
|
},
|
||||||
}).then((x) => x ?? undefined),
|
}).then((x) => x ?? undefined),
|
||||||
true,
|
true,
|
||||||
)) ?? null
|
)) ?? null
|
||||||
|
@ -173,7 +177,11 @@ export default class DbResolver {
|
||||||
return {
|
return {
|
||||||
user: (await userByIdCache.fetch(
|
user: (await userByIdCache.fetch(
|
||||||
key.userId,
|
key.userId,
|
||||||
() => Users.findOneByOrFail({ id: key.userId }),
|
() =>
|
||||||
|
Users.findOneOrFail({
|
||||||
|
where: { id: key.userId },
|
||||||
|
relations: { avatar: true, banner: true },
|
||||||
|
}),
|
||||||
true,
|
true,
|
||||||
)) as CacheableRemoteUser,
|
)) as CacheableRemoteUser,
|
||||||
key,
|
key,
|
||||||
|
|
|
@ -14,6 +14,8 @@ import { normalizeForSearch } from "@/misc/normalize-for-search.js";
|
||||||
import { langmap } from "@/misc/langmap.js";
|
import { langmap } from "@/misc/langmap.js";
|
||||||
import { ApiError } from "../../error.js";
|
import { ApiError } from "../../error.js";
|
||||||
import define from "../../define.js";
|
import define from "../../define.js";
|
||||||
|
import { userByIdCache } from "@/services/user-cache.js";
|
||||||
|
import type { DriveFile } from "@/models/entities/drive-file.js";
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ["account"],
|
tags: ["account"],
|
||||||
|
@ -204,8 +206,9 @@ export default define(meta, paramDef, async (ps, _user, token) => {
|
||||||
if (ps.emailNotificationTypes !== undefined)
|
if (ps.emailNotificationTypes !== undefined)
|
||||||
profileUpdates.emailNotificationTypes = ps.emailNotificationTypes;
|
profileUpdates.emailNotificationTypes = ps.emailNotificationTypes;
|
||||||
|
|
||||||
|
let avatar: DriveFile | null = null;
|
||||||
if (ps.avatarId) {
|
if (ps.avatarId) {
|
||||||
const avatar = await DriveFiles.findOneBy({ id: ps.avatarId });
|
avatar = await DriveFiles.findOneBy({ id: ps.avatarId });
|
||||||
|
|
||||||
if (avatar == null || avatar.userId !== user.id)
|
if (avatar == null || avatar.userId !== user.id)
|
||||||
throw new ApiError(meta.errors.noSuchAvatar);
|
throw new ApiError(meta.errors.noSuchAvatar);
|
||||||
|
@ -213,8 +216,9 @@ export default define(meta, paramDef, async (ps, _user, token) => {
|
||||||
throw new ApiError(meta.errors.avatarNotAnImage);
|
throw new ApiError(meta.errors.avatarNotAnImage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let banner: DriveFile | null = null;
|
||||||
if (ps.bannerId) {
|
if (ps.bannerId) {
|
||||||
const banner = await DriveFiles.findOneBy({ id: ps.bannerId });
|
banner = await DriveFiles.findOneBy({ id: ps.bannerId });
|
||||||
|
|
||||||
if (banner == null || banner.userId !== user.id)
|
if (banner == null || banner.userId !== user.id)
|
||||||
throw new ApiError(meta.errors.noSuchBanner);
|
throw new ApiError(meta.errors.noSuchBanner);
|
||||||
|
@ -278,7 +282,10 @@ export default define(meta, paramDef, async (ps, _user, token) => {
|
||||||
updateUsertags(user, tags);
|
updateUsertags(user, tags);
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
if (Object.keys(updates).length > 0) await Users.update(user.id, updates);
|
if (Object.keys(updates).length > 0) {
|
||||||
|
await Users.update(user.id, updates);
|
||||||
|
await userByIdCache.set(user.id, { ...user, ...updates, avatar, banner });
|
||||||
|
}
|
||||||
if (Object.keys(profileUpdates).length > 0)
|
if (Object.keys(profileUpdates).length > 0)
|
||||||
await UserProfiles.update(user.id, profileUpdates);
|
await UserProfiles.update(user.id, profileUpdates);
|
||||||
|
|
||||||
|
|
|
@ -89,17 +89,10 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
ps.untilDate,
|
ps.untilDate,
|
||||||
)
|
)
|
||||||
.andWhere("(note.visibility = 'public') AND (note.userHost IS NULL)")
|
.andWhere("(note.visibility = 'public') AND (note.userHost IS NULL)")
|
||||||
.innerJoinAndSelect("note.user", "user")
|
|
||||||
.leftJoinAndSelect("user.avatar", "avatar")
|
|
||||||
.leftJoinAndSelect("user.banner", "banner")
|
|
||||||
.leftJoinAndSelect("note.reply", "reply")
|
.leftJoinAndSelect("note.reply", "reply")
|
||||||
.leftJoinAndSelect("note.renote", "renote")
|
.leftJoinAndSelect("note.renote", "renote")
|
||||||
.leftJoinAndSelect("reply.user", "replyUser")
|
|
||||||
.leftJoinAndSelect("replyUser.avatar", "replyUserAvatar")
|
// TODO: Use materialized view of postgres as much for non-denormalizable columns as possible.
|
||||||
.leftJoinAndSelect("replyUser.banner", "replyUserBanner")
|
|
||||||
.leftJoinAndSelect("renote.user", "renoteUser")
|
|
||||||
.leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar")
|
|
||||||
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner");
|
|
||||||
|
|
||||||
generateChannelQuery(query, user);
|
generateChannelQuery(query, user);
|
||||||
generateRepliesQuery(query, ps.withReplies, user);
|
generateRepliesQuery(query, ps.withReplies, user);
|
||||||
|
|
|
@ -92,16 +92,10 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.innerJoinAndSelect("note.user", "user")
|
.innerJoinAndSelect("note.user", "user")
|
||||||
.leftJoinAndSelect("user.avatar", "avatar")
|
|
||||||
.leftJoinAndSelect("user.banner", "banner")
|
|
||||||
.leftJoinAndSelect("note.reply", "reply")
|
.leftJoinAndSelect("note.reply", "reply")
|
||||||
.leftJoinAndSelect("note.renote", "renote")
|
.leftJoinAndSelect("note.renote", "renote")
|
||||||
.leftJoinAndSelect("reply.user", "replyUser")
|
.leftJoinAndSelect("reply.user", "replyUser")
|
||||||
.leftJoinAndSelect("replyUser.avatar", "replyUserAvatar")
|
|
||||||
.leftJoinAndSelect("replyUser.banner", "replyUserBanner")
|
|
||||||
.leftJoinAndSelect("renote.user", "renoteUser")
|
.leftJoinAndSelect("renote.user", "renoteUser")
|
||||||
.leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar")
|
|
||||||
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner")
|
|
||||||
.setParameters(followingQuery.getParameters());
|
.setParameters(followingQuery.getParameters());
|
||||||
|
|
||||||
generateChannelQuery(query, user);
|
generateChannelQuery(query, user);
|
||||||
|
|
|
@ -40,7 +40,10 @@ subscriber.on("message", async (_, data) => {
|
||||||
case "userChangeSilencedState":
|
case "userChangeSilencedState":
|
||||||
case "userChangeModeratorState":
|
case "userChangeModeratorState":
|
||||||
case "remoteUserUpdated": {
|
case "remoteUserUpdated": {
|
||||||
const user = await Users.findOneByOrFail({ id: body.id });
|
const user = await Users.findOneOrFail({
|
||||||
|
where: { id: body.id },
|
||||||
|
relations: { avatar: true, banner: true },
|
||||||
|
});
|
||||||
await userByIdCache.set(user.id, user);
|
await userByIdCache.set(user.id, user);
|
||||||
const trans = redisClient.multi();
|
const trans = redisClient.multi();
|
||||||
for (const [k, v] of (await uriPersonCache.getAll()).entries()) {
|
for (const [k, v] of (await uriPersonCache.getAll()).entries()) {
|
||||||
|
|
|
@ -159,6 +159,9 @@ importers:
|
||||||
calckey-js:
|
calckey-js:
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../calckey-js
|
version: link:../calckey-js
|
||||||
|
cassandra-driver:
|
||||||
|
specifier: ^4.6.4
|
||||||
|
version: 4.6.4
|
||||||
cbor:
|
cbor:
|
||||||
specifier: 8.1.0
|
specifier: 8.1.0
|
||||||
version: 8.1.0
|
version: 8.1.0
|
||||||
|
@ -6220,6 +6223,16 @@ packages:
|
||||||
/caseless@0.12.0:
|
/caseless@0.12.0:
|
||||||
resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
|
resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
|
||||||
|
|
||||||
|
/cassandra-driver@4.6.4:
|
||||||
|
resolution: {integrity: sha512-SksbIK0cZ2QZRx8ti7w+PnLqldyY+6kU2gRWFChwXFTtrD/ce8cQICDEHxyPwx+DeILwRnMrPf9cjUGizYw9Vg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
dependencies:
|
||||||
|
'@types/long': 4.0.2
|
||||||
|
'@types/node': 20.4.1
|
||||||
|
adm-zip: 0.5.10
|
||||||
|
long: 2.4.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/cbor@8.1.0:
|
/cbor@8.1.0:
|
||||||
resolution: {integrity: sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==}
|
resolution: {integrity: sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==}
|
||||||
engines: {node: '>=12.19'}
|
engines: {node: '>=12.19'}
|
||||||
|
@ -12887,6 +12900,11 @@ packages:
|
||||||
wrap-ansi: 6.2.0
|
wrap-ansi: 6.2.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/long@2.4.0:
|
||||||
|
resolution: {integrity: sha512-ijUtjmO/n2A5PaosNG9ZGDsQ3vxJg7ZW8vsY8Kp0f2yIZWhSJvjmegV7t+9RPQKxKrvj8yKGehhS+po14hPLGQ==}
|
||||||
|
engines: {node: '>=0.6'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/long@4.0.0:
|
/long@4.0.0:
|
||||||
resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==}
|
resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
Loading…
Reference in a new issue