diff options
author | Bruce Berrios <[email protected]> | 2024-09-09 23:02:52 -0400 |
---|---|---|
committer | Bruce Berrios <[email protected]> | 2024-09-09 23:02:52 -0400 |
commit | bf6a526678f74e32ff42a6060bcb251f0039ee84 (patch) | |
tree | bb25b3b623feea8b062a5006d70b36a324b5212c | |
parent | 14277538c307a5b2a0d03b183418e45304278846 (diff) | |
download | monkeytype-bf6a526678f74e32ff42a6060bcb251f0039ee84.tar.gz monkeytype-bf6a526678f74e32ff42a6060bcb251f0039ee84.zip |
Migrate deletion to asynchronous job
-rw-r--r-- | backend/src/api/controllers/user.ts | 36 | ||||
-rw-r--r-- | backend/src/dal/user.ts | 17 | ||||
-rw-r--r-- | backend/src/jobs/delete-users.ts | 67 | ||||
-rw-r--r-- | backend/src/jobs/index.ts | 2 | ||||
-rw-r--r-- | backend/src/utils/misc.ts | 32 |
5 files changed, 120 insertions, 34 deletions
diff --git a/backend/src/api/controllers/user.ts b/backend/src/api/controllers/user.ts index 738803bb3..2a4432a97 100644 --- a/backend/src/api/controllers/user.ts +++ b/backend/src/api/controllers/user.ts @@ -38,7 +38,7 @@ import { TestActivity, UserProfileDetails, } from "@monkeytype/contracts/schemas/users"; -import { addImportantLog, addLog, deleteUserLogs } from "../../dal/logs"; +import { addImportantLog, addLog } from "../../dal/logs"; import { sendForgotPasswordEmail as authSendForgotPasswordEmail } from "../../utils/auth"; import { AddCustomThemeRequest, @@ -231,39 +231,7 @@ export async function deleteUser( ): Promise<MonkeyResponse2> { const { uid } = req.ctx.decodedToken; - const userInfo = await UserDAL.getPartialUser(uid, "delete user", [ - "banned", - "name", - "email", - "discordId", - ]); - - if (userInfo.banned === true) { - await BlocklistDal.add(userInfo); - } - - //cleanup database - await Promise.all([ - UserDAL.deleteUser(uid), - deleteUserLogs(uid), - deleteAllApeKeys(uid), - deleteAllPresets(uid), - deleteConfig(uid), - deleteAllResults(uid), - purgeUserFromDailyLeaderboards( - uid, - req.ctx.configuration.dailyLeaderboards - ), - ]); - - //delete user from - await AuthUtil.deleteUser(uid); - - void addImportantLog( - "user_deleted", - `${userInfo.email} ${userInfo.name}`, - uid - ); + await UserDAL.softDeleteUser(uid); return new MonkeyResponse2("User deleted", null); } diff --git a/backend/src/dal/user.ts b/backend/src/dal/user.ts index 6e36ecf08..cd2117209 100644 --- a/backend/src/dal/user.ts +++ b/backend/src/dal/user.ts @@ -76,6 +76,23 @@ export async function deleteUser(uid: string): Promise<void> { await getUsersCollection().deleteOne({ uid }); } +export async function softDeleteUser(uid: string): Promise<void> { + await getUsersCollection().updateOne( + { uid }, + { + $set: { + deleted: true, + }, + } + ); +} + +export async function getSoftDeletedUsers( + limit: number +): Promise<MonkeyTypes.DBUser[]> { + return getUsersCollection().find({ deleted: true }, { limit }).toArray(); +} + export async function resetUser(uid: string): Promise<void> { await getUsersCollection().updateOne( { uid }, diff --git a/backend/src/jobs/delete-users.ts b/backend/src/jobs/delete-users.ts new file mode 100644 index 000000000..2d8e57b47 --- /dev/null +++ b/backend/src/jobs/delete-users.ts @@ -0,0 +1,67 @@ +import { CronJob } from "cron"; + +import { deleteAllApeKeys } from "../dal/ape-keys"; +import * as BlocklistDal from "../dal/blocklist"; +import { deleteConfig } from "../dal/config"; +import { addImportantLog, deleteUserLogs } from "../dal/logs"; +import { deleteAllPresets } from "../dal/preset"; +import { deleteAll as deleteAllResults } from "../dal/result"; +import * as UserDAL from "../dal/user"; +import { getCachedConfiguration } from "../init/configuration"; +import * as AuthUtil from "../utils/auth"; +import { purgeUserFromDailyLeaderboards } from "../utils/daily-leaderboards"; +import { mapLimit } from "../utils/misc"; + +const CRON_SCHEDULE = "*/10 * * * *"; // every 10 minutes +const DELETE_BATCH_SIZE = 50; +const CONCURRENT_DELETIONS = 5; + +async function deleteUser(uid: string): Promise<void> { + const config = await getCachedConfiguration(); + + const userInfo = await UserDAL.getPartialUser(uid, "delete user", [ + "banned", + "name", + "email", + "discordId", + ]); + + if (userInfo.banned === true) { + await BlocklistDal.add(userInfo); + } + + // cleanup database + await Promise.all([ + deleteUserLogs(uid), + deleteAllApeKeys(uid), + deleteAllPresets(uid), + deleteConfig(uid), + deleteAllResults(uid), + purgeUserFromDailyLeaderboards(uid, config.dailyLeaderboards), + ]); + + // delete user from auth + await AuthUtil.deleteUser(uid); + + void addImportantLog( + "user_deleted", + `${userInfo.email} ${userInfo.name}`, + uid + ); + + await UserDAL.deleteUser(uid); +} + +async function deleteUsers(): Promise<void> { + const softDeletedUsers = await UserDAL.getSoftDeletedUsers(DELETE_BATCH_SIZE); + + if (softDeletedUsers.length === 0) { + return; + } + + await mapLimit(softDeletedUsers, CONCURRENT_DELETIONS, async (user) => { + await deleteUser(user.uid); + }); +} + +export default new CronJob(CRON_SCHEDULE, deleteUsers); diff --git a/backend/src/jobs/index.ts b/backend/src/jobs/index.ts index 13a821331..6fe979a17 100644 --- a/backend/src/jobs/index.ts +++ b/backend/src/jobs/index.ts @@ -2,10 +2,12 @@ import updateLeaderboards from "./update-leaderboards"; import deleteOldLogs from "./delete-old-logs"; import logCollectionSizes from "./log-collection-sizes"; import logQueueSizes from "./log-queue-sizes"; +import deleteUsers from "./delete-users"; export default [ updateLeaderboards, deleteOldLogs, logCollectionSizes, logQueueSizes, + deleteUsers, ]; diff --git a/backend/src/utils/misc.ts b/backend/src/utils/misc.ts index 985784642..a5ee6e1dd 100644 --- a/backend/src/utils/misc.ts +++ b/backend/src/utils/misc.ts @@ -343,3 +343,35 @@ export function replaceObjectIds<T extends { _id: ObjectId }>( if (data === undefined) return data; return data.map((it) => replaceObjectId(it)); } + +type MapLimitIteratee<T, V> = (element: T, index: number) => V; + +export async function mapLimit<T, V>( + input: T[], + limit: number, + iteratee: MapLimitIteratee<T, V> +): Promise<V[]> { + const size = input.length; + + const allElements = new Array(size); + const results = new Array(size); + + for (let i = 0; i < size; ++i) { + allElements[size - 1 - i] = [input[i], i]; + } + + const execute = async () => { + while (allElements.length > 0) { + const [element, index] = allElements.pop(); + results[index] = await iteratee(element, index); + } + }; + + const allExecutors: Promise<void>[] = []; + for (let i = 0; i < limit; ++i) { + allExecutors.push(execute()); + } + await Promise.all(allExecutors); + + return results; +} |