Skip to content

Commit

Permalink
feat: worker script
Browse files Browse the repository at this point in the history
  • Loading branch information
nyet-ty committed Feb 5, 2024
1 parent c877a7f commit a1d6c9a
Show file tree
Hide file tree
Showing 14 changed files with 419 additions and 240 deletions.
413 changes: 197 additions & 216 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"@sentry/nextjs": "7.94.1",
"@tanstack/react-query": "4.29.13",
"@tanstack/react-query-devtools": "4.29.13",
"@taskany/bricks": "5.0.1",
"@taskany/bricks": "5.7.0",
"@taskany/colors": "1.8.0",
"@taskany/icons": "2.0.1",
"@trpc/client": "10.1.0",
Expand Down Expand Up @@ -78,7 +78,7 @@
},
"devDependencies": {
"@babel/core": "7.22.5",
"@commitlint/cli": "18.5.0",
"@commitlint/cli": "18.6.0",
"@commitlint/config-conventional": "18.6.0",
"@next/bundle-analyzer": "13.5.4",
"@prisma/client": "5.2.0",
Expand Down
18 changes: 18 additions & 0 deletions prisma/migrations/20240202101116_jobs/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
-- CreateTable
CREATE TABLE "Job" (
"id" TEXT NOT NULL,
"state" TEXT NOT NULL,
"priority" INTEGER NOT NULL DEFAULT 0,
"kind" TEXT NOT NULL,
"data" JSONB NOT NULL,
"delay" INTEGER,
"retry" INTEGER,
"runs" INTEGER NOT NULL DEFAULT 0,
"force" BOOLEAN NOT NULL DEFAULT false,
"cron" TEXT,
"error" TEXT,
"createdAt" TIMESTAMP NOT NULL DEFAULT timezone('utc'::text, now()),
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT timezone('utc'::text, now()),

CONSTRAINT "Job_pkey" PRIMARY KEY ("id")
);
17 changes: 17 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -429,3 +429,20 @@ model UserSettings {
theme String @default("system")
}

model Job {
id String @id @default(cuid())
state String
priority Int @default(0)
kind String
data Json
delay Int?
retry Int?
runs Int @default(0)
force Boolean @default(false)
cron String?
error String?
createdAt DateTime @default(dbgenerated("timezone('utc'::text, now())")) @db.Timestamp()
updatedAt DateTime @default(dbgenerated("timezone('utc'::text, now())")) @updatedAt
}
12 changes: 8 additions & 4 deletions src/components/OfflineBanner/OfflineBanner.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { onlineManager } from '@tanstack/react-query';
import styled from 'styled-components';
import { useOfflineDetector } from '@taskany/bricks';
import { nullable, useOfflineDetector } from '@taskany/bricks';

import { tr } from './OfflineBanner.i18n';

Expand All @@ -14,9 +14,13 @@ const StyledBanner = styled.div`
`;

export const OfflineBanner = () => {
const online = useOfflineDetector({
setStatus: (online) => onlineManager.setOnline(online),
const [globalOnlineStatus, remoteServerStatus] = useOfflineDetector({
setStatus: () => {
onlineManager.setOnline(globalOnlineStatus || remoteServerStatus);
},
remoteServerUrl: '/api/health',
});
return online ? null : <StyledBanner>{tr('You are currently offline. Check connection.')}</StyledBanner>;
return nullable(!globalOnlineStatus || !remoteServerStatus, () => (
<StyledBanner>{tr('You are currently offline. Check connection.')}</StyledBanner>
));
};
1 change: 1 addition & 0 deletions src/components/PhoneField/PhoneField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const tweakSelection = (e: SyntheticEvent<HTMLInputElement>) => {
export type PhoneFieldProps<T extends FieldValues> = InputProps & {
name: FieldPath<T>;
control: Control<T>;
size?: number;
options?: RegisterOptions<T>;
};

Expand Down
4 changes: 4 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ export default {
authUser: process.env.MAIL_USER,
enabled: process.env.MAIL_ENABLE,
},
worker: {
queueInterval: process.env.WORKER_JOBS_INTERVAL,
retryLimit: process.env.WORKER_JOBS_RETRY,
},
pluginMenuItems: parsePluginMenuItems(process.env.NEXT_PUBLIC_PLUGIN_MENU_ITEMS),
debugCookieEnabled: process.env.DEBUG_COOKIE_ENABLE,
nextAuthEnabled: process.env.NEXT_AUTH_ENABLE,
Expand Down
26 changes: 13 additions & 13 deletions src/modules/calendarEventMethods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { RRule } from 'rrule';
import { prisma } from '../utils/prisma';
import { calendarEvents, createIcalEventData } from '../utils/ical';
import { userOfEvent } from '../utils/calendar';
import { createEmailJob } from '../utils/worker/create';

import { calendarRecurrenceMethods } from './calendarRecurrenceMethods';
import { calendarMethods } from './calendarMethods';
Expand All @@ -18,7 +19,6 @@ import {
UpdateCalendarEvent,
UpdateCalendarException,
} from './calendarTypes';
import { sendMail } from './nodemailer';

async function createEvent(params: CreateCalendarEvent, user: User): Promise<CalendarEventCreateResult> {
const { date, title, duration, description = '', recurrence } = params;
Expand Down Expand Up @@ -46,7 +46,7 @@ async function createEvent(params: CreateCalendarEvent, user: User): Promise<Cal
rule: rRule.options.freq,
});

await sendMail({
await createEmailJob({
to: user.email,
subject: title,
text: '',
Expand Down Expand Up @@ -124,7 +124,7 @@ async function updateEventSeries(params: UpdateCalendarEvent, user: User): Promi
sequence: event.sequence + 1,
});

await sendMail({
await createEmailJob({
to: user.email,
subject: event.eventDetails.title,
text: '',
Expand Down Expand Up @@ -246,7 +246,7 @@ async function splitEventSeries(params: UpdateCalendarEvent, user: User): Promis
until: newRRule.options.until || undefined,
});

await sendMail({
await createEmailJob({
to: user.email,
subject: oldEvent.eventDetails.title,
text: '',
Expand All @@ -255,7 +255,7 @@ async function splitEventSeries(params: UpdateCalendarEvent, user: User): Promis
events: [icalEventDataOldEvent],
}),
});
await sendMail({
await createEmailJob({
to: user.email,
subject: newEvent.eventDetails.title,
text: '',
Expand Down Expand Up @@ -323,7 +323,7 @@ async function createEventException(params: UpdateCalendarEvent, user: User): Pr
summary: title ?? eventDetails.title,
});

await sendMail({
await createEmailJob({
to: user.email,
subject: eventDetails.title,
text: '',
Expand All @@ -333,7 +333,7 @@ async function createEventException(params: UpdateCalendarEvent, user: User): Pr
}),
});

await sendMail({
await createEmailJob({
to: user.email,
subject: title ?? eventDetails.title,
text: '',
Expand Down Expand Up @@ -373,7 +373,7 @@ async function updateEventException(params: UpdateCalendarException, user: User)
sequence: eventException.sequence,
});

await sendMail({
await createEmailJob({
to: user.email,
subject: title ?? eventDetails.title,
text: '',
Expand Down Expand Up @@ -432,7 +432,7 @@ async function stopEventSeries(eventId: string, originalDate: Date, user: User):
sequence,
});

await sendMail({
await createEmailJob({
to: user.email,
subject: eventDetails.title,
text: '',
Expand Down Expand Up @@ -466,7 +466,7 @@ async function cancelEventException(eventId: string, exceptionId: string, user:
status: ICalEventStatus.CANCELLED,
});

await sendMail({
await createEmailJob({
to: user.email,
subject: restException.eventDetails.title,
text: '',
Expand Down Expand Up @@ -518,7 +518,7 @@ async function createEventCancellation(eventId: string, originalDate: Date, user
status: ICalEventStatus.CANCELLED,
});

await sendMail({
await createEmailJob({
to: user.email,
subject: eventDetails.title,
text: '',
Expand All @@ -528,7 +528,7 @@ async function createEventCancellation(eventId: string, originalDate: Date, user
}),
});

await sendMail({
await createEmailJob({
to: user.email,
subject: eventDetails.title,
text: '',
Expand Down Expand Up @@ -556,7 +556,7 @@ async function removeEventSeries(eventId: string, user: User): Promise<void> {
sequence,
});

await sendMail({
await createEmailJob({
to: creator?.email || user.email,
subject: eventDetails.title,
text: '',
Expand Down
10 changes: 5 additions & 5 deletions src/modules/emailMethods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import { formatDateToLocaleString } from '../utils/date';
import { Paths, generatePath } from '../utils/paths';
import { calendarEvents, createIcalEventData } from '../utils/ical';
import { userOfEvent } from '../utils/calendar';
import { createEmailJob } from '../utils/worker/create';

import { UpdateSection } from './sectionTypes';
import { tr } from './modules.i18n';
import { sendMail } from './nodemailer';
import { calendarMethods } from './calendarMethods';

export const notifyHR = async (id: number, data: UpdateSection) => {
Expand All @@ -28,7 +28,7 @@ export const notifyHR = async (id: number, data: UpdateSection) => {
});
if (section?.interview?.creator?.email) {
// TODO: add localization after https://github.com/taskany-inc/hire/issues/191
return sendMail({
return createEmailJob({
to: section?.interview?.creator?.email,
subject: `Interviewer ${section.interviewer.name || ''} left feedback for the section with ${
section.interview.candidate.name
Expand Down Expand Up @@ -90,7 +90,7 @@ export const cancelSectionEmail = async (sectionId: number) => {
cancelEmailSubject = restException.eventDetails.title;
}

return sendMail({
return createEmailJob({
to: section.interviewer.email,
subject: cancelEmailSubject,
text: `${tr('Canceled section with')} ${section.interview.candidate.name} ${date}
Expand Down Expand Up @@ -163,7 +163,7 @@ export const assignSectionEmail = async (
sequence,
});

await sendMail({
await createEmailJob({
to: interviewer.email,
subject: eventDetails.title,
text: '',
Expand All @@ -174,7 +174,7 @@ export const assignSectionEmail = async (
});
}

await sendMail({
await createEmailJob({
to: interviewer.email,
subject: `${tr('Interview with')} ${candidateName}`,
text: `${config.defaultPageURL}${generatePath(Paths.SECTION, {
Expand Down
3 changes: 3 additions & 0 deletions src/pages/404.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ export type ErrorProps = {
};

export default function Custom404() {
if (typeof window === 'undefined') {
return;
}
return (
<Sentry.ErrorBoundary fallback={<p>404 on hire</p>}>
<LayoutMain pageTitle="Nothing found 😔" />
Expand Down
45 changes: 45 additions & 0 deletions src/utils/worker/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { prisma } from '../prisma';
import { MessageBody } from '../../modules/nodemailer';

export const defaultJobDelay = process.env.WORKER_JOBS_DELAY ? parseInt(process.env.WORKER_JOBS_DELAY, 10) : 1000;

export enum jobState {
scheduled = 'scheduled',
pending = 'pending',
completed = 'completed',
}

export interface JobDataMap {
email: {
data: MessageBody;
};
}

export type JobKind = keyof JobDataMap;

interface CreateJobProps<K extends keyof JobDataMap> {
data: JobDataMap[K];
priority?: number;
delay?: number;
cron?: string;
}

export function createJob<K extends keyof JobDataMap>(
kind: K,
{ data, priority, delay = defaultJobDelay, cron }: CreateJobProps<K>,
) {
return prisma.job.create({
data: {
state: jobState.scheduled,
data,
kind,
priority,
delay,
cron,
},
});
}

export function createEmailJob(data: MessageBody) {
return createJob('email', { data: { data } });
}
48 changes: 48 additions & 0 deletions src/utils/worker/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import * as Sentry from '@sentry/nextjs';
import { worker, Job } from '@taskany/bricks';

import config from '../../config';

import { defaultJobDelay } from './create';
import * as resolve from './resolve';
import { getNextJob, jobDelete, jobUpdate } from './jobOperations';

const queueInterval = config.worker.queueInterval ? parseInt(config.worker.queueInterval, 10) : 3000;
const retryLimit = config.worker.retryLimit ? parseInt(config.worker.retryLimit, 10) : 3;

// eslint-disable-next-line no-console
const log = (...rest: unknown[]) => console.log('[WORKER]:', ...rest);

log('Worker started successfully');

const onRetryLimitExeed = (error: any, job: Job) =>
Sentry.captureException(error, {
fingerprint: ['worker', 'resolve', 'retry'],
extra: {
job,
},
});

const onQueeTooLong = () => Sentry.captureMessage('Queue too long. Smth went wrong.');

// eslint-disable-next-line no-console
const onError = (error: any) => console.log(error.message);

const init = () =>
worker(
getNextJob,
jobUpdate,
jobDelete,
resolve,
onRetryLimitExeed,
onQueeTooLong,
log,
onError,
defaultJobDelay,
retryLimit,
);

(() =>
setInterval(async () => {
await init();
}, queueInterval))();
Loading

0 comments on commit a1d6c9a

Please sign in to comment.