add backend hatchet worker for calendar predue sync
This commit is contained in:
13
backend_worker/Dockerfile
Normal file
13
backend_worker/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM node:22-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY src ./src
|
||||
COPY tsconfig.json ./tsconfig.json
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
CMD ["npm", "run", "start"]
|
||||
24
backend_worker/README.md
Normal file
24
backend_worker/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# backend_worker
|
||||
|
||||
Hatchet worker для периодических backend-задач.
|
||||
|
||||
## Назначение
|
||||
|
||||
- запускает cron workflow `backend-calendar-timeline-scheduler`;
|
||||
- вызывает `backend` GraphQL mutation `syncCalendarPredueTimeline`;
|
||||
- заменяет legacy `schedulers/` сервис для предзаписи календарных событий в `ClientTimelineEntry`.
|
||||
|
||||
## Переменные окружения
|
||||
|
||||
- `BACKEND_GRAPHQL_URL` (required)
|
||||
- `BACKEND_GRAPHQL_SHARED_SECRET` (optional)
|
||||
- `BACKEND_TIMELINE_SYNC_CRON` (default: `* * * * *`)
|
||||
- `HATCHET_CLIENT_TOKEN` (required)
|
||||
- `HATCHET_CLIENT_TLS_STRATEGY` (optional, например `none` для self-host без TLS)
|
||||
- `HATCHET_CLIENT_HOST_PORT` (optional, например `hatchet-engine:7070`)
|
||||
- `HATCHET_CLIENT_API_URL` (optional)
|
||||
|
||||
## Скрипты
|
||||
|
||||
- `npm run start` — запуск Hatchet worker.
|
||||
- `npm run typecheck` — проверка TypeScript.
|
||||
1456
backend_worker/package-lock.json
generated
Normal file
1456
backend_worker/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
backend_worker/package.json
Normal file
17
backend_worker/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "crm-backend-worker",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "tsx src/hatchet/worker.ts",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hatchet-dev/typescript-sdk": "^1.15.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.13.9",
|
||||
"tsx": "^4.20.5",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
3
backend_worker/src/hatchet/client.ts
Normal file
3
backend_worker/src/hatchet/client.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { HatchetClient } from "@hatchet-dev/typescript-sdk/v1";
|
||||
|
||||
export const hatchet = HatchetClient.init();
|
||||
22
backend_worker/src/hatchet/worker.ts
Normal file
22
backend_worker/src/hatchet/worker.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { hatchet } from "./client";
|
||||
import { backendCalendarTimelineScheduler } from "./workflow";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
async function main() {
|
||||
const worker = await hatchet.worker("backend-worker", {
|
||||
workflows: [backendCalendarTimelineScheduler],
|
||||
});
|
||||
|
||||
await worker.start();
|
||||
}
|
||||
|
||||
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
||||
|
||||
if (isMain) {
|
||||
main().catch((error) => {
|
||||
const message = error instanceof Error ? error.stack || error.message : String(error);
|
||||
console.error(`[backend_worker/hatchet] worker failed: ${message}`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
119
backend_worker/src/hatchet/workflow.ts
Normal file
119
backend_worker/src/hatchet/workflow.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { hatchet } from "./client";
|
||||
|
||||
type SyncCalendarPredueResult = {
|
||||
syncCalendarPredueTimeline: {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
now: string;
|
||||
scanned: number;
|
||||
updated: number;
|
||||
skippedBeforeWindow: number;
|
||||
skippedLocked: boolean;
|
||||
preDueMinutes: number;
|
||||
lookbackMinutes: number;
|
||||
lookaheadMinutes: number;
|
||||
lockKey: number;
|
||||
};
|
||||
};
|
||||
|
||||
type GraphqlResponse<T> = {
|
||||
data?: T;
|
||||
errors?: Array<{ message?: string }>;
|
||||
};
|
||||
|
||||
function asString(value: unknown) {
|
||||
if (typeof value !== "string") return null;
|
||||
const v = value.trim();
|
||||
return v || null;
|
||||
}
|
||||
|
||||
function requiredEnv(name: string) {
|
||||
const value = asString(process.env[name]);
|
||||
if (!value) {
|
||||
throw new Error(`${name} is required`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async function callBackendSyncMutation() {
|
||||
const url = requiredEnv("BACKEND_GRAPHQL_URL");
|
||||
const secret = asString(process.env.BACKEND_GRAPHQL_SHARED_SECRET);
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
"content-type": "application/json",
|
||||
};
|
||||
|
||||
if (secret) {
|
||||
headers["x-graphql-secret"] = secret;
|
||||
}
|
||||
|
||||
const query = `mutation SyncCalendarPredueTimeline {
|
||||
syncCalendarPredueTimeline {
|
||||
ok
|
||||
message
|
||||
now
|
||||
scanned
|
||||
updated
|
||||
skippedBeforeWindow
|
||||
skippedLocked
|
||||
preDueMinutes
|
||||
lookbackMinutes
|
||||
lookaheadMinutes
|
||||
lockKey
|
||||
}
|
||||
}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
operationName: "SyncCalendarPredueTimeline",
|
||||
query,
|
||||
variables: {},
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = (await response.json()) as GraphqlResponse<SyncCalendarPredueResult>;
|
||||
if (!response.ok || payload.errors?.length) {
|
||||
const message = payload.errors?.map((error) => error.message).filter(Boolean).join("; ") || `HTTP ${response.status}`;
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
const result = payload.data?.syncCalendarPredueTimeline;
|
||||
if (!result?.ok) {
|
||||
throw new Error(result?.message || "syncCalendarPredueTimeline failed");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const BACKEND_WORKER_CRON = asString(process.env.BACKEND_TIMELINE_SYNC_CRON) || "* * * * *";
|
||||
|
||||
export const backendCalendarTimelineScheduler = hatchet.workflow({
|
||||
name: "backend-calendar-timeline-scheduler",
|
||||
on: {
|
||||
cron: BACKEND_WORKER_CRON,
|
||||
},
|
||||
});
|
||||
|
||||
backendCalendarTimelineScheduler.task({
|
||||
name: "sync-calendar-predue-timeline-in-backend",
|
||||
retries: 6,
|
||||
backoff: {
|
||||
factor: 2,
|
||||
maxSeconds: 60,
|
||||
},
|
||||
fn: async (_, ctx) => {
|
||||
const result = await callBackendSyncMutation();
|
||||
|
||||
await ctx.logger.info("backend timeline predue sync completed", {
|
||||
scanned: result.scanned,
|
||||
updated: result.updated,
|
||||
skippedBeforeWindow: result.skippedBeforeWindow,
|
||||
skippedLocked: result.skippedLocked,
|
||||
now: result.now,
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
});
|
||||
14
backend_worker/tsconfig.json
Normal file
14
backend_worker/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"types": ["node"],
|
||||
"resolveJsonModule": true,
|
||||
"verbatimModuleSyntax": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user