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:
36
server/queues/tasks/BaseTask.ts
Normal file
36
server/queues/tasks/BaseTask.ts
Normal 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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
72
server/queues/tasks/CleanupDeletedDocumentsTask.test.ts
Normal file
72
server/queues/tasks/CleanupDeletedDocumentsTask.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
40
server/queues/tasks/CleanupDeletedDocumentsTask.ts
Normal file
40
server/queues/tasks/CleanupDeletedDocumentsTask.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
42
server/queues/tasks/CleanupDeletedTeamsTask.ts
Normal file
42
server/queues/tasks/CleanupDeletedTeamsTask.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
56
server/queues/tasks/CleanupExpiredFileOperationsTask.test.ts
Normal file
56
server/queues/tasks/CleanupExpiredFileOperationsTask.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
40
server/queues/tasks/CleanupExpiredFileOperationsTask.ts
Normal file
40
server/queues/tasks/CleanupExpiredFileOperationsTask.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
15
server/queues/tasks/EmailTask.ts
Normal file
15
server/queues/tasks/EmailTask.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
16
server/queues/tasks/index.ts
Normal file
16
server/queues/tasks/index.ts
Normal 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;
|
||||
Reference in New Issue
Block a user