chore: Refactor worker, emails and data cleanup to task system (#3337)

* Refactor worker, all emails on task system

* fix

* lint

* fix: Remove a bunch of expect-error comments in related tests

* refactor: Move work from utils.gc into tasks

* test

* Add tracing to tasks and processors
fix: DebounceProcessor triggering on all events
Event.add -> Event.schedule
This commit is contained in:
Tom Moor
2022-04-06 16:48:28 -07:00
committed by GitHub
parent 9c766362ed
commit dbfdcd6d23
41 changed files with 729 additions and 444 deletions

View File

@@ -0,0 +1,36 @@
import { JobOptions } from "bull";
import { taskQueue } from "../";
export enum TaskPriority {
Background = 40,
Low = 30,
Normal = 20,
High = 10,
}
export default abstract class BaseTask<T> {
public static schedule<T>(props: T) {
// @ts-expect-error cannot create an instance of an abstract class, we wont
const task = new this();
return taskQueue.add(
{
name: this.name,
props,
},
task.options
);
}
public abstract perform(props: T): Promise<void>;
public get options(): JobOptions {
return {
priority: TaskPriority.Normal,
attempts: 5,
backoff: {
type: "exponential",
delay: 60 * 1000,
},
};
}
}

View File

@@ -0,0 +1,72 @@
import { subDays } from "date-fns";
import { Document } from "@server/models";
import { buildDocument } from "@server/test/factories";
import { flushdb } from "@server/test/support";
import CleanupDeletedDocumentsTask from "./CleanupDeletedDocumentsTask";
beforeEach(() => flushdb());
describe("CleanupDeletedDocumentsTask", () => {
it("should not destroy documents not deleted", async () => {
await buildDocument({
publishedAt: new Date(),
});
const task = new CleanupDeletedDocumentsTask();
await task.perform({ limit: 100 });
expect(
await Document.unscoped().count({
paranoid: false,
})
).toEqual(1);
});
it("should not destroy documents deleted less than 30 days ago", async () => {
await buildDocument({
publishedAt: new Date(),
deletedAt: subDays(new Date(), 25),
});
const task = new CleanupDeletedDocumentsTask();
await task.perform({ limit: 100 });
expect(
await Document.unscoped().count({
paranoid: false,
})
).toEqual(1);
});
it("should destroy documents deleted more than 30 days ago", async () => {
await buildDocument({
publishedAt: new Date(),
deletedAt: subDays(new Date(), 60),
});
const task = new CleanupDeletedDocumentsTask();
await task.perform({ limit: 100 });
expect(
await Document.unscoped().count({
paranoid: false,
})
).toEqual(0);
});
it("should destroy draft documents deleted more than 30 days ago", async () => {
await buildDocument({
publishedAt: undefined,
deletedAt: subDays(new Date(), 60),
});
const task = new CleanupDeletedDocumentsTask();
await task.perform({ limit: 100 });
expect(
await Document.unscoped().count({
paranoid: false,
})
).toEqual(0);
});
});

View File

@@ -0,0 +1,40 @@
import { subDays } from "date-fns";
import { Op } from "sequelize";
import documentPermanentDeleter from "@server/commands/documentPermanentDeleter";
import Logger from "@server/logging/logger";
import { APM } from "@server/logging/tracing";
import { Document } from "@server/models";
import BaseTask, { TaskPriority } from "./BaseTask";
type Props = {
limit: number;
};
@APM.trace()
export default class CleanupDeletedDocumentsTask extends BaseTask<Props> {
public async perform({ limit }: Props) {
Logger.info(
"task",
`Permanently destroying upto ${limit} documents older than 30 days…`
);
const documents = await Document.scope("withDrafts").findAll({
attributes: ["id", "teamId", "text", "deletedAt"],
where: {
deletedAt: {
[Op.lt]: subDays(new Date(), 30),
},
},
paranoid: false,
limit,
});
const countDeletedDocument = await documentPermanentDeleter(documents);
Logger.info("task", `Destroyed ${countDeletedDocument} documents`);
}
public get options() {
return {
attempts: 1,
priority: TaskPriority.Background,
};
}
}

View File

@@ -0,0 +1,42 @@
import { subDays } from "date-fns";
import { Op } from "sequelize";
import teamPermanentDeleter from "@server/commands/teamPermanentDeleter";
import Logger from "@server/logging/logger";
import { APM } from "@server/logging/tracing";
import { Team } from "@server/models";
import BaseTask, { TaskPriority } from "./BaseTask";
type Props = {
limit: number;
};
@APM.trace()
export default class CleanupDeletedTeamsTask extends BaseTask<Props> {
public async perform({ limit }: Props) {
Logger.info(
"task",
`Permanently destroying upto ${limit} teams older than 30 days…`
);
const teams = await Team.findAll({
where: {
deletedAt: {
[Op.lt]: subDays(new Date(), 30),
},
},
paranoid: false,
limit,
});
for (const team of teams) {
await teamPermanentDeleter(team);
}
Logger.info("task", `Destroyed ${teams.length} teams`);
}
public get options() {
return {
attempts: 1,
priority: TaskPriority.Background,
};
}
}

View File

@@ -0,0 +1,56 @@
import { subDays } from "date-fns";
import { FileOperation } from "@server/models";
import { buildFileOperation } from "@server/test/factories";
import { flushdb } from "@server/test/support";
import CleanupExpiredFileOperationsTask from "./CleanupExpiredFileOperationsTask";
beforeEach(() => flushdb());
describe("CleanupExpiredFileOperationsTask", () => {
it("should expire exports older than 30 days ago", async () => {
await buildFileOperation({
type: "export",
state: "complete",
createdAt: subDays(new Date(), 30),
});
await buildFileOperation({
type: "export",
state: "complete",
});
/* This is a test helper that creates a new task and runs it. */
const task = new CleanupExpiredFileOperationsTask();
await task.perform({ limit: 100 });
const data = await FileOperation.count({
where: {
type: "export",
state: "expired",
},
});
expect(data).toEqual(1);
});
it("should not expire exports made less than 30 days ago", async () => {
await buildFileOperation({
type: "export",
state: "complete",
createdAt: subDays(new Date(), 29),
});
await buildFileOperation({
type: "export",
state: "complete",
});
const task = new CleanupExpiredFileOperationsTask();
await task.perform({ limit: 100 });
const data = await FileOperation.count({
where: {
type: "export",
state: "expired",
},
});
expect(data).toEqual(0);
});
});

View File

@@ -0,0 +1,40 @@
import { subDays } from "date-fns";
import { Op } from "sequelize";
import Logger from "@server/logging/logger";
import { APM } from "@server/logging/tracing";
import { FileOperation } from "@server/models";
import BaseTask, { TaskPriority } from "./BaseTask";
type Props = {
limit: number;
};
@APM.trace()
export default class CleanupExpiredFileOperationsTask extends BaseTask<Props> {
public async perform({ limit }: Props) {
Logger.info("task", `Expiring export file operations older than 30 days…`);
const fileOperations = await FileOperation.unscoped().findAll({
where: {
type: "export",
createdAt: {
[Op.lt]: subDays(new Date(), 30),
},
state: {
[Op.ne]: "expired",
},
},
limit,
});
await Promise.all(
fileOperations.map((fileOperation) => fileOperation.expire())
);
Logger.info("task", `Expired ${fileOperations.length} file operations`);
}
public get options() {
return {
attempts: 1,
priority: TaskPriority.Background,
};
}
}

View File

@@ -0,0 +1,15 @@
import { APM } from "@server/logging/tracing";
import mailer, { EmailSendOptions, EmailTypes } from "../../mailer";
import BaseTask from "./BaseTask";
type Props = {
type: EmailTypes;
options: EmailSendOptions;
};
@APM.trace()
export default class EmailTask extends BaseTask<Props> {
public async perform(props: Props) {
await mailer[props.type](props.options);
}
}

View File

@@ -0,0 +1,16 @@
import { requireDirectory } from "@server/utils/fs";
const tasks = {};
requireDirectory(__dirname).forEach(([module, id]) => {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'default' does not exist on type 'unknown'
const { default: Task } = module;
if (id === "index") {
return;
}
tasks[id] = Task;
});
export default tasks;