144 lines
4.7 KiB
JavaScript
144 lines
4.7 KiB
JavaScript
'use strict';
|
|
|
|
const cron = require('node-cron');
|
|
|
|
const planningDatabase = require('../services/planningDatabase');
|
|
const {
|
|
syncPlanningDingWorkflowTables,
|
|
} = require('../services/planningDingWorkflowSyncService');
|
|
|
|
let running = false;
|
|
|
|
const JOB_ENABLED = String(process.env.DING_WORKFLOW_SYNC_ENABLED || 'true').toLowerCase() !== 'false';
|
|
const HOURLY_CRON = process.env.DING_WORKFLOW_SYNC_CRON || '5 * * * *';
|
|
const FULL_CRON = process.env.DING_WORKFLOW_SYNC_FULL_CRON || '35 3 * * *';
|
|
const TIMEZONE = process.env.DING_WORKFLOW_SYNC_TIMEZONE || 'Asia/Shanghai';
|
|
const STARTUP_SYNC_ENABLED = String(process.env.DING_WORKFLOW_SYNC_STARTUP_ENABLED || 'true').toLowerCase() !== 'false';
|
|
const STARTUP_FULL_SYNC = String(process.env.DING_WORKFLOW_SYNC_STARTUP_FULL || 'false').toLowerCase() === 'true';
|
|
const STARTUP_DELAY_MS = Math.max(
|
|
Number.parseInt(process.env.DING_WORKFLOW_SYNC_STARTUP_DELAY_MS || '30000', 10) || 30000,
|
|
1000
|
|
);
|
|
const JOB_LOCK_WAIT_SECONDS = Math.max(
|
|
Number.parseInt(process.env.DING_WORKFLOW_SYNC_LOCK_WAIT_SECONDS || '1', 10) || 1,
|
|
0
|
|
);
|
|
const CHUNK_SIZE = Math.max(Number.parseInt(process.env.DING_WORKFLOW_SYNC_CHUNK_SIZE || '2000', 10) || 2000, 100);
|
|
const LOOKBACK_MINUTES = Math.max(
|
|
Number.parseInt(process.env.DING_WORKFLOW_SYNC_LOOKBACK_MINUTES || '180', 10) || 180,
|
|
0
|
|
);
|
|
const LOOKBACK_ROWS = Math.max(
|
|
Number.parseInt(process.env.DING_WORKFLOW_SYNC_LOOKBACK_ROWS || '10000', 10) || 10000,
|
|
0
|
|
);
|
|
|
|
async function withGlobalJobLock(lockKey, trigger, runner) {
|
|
const pool = await planningDatabase.getPool();
|
|
const connection = await pool.getConnection();
|
|
let lockAcquired = false;
|
|
|
|
try {
|
|
const [rows] = await connection.query('SELECT GET_LOCK(?, ?) AS lockStatus', [lockKey, JOB_LOCK_WAIT_SECONDS]);
|
|
const lockStatus = rows && rows[0] ? Number(rows[0].lockStatus) : 0;
|
|
lockAcquired = lockStatus === 1;
|
|
|
|
if (!lockAcquired) {
|
|
console.log(`[DingWorkflowSync] global lock not acquired, skip trigger=${trigger}`);
|
|
return { success: false, skipped: true, reason: 'global_lock_not_acquired', trigger };
|
|
}
|
|
|
|
return runner();
|
|
} finally {
|
|
try {
|
|
if (lockAcquired) {
|
|
await connection.query('DO RELEASE_LOCK(?)', [lockKey]);
|
|
}
|
|
} finally {
|
|
connection.release();
|
|
}
|
|
}
|
|
}
|
|
|
|
async function runPlanningDingWorkflowSyncJob(trigger = 'manual', options = {}) {
|
|
if (running) {
|
|
console.log(`[DingWorkflowSync] job is already running, skip trigger=${trigger}`);
|
|
return { success: false, skipped: true, reason: 'running', trigger };
|
|
}
|
|
|
|
running = true;
|
|
const startedAt = Date.now();
|
|
|
|
try {
|
|
return await withGlobalJobLock('planning_ding_workflow_sync', trigger, async () => {
|
|
const full = Boolean(options.full);
|
|
const table = options.table || 'all';
|
|
const result = await syncPlanningDingWorkflowTables({
|
|
table,
|
|
full,
|
|
chunkSize: options.chunkSize || CHUNK_SIZE,
|
|
lookbackMinutes: options.lookbackMinutes || LOOKBACK_MINUTES,
|
|
lookbackRows: options.lookbackRows || LOOKBACK_ROWS,
|
|
onProgress: (payload) => {
|
|
console.log(
|
|
`[DingWorkflowSync] ${trigger} ${payload.table} ${payload.fetchedRows}/${payload.totalRows}`
|
|
);
|
|
},
|
|
});
|
|
|
|
return {
|
|
...result,
|
|
trigger,
|
|
elapsedMs: Date.now() - startedAt,
|
|
};
|
|
});
|
|
} finally {
|
|
running = false;
|
|
}
|
|
}
|
|
|
|
function startPlanningDingWorkflowSyncSchedulers() {
|
|
if (!JOB_ENABLED) {
|
|
console.log('[Scheduler] 钉钉流程表同步任务未启用');
|
|
return;
|
|
}
|
|
|
|
cron.schedule(HOURLY_CRON, () => {
|
|
runPlanningDingWorkflowSyncJob('hourly_incremental', { full: false }).catch((error) => {
|
|
console.error('[DingWorkflowSync] hourly scheduled job failed', error);
|
|
});
|
|
}, { timezone: TIMEZONE });
|
|
|
|
console.log(`[Scheduler] 钉钉流程表同步任务已注册: ${HOURLY_CRON} (${TIMEZONE})`);
|
|
|
|
if (FULL_CRON && String(FULL_CRON).trim()) {
|
|
cron.schedule(FULL_CRON, () => {
|
|
runPlanningDingWorkflowSyncJob('daily_full_reconcile', { full: true }).catch((error) => {
|
|
console.error('[DingWorkflowSync] daily full reconcile failed', error);
|
|
});
|
|
}, { timezone: TIMEZONE });
|
|
|
|
console.log(`[Scheduler] 钉钉流程表全量校准任务已注册: ${FULL_CRON} (${TIMEZONE})`);
|
|
}
|
|
|
|
if (STARTUP_SYNC_ENABLED) {
|
|
const timer = setTimeout(() => {
|
|
runPlanningDingWorkflowSyncJob(
|
|
STARTUP_FULL_SYNC ? 'startup_full_sync' : 'startup_incremental_sync',
|
|
{ full: STARTUP_FULL_SYNC }
|
|
).catch((error) => {
|
|
console.error('[DingWorkflowSync] startup sync failed', error);
|
|
});
|
|
}, STARTUP_DELAY_MS);
|
|
|
|
if (typeof timer.unref === 'function') {
|
|
timer.unref();
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
startPlanningDingWorkflowSyncSchedulers,
|
|
runPlanningDingWorkflowSyncJob,
|
|
};
|