160 lines
4.5 KiB
JavaScript
160 lines
4.5 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
const args = new Set(process.argv.slice(2));
|
|
const intervalSecondsRaw = process.env.CAMPAIGN_RETRY_DAEMON_INTERVAL_SECONDS?.trim();
|
|
const timeoutMsRaw = process.env.CAMPAIGN_RETRY_DAEMON_TIMEOUT_MS?.trim();
|
|
const runOnce = args.has("--once") || args.has("-1");
|
|
const shouldJitter = !args.has("--no-jitter");
|
|
|
|
const intervalMs = normalizePositiveNumber(intervalSecondsRaw, 300) * 1000;
|
|
const requestTimeoutMs = normalizePositiveNumber(timeoutMsRaw, 30000);
|
|
|
|
if (isNaN(intervalMs) || intervalMs <= 0) {
|
|
console.error("Invalid CAMPAIGN_RETRY_DAEMON_INTERVAL_SECONDS");
|
|
process.exit(1);
|
|
}
|
|
|
|
const endpoint = resolveCampaignRetryEndpoint();
|
|
const token = process.env.CAMPAIGN_RETRY_JOB_TOKEN?.trim();
|
|
|
|
const payload = {
|
|
...(process.env.CAMPAIGN_RETRY_TENANT_ID?.trim() ? { tenantId: process.env.CAMPAIGN_RETRY_TENANT_ID.trim() } : {}),
|
|
...(process.env.CAMPAIGN_RETRY_CAMPAIGN_ID?.trim()
|
|
? { campaignId: process.env.CAMPAIGN_RETRY_CAMPAIGN_ID.trim() }
|
|
: {}),
|
|
...(isPositiveInt(process.env.CAMPAIGN_RETRY_BATCH_SIZE)
|
|
? { recipientBatchSize: Number(process.env.CAMPAIGN_RETRY_BATCH_SIZE) }
|
|
: {}),
|
|
...(isPositiveInt(process.env.CAMPAIGN_RETRY_MAX_CAMPAIGNS)
|
|
? { maxCampaigns: Number(process.env.CAMPAIGN_RETRY_MAX_CAMPAIGNS) }
|
|
: {})
|
|
};
|
|
|
|
let isShuttingDown = false;
|
|
let lastSummary = null;
|
|
|
|
process.on("SIGINT", () => {
|
|
isShuttingDown = true;
|
|
console.info("\n[daemon] shutdown requested");
|
|
});
|
|
|
|
process.on("SIGTERM", () => {
|
|
isShuttingDown = true;
|
|
console.info("\n[daemon] shutdown requested");
|
|
});
|
|
|
|
(async function main() {
|
|
if (runOnce) {
|
|
await runLoopOnce();
|
|
} else {
|
|
await runDaemon();
|
|
}
|
|
})();
|
|
|
|
async function runDaemon() {
|
|
console.info(`[daemon] starting campaign retry worker, interval=${intervalMs / 1000}s`);
|
|
while (!isShuttingDown) {
|
|
await runLoopOnce();
|
|
if (isShuttingDown) {
|
|
break;
|
|
}
|
|
|
|
const jitter = shouldJitter ? Math.random() * Math.min(1000, intervalMs * 0.1) : 0;
|
|
const waitMs = Math.max(1000, intervalMs + jitter);
|
|
await sleep(waitMs);
|
|
}
|
|
|
|
if (lastSummary) {
|
|
console.info(`[daemon] last run summary: ${JSON.stringify(lastSummary)}`);
|
|
}
|
|
console.info("[daemon] stopped");
|
|
}
|
|
|
|
async function runLoopOnce() {
|
|
const startedAt = new Date().toISOString();
|
|
const headers = {
|
|
"content-type": "application/json"
|
|
};
|
|
if (token) {
|
|
headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), requestTimeoutMs);
|
|
|
|
try {
|
|
const response = await fetch(endpoint, {
|
|
method: "POST",
|
|
headers,
|
|
body: JSON.stringify(payload),
|
|
signal: controller.signal
|
|
});
|
|
|
|
const text = await response.text();
|
|
clearTimeout(timeout);
|
|
|
|
if (!response.ok) {
|
|
console.error(`[${startedAt}] campaign retry failed: ${response.status} ${response.statusText}`);
|
|
console.error(text);
|
|
lastSummary = { ts: startedAt, status: "failed", code: response.status, message: response.statusText };
|
|
if (runOnce) {
|
|
process.exitCode = 1;
|
|
}
|
|
return;
|
|
}
|
|
|
|
lastSummary = safeParseJson(text) ?? { ts: startedAt, raw: text, status: "ok" };
|
|
console.log(`[${startedAt}] campaign retry done: ${typeof text === "string" ? text.slice(0, 250) : text}`);
|
|
} catch (error) {
|
|
clearTimeout(timeout);
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
lastSummary = { ts: startedAt, status: "error", message };
|
|
console.error(`[${startedAt}] campaign retry request failed: ${message}`);
|
|
if (runOnce) {
|
|
process.exitCode = 1;
|
|
}
|
|
}
|
|
|
|
if (runOnce) {
|
|
process.exit(process.exitCode ?? 0);
|
|
}
|
|
}
|
|
|
|
function resolveCampaignRetryEndpoint() {
|
|
const baseUrl = process.env.CAMPAIGN_RETRY_JOB_URL?.trim() || process.env.APP_URL?.trim() || process.env.NEXT_PUBLIC_APP_URL?.trim();
|
|
|
|
if (!baseUrl) {
|
|
console.error("Missing CAMPAIGN_RETRY_JOB_URL / APP_URL / NEXT_PUBLIC_APP_URL");
|
|
process.exit(1);
|
|
}
|
|
|
|
return baseUrl.endsWith("/api/jobs/campaign-retry")
|
|
? baseUrl
|
|
: `${baseUrl.replace(/\/+$/, "")}/api/jobs/campaign-retry`;
|
|
}
|
|
|
|
function normalizePositiveNumber(value, fallback) {
|
|
const raw = Number(value);
|
|
if (!Number.isFinite(raw) || raw <= 0) {
|
|
return fallback;
|
|
}
|
|
return raw;
|
|
}
|
|
|
|
function isPositiveInt(raw) {
|
|
const value = Number(raw);
|
|
return Number.isInteger(value) && value > 0;
|
|
}
|
|
|
|
function safeParseJson(raw) {
|
|
try {
|
|
return JSON.parse(raw);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function sleep(ms) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|