From d0665c2fc2b28db035c5bda772d0eb563e5f1282 Mon Sep 17 00:00:00 2001 From: Namekuji Date: Sat, 12 Aug 2023 14:21:48 -0400 Subject: [PATCH] fix: decrement counts of boost and reply --- .../cql/1689400417034_timeline/down.cql | 1 + .../cql/1689400417034_timeline/up.cql | 11 ++ .../backend/src/misc/count-same-renotes.ts | 24 ++++- .../server/api/endpoints/notes/unrenote.ts | 9 +- packages/backend/src/services/note/delete.ts | 102 +++++++++++++++++- 5 files changed, 140 insertions(+), 7 deletions(-) diff --git a/packages/backend/native-utils/scylla-migration/cql/1689400417034_timeline/down.cql b/packages/backend/native-utils/scylla-migration/cql/1689400417034_timeline/down.cql index 3db9222563..4664088415 100644 --- a/packages/backend/native-utils/scylla-migration/cql/1689400417034_timeline/down.cql +++ b/packages/backend/native-utils/scylla-migration/cql/1689400417034_timeline/down.cql @@ -6,6 +6,7 @@ DROP INDEX IF EXISTS home_by_id; DROP TABLE IF EXISTS home_timeline; DROP MATERIALIZED VIEW IF EXISTS local_timeline; DROP MATERIALIZED VIEW IF EXISTS global_timeline; +DROP MATERIALIZED VIEW IF EXISTS note_by_renote_id_and_user_id; DROP MATERIALIZED VIEW IF EXISTS note_by_renote_id; DROP MATERIALIZED VIEW IF EXISTS note_by_userid; DROP INDEX IF EXISTS note_by_id; diff --git a/packages/backend/native-utils/scylla-migration/cql/1689400417034_timeline/up.cql b/packages/backend/native-utils/scylla-migration/cql/1689400417034_timeline/up.cql index 2e76da4a79..080a85cba4 100644 --- a/packages/backend/native-utils/scylla-migration/cql/1689400417034_timeline/up.cql +++ b/packages/backend/native-utils/scylla-migration/cql/1689400417034_timeline/up.cql @@ -97,6 +97,17 @@ CREATE MATERIALIZED VIEW IF NOT EXISTS note_by_renote_id AS PRIMARY KEY ("renoteId", "createdAt", "createdAtDate", "userId", "userHost", "visibility") WITH CLUSTERING ORDER BY ("createdAt" DESC); +CREATE MATERIALIZED VIEW IF NOT EXISTS note_by_renote_id_and_user_id AS + SELECT "renoteId", "userId", "createdAt", "createdAtDate", "userHost", "visibility", "id" FROM note + WHERE "renoteId" IS NOT NULL + AND "createdAt" IS NOT NULL + AND "createdAtDate" IS NOT NULL + AND "userId" IS NOT NULL + AND "userHost" IS NOT NULL + AND "visibility" IS NOT NULL + PRIMARY KEY (("renoteId", "userId"), "createdAt", "createdAtDate", "userHost", "visibility") + WITH CLUSTERING ORDER BY ("createdAt" DESC); + CREATE MATERIALIZED VIEW IF NOT EXISTS global_timeline AS SELECT * FROM note WHERE "createdAtDate" IS NOT NULL diff --git a/packages/backend/src/misc/count-same-renotes.ts b/packages/backend/src/misc/count-same-renotes.ts index 45a6c1d35a..705fc35d57 100644 --- a/packages/backend/src/misc/count-same-renotes.ts +++ b/packages/backend/src/misc/count-same-renotes.ts @@ -1,16 +1,36 @@ +import { prepared, scyllaClient } from "@/db/scylla"; import { Notes } from "@/models/index.js"; +/** + * Sum of notes that have the given userId and renoteId + */ export async function countSameRenotes( userId: string, renoteId: string, excludeNoteId: string | undefined, ): Promise { - // 指定したユーザーの指定したノートのリノートがいくつあるか数える + if (scyllaClient) { + const result = await scyllaClient.execute( + `SELECT "renoteId","userId","createdAt","id" FROM note_by_renote_id_and_user_id WHERE "renoteId" = ? AND "userId" = ?`, + [renoteId, userId], + { prepare: true }, + ); + if (excludeNoteId) { + const renotes = result.rows + .map((row) => row.get("id") as string) + .filter((id) => id !== excludeNoteId); + + return renotes.length; + } + + return result.rowLength; + } + const query = Notes.createQueryBuilder("note") .where("note.userId = :userId", { userId }) .andWhere("note.renoteId = :renoteId", { renoteId }); - // 指定した投稿を除く + // Exclude if specified if (excludeNoteId) { query.andWhere("note.id != :excludeNoteId", { excludeNoteId }); } diff --git a/packages/backend/src/server/api/endpoints/notes/unrenote.ts b/packages/backend/src/server/api/endpoints/notes/unrenote.ts index f03dc6cc1c..6f94d5565e 100644 --- a/packages/backend/src/server/api/endpoints/notes/unrenote.ts +++ b/packages/backend/src/server/api/endpoints/notes/unrenote.ts @@ -6,6 +6,7 @@ import { ApiError } from "../../error.js"; import { SECOND, HOUR } from "@/const.js"; import type { Note } from "@/models/entities/note.js"; import { parseScyllaNote, prepared, scyllaClient } from "@/db/scylla.js"; +import { userByIdCache } from "@/services/user-cache.js"; export const meta = { tags: ["notes"], @@ -59,6 +60,12 @@ export default define(meta, paramDef, async (ps, user) => { } for (const note of renotes) { - deleteNote(await Users.findOneByOrFail({ id: user.id }), note); + deleteNote( + await userByIdCache.fetch( + user.id, + () => Users.findOneByOrFail({ id: user.id }), + ), + note, + ); } }); diff --git a/packages/backend/src/services/note/delete.ts b/packages/backend/src/services/note/delete.ts index 33b893720f..5482c9563b 100644 --- a/packages/backend/src/services/note/delete.ts +++ b/packages/backend/src/services/note/delete.ts @@ -22,7 +22,12 @@ import { countSameRenotes } from "@/misc/count-same-renotes.js"; import { registerOrFetchInstanceDoc } from "../register-or-fetch-instance-doc.js"; import { deliverToRelays } from "../relay.js"; import meilisearch from "@/db/meilisearch.js"; -import { parseHomeTimeline, prepared, scyllaClient } from "@/db/scylla.js"; +import { + parseHomeTimeline, + parseScyllaNote, + prepared, + scyllaClient, +} from "@/db/scylla.js"; /** * 投稿を削除します。 @@ -41,12 +46,101 @@ export default async function ( note.renoteId && (await countSameRenotes(user.id, note.renoteId, note.id)) === 0 ) { - Notes.decrement({ id: note.renoteId }, "renoteCount", 1); - Notes.decrement({ id: note.renoteId }, "score", 1); + if (scyllaClient) { + const result = await scyllaClient.execute( + prepared.note.select.byId, + [note.renoteId], + { prepare: true }, + ); + if (result.rowLength > 0) { + const renote = parseScyllaNote(result.first()); + const count = isNaN(renote.renoteCount) ? 0 : renote.renoteCount; + const score = isNaN(renote.score) ? 0 : renote.score; + await scyllaClient.execute( + prepared.note.update.renoteCount, + [ + Math.max(count - 1, 0), + Math.max(score - 1, 0), + renote.createdAt, + renote.createdAt, + renote.userId, + renote.userHost ?? "local", + renote.visibility, + ], + { prepare: true }, + ); + const homeTimelines = await scyllaClient + .execute(prepared.homeTimeline.select.byId, [renote.id], { + prepare: true, + }) + .then((result) => result.rows.map(parseHomeTimeline)); + // Do not issue BATCH because different home timelines involve different partitions + for (const timeline of homeTimelines) { + scyllaClient.execute( + prepared.homeTimeline.update.renoteCount, + [ + Math.max(count - 1, 0), + Math.max(score - 1, 0), + timeline.feedUserId, + timeline.createdAtDate, + timeline.createdAt, + timeline.userId, + ], + { prepare: true }, + ); + } + } + } else { + Notes.decrement({ id: note.renoteId }, "renoteCount", 1); + Notes.decrement({ id: note.renoteId }, "score", 1); + } } if (note.replyId) { - await Notes.decrement({ id: note.replyId }, "repliesCount", 1); + if (scyllaClient) { + const result = await scyllaClient.execute( + prepared.note.select.byId, + [note.replyId], + { prepare: true }, + ); + if (result.rowLength > 0) { + const reply = parseScyllaNote(result.first()); + const count = isNaN(reply.repliesCount) ? 0 : reply.repliesCount; + await scyllaClient.execute( + prepared.note.update.repliesCount, + [ + Math.max(count - 1, 0), + reply.createdAt, + reply.createdAt, + reply.userId, + reply.userHost ?? "local", + reply.visibility, + ], + { prepare: true }, + ); + const homeTimelines = await scyllaClient + .execute(prepared.homeTimeline.select.byId, [reply.id], { + prepare: true, + }) + .then((result) => result.rows.map(parseHomeTimeline)); + // Do not issue BATCH because different home timelines involve different partitions + for (const timeline of homeTimelines) { + scyllaClient.execute( + prepared.homeTimeline.update.repliesCount, + [ + Math.max(count - 1, 0), + timeline.feedUserId, + timeline.createdAtDate, + timeline.createdAt, + timeline.userId, + ], + { prepare: true }, + ); + } + } + } else { + await Notes.decrement({ id: note.replyId }, "repliesCount", 1); + } } if (!quiet) {