aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorBruce Berrios <[email protected]>2024-09-09 23:02:52 -0400
committerBruce Berrios <[email protected]>2024-09-09 23:02:52 -0400
commitbf6a526678f74e32ff42a6060bcb251f0039ee84 (patch)
treebb25b3b623feea8b062a5006d70b36a324b5212c
parent14277538c307a5b2a0d03b183418e45304278846 (diff)
downloadmonkeytype-bf6a526678f74e32ff42a6060bcb251f0039ee84.tar.gz
monkeytype-bf6a526678f74e32ff42a6060bcb251f0039ee84.zip
Migrate deletion to asynchronous job
-rw-r--r--backend/src/api/controllers/user.ts36
-rw-r--r--backend/src/dal/user.ts17
-rw-r--r--backend/src/jobs/delete-users.ts67
-rw-r--r--backend/src/jobs/index.ts2
-rw-r--r--backend/src/utils/misc.ts32
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;
+}