chore: Move initial avatar upload to background worker (#3727)

* chore: Async user avatar upload processor

* chore: Async team avatar upload

* Refactor to task for retries

* Docs
Include avatarUrl in task props to prevent race condition
Remove transaction around upload fetch request
This commit is contained in:
Tom Moor
2022-07-03 11:36:15 +02:00
committed by GitHub
parent 1f3a1d4b86
commit 62d9bf7105
14 changed files with 187 additions and 74 deletions

View File

@@ -0,0 +1,40 @@
import { Team, User } from "@server/models";
import { Event, TeamEvent, UserEvent } from "@server/types";
import UploadTeamAvatarTask from "../tasks/UploadTeamAvatarTask";
import UploadUserAvatarTask from "../tasks/UploadUserAvatarTask";
import BaseProcessor from "./BaseProcessor";
export default class AvatarProcessor extends BaseProcessor {
static applicableEvents: Event["name"][] = ["users.create", "teams.create"];
async perform(event: UserEvent | TeamEvent) {
// The uploads are performed in a separate task to allow for retrying in the
// case of failures as it involves network calls to third party services.
if (event.name === "users.create") {
const user = await User.findByPk(event.userId, {
rejectOnEmpty: true,
});
if (user.avatarUrl) {
await UploadUserAvatarTask.schedule({
userId: event.userId,
avatarUrl: user.avatarUrl,
});
}
}
if (event.name === "teams.create") {
const team = await Team.findByPk(event.teamId, {
rejectOnEmpty: true,
});
if (team.avatarUrl) {
await UploadTeamAvatarTask.schedule({
teamId: event.teamId,
avatarUrl: team.avatarUrl,
});
}
}
}
}

View File

@@ -157,6 +157,9 @@ export default class DeliverWebhookTask extends BaseTask<Props> {
case "integrations.update":
await this.handleIntegrationEvent(subscription, event);
return;
case "teams.create":
// Ignored
return;
case "teams.update":
await this.handleTeamEvent(subscription, event);
return;

View File

@@ -0,0 +1,40 @@
import { v4 as uuidv4 } from "uuid";
import { Team } from "@server/models";
import { uploadToS3FromUrl } from "@server/utils/s3";
import BaseTask, { TaskPriority } from "./BaseTask";
type Props = {
/* The teamId to operate on */
teamId: string;
/* The original avatarUrl from the SSO provider */
avatarUrl: string;
};
/**
* A task that uploads the provided avatarUrl to S3 storage and updates the
* team's record with the new url.
*/
export default class UploadTeamAvatarTask extends BaseTask<Props> {
public async perform(props: Props) {
const team = await Team.findByPk(props.teamId, {
rejectOnEmpty: true,
});
const avatarUrl = await uploadToS3FromUrl(
props.avatarUrl,
`avatars/${team.id}/${uuidv4()}`,
"public-read"
);
if (avatarUrl) {
await team.update({ avatarUrl });
}
}
public get options() {
return {
attempts: 3,
priority: TaskPriority.Normal,
};
}
}

View File

@@ -0,0 +1,40 @@
import { v4 as uuidv4 } from "uuid";
import { User } from "@server/models";
import { uploadToS3FromUrl } from "@server/utils/s3";
import BaseTask, { TaskPriority } from "./BaseTask";
type Props = {
/* The userId to operate on */
userId: string;
/* The original avatarUrl from the SSO provider */
avatarUrl: string;
};
/**
* A task that uploads the provided avatarUrl to S3 storage and updates the
* user's record with the new url.
*/
export default class UploadUserAvatarTask extends BaseTask<Props> {
public async perform(props: Props) {
const user = await User.findByPk(props.userId, {
rejectOnEmpty: true,
});
const avatarUrl = await uploadToS3FromUrl(
props.avatarUrl,
`avatars/${user.id}/${uuidv4()}`,
"public-read"
);
if (avatarUrl) {
await user.update({ avatarUrl });
}
}
public get options() {
return {
attempts: 3,
priority: TaskPriority.Normal,
};
}
}