feat(network-map): implement next run scheduling persistence and recovery logic for network map scheduler
This commit is contained in:
@@ -11,9 +11,11 @@ const { readS3TextObject, writeS3JsonObject } = require('./s3Service');
|
|||||||
|
|
||||||
const UI_SETTINGS_KEY = 'bgp_data/rt_ui_settings.json';
|
const UI_SETTINGS_KEY = 'bgp_data/rt_ui_settings.json';
|
||||||
const NETWORK_MAP_CACHE_KEY = 'network-map-cache/latest.json';
|
const NETWORK_MAP_CACHE_KEY = 'network-map-cache/latest.json';
|
||||||
|
const NEXT_RUN_AT_KEY = 'networkMapSchedulerNextRunAt';
|
||||||
|
|
||||||
const DEFAULT_INTERVAL_MINUTES = 5;
|
const DEFAULT_INTERVAL_MINUTES = 5;
|
||||||
const DEFAULT_LOG_MAX_LINES = 500;
|
const DEFAULT_LOG_MAX_LINES = 500;
|
||||||
|
const RESTART_RECOVERY_DELAY_MS = 10 * 60 * 1000;
|
||||||
|
|
||||||
/** Кольцевой буфер логов */
|
/** Кольцевой буфер логов */
|
||||||
class LogBuffer {
|
class LogBuffer {
|
||||||
@@ -329,16 +331,34 @@ async function writeCache(pingMap, speedMap) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let schedulerTimer = null;
|
let schedulerTimer = null;
|
||||||
|
let schedulerStartTimeout = null;
|
||||||
/** Время последнего запуска по расписанию (для расчёта следующего) */
|
/** Время последнего запуска по расписанию (для расчёта следующего) */
|
||||||
let lastScheduledRunAt = null;
|
let lastScheduledRunAt = null;
|
||||||
|
/** Реально запланированное время следующего запуска */
|
||||||
|
let nextScheduledRunAt = null;
|
||||||
|
/** Интервал активного таймера (мс) */
|
||||||
|
let activeIntervalMs = null;
|
||||||
|
|
||||||
|
async function persistNextRunAt(nextRunAt) {
|
||||||
|
try {
|
||||||
|
const ui = await loadUiSettings();
|
||||||
|
ui[NEXT_RUN_AT_KEY] = typeof nextRunAt === 'number' ? Math.round(nextRunAt) : null;
|
||||||
|
await writeS3JsonObject(UI_SETTINGS_KEY, ui);
|
||||||
|
} catch (e) {
|
||||||
|
logBuffer.push(`Не удалось сохранить nextRunAt: ${e?.message || e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getSettingsFromUiSettings(uiSettings) {
|
function getSettingsFromUiSettings(uiSettings) {
|
||||||
|
const persistedNextRunAtRaw = uiSettings?.[NEXT_RUN_AT_KEY];
|
||||||
|
const persistedNextRunAt = Number(persistedNextRunAtRaw);
|
||||||
return {
|
return {
|
||||||
enabled: Boolean(uiSettings.networkMapSchedulerEnabled),
|
enabled: Boolean(uiSettings.networkMapSchedulerEnabled),
|
||||||
intervalMinutes: Math.max(1, Math.min(1440, parseInt(uiSettings.networkMapSchedulerIntervalMinutes, 10) || DEFAULT_INTERVAL_MINUTES)),
|
intervalMinutes: Math.max(1, Math.min(1440, parseInt(uiSettings.networkMapSchedulerIntervalMinutes, 10) || DEFAULT_INTERVAL_MINUTES)),
|
||||||
runPing: uiSettings.networkMapSchedulerRunPing !== false,
|
runPing: uiSettings.networkMapSchedulerRunPing !== false,
|
||||||
runSpeedTest: uiSettings.networkMapSchedulerRunSpeedTest !== false,
|
runSpeedTest: uiSettings.networkMapSchedulerRunSpeedTest !== false,
|
||||||
logMaxLines: Math.max(100, Math.min(2000, parseInt(uiSettings.networkMapSchedulerLogMaxLines, 10) || DEFAULT_LOG_MAX_LINES)),
|
logMaxLines: Math.max(100, Math.min(2000, parseInt(uiSettings.networkMapSchedulerLogMaxLines, 10) || DEFAULT_LOG_MAX_LINES)),
|
||||||
|
persistedNextRunAt: Number.isFinite(persistedNextRunAt) ? persistedNextRunAt : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,7 +372,13 @@ function initNetworkMapScheduler(logger, port) {
|
|||||||
const baseUrl = `http://127.0.0.1:${port}`;
|
const baseUrl = `http://127.0.0.1:${port}`;
|
||||||
|
|
||||||
async function tick(source = 'interval') {
|
async function tick(source = 'interval') {
|
||||||
lastScheduledRunAt = Date.now();
|
if (source === 'interval') {
|
||||||
|
lastScheduledRunAt = Date.now();
|
||||||
|
if (activeIntervalMs) {
|
||||||
|
nextScheduledRunAt = lastScheduledRunAt + activeIntervalMs;
|
||||||
|
persistNextRunAt(nextScheduledRunAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
let uiSettings;
|
let uiSettings;
|
||||||
try {
|
try {
|
||||||
const raw = await readS3TextObject(UI_SETTINGS_KEY).catch(() => ({ body: '{}' }));
|
const raw = await readS3TextObject(UI_SETTINGS_KEY).catch(() => ({ body: '{}' }));
|
||||||
@@ -390,27 +416,62 @@ function initNetworkMapScheduler(logger, port) {
|
|||||||
}
|
}
|
||||||
const settings = getSettingsFromUiSettings(uiSettings);
|
const settings = getSettingsFromUiSettings(uiSettings);
|
||||||
if (!settings.enabled) {
|
if (!settings.enabled) {
|
||||||
|
activeIntervalMs = null;
|
||||||
|
nextScheduledRunAt = null;
|
||||||
|
persistNextRunAt(null);
|
||||||
log.info({ component: 'network-map-scheduler' }, 'Scheduler disabled in settings');
|
log.info({ component: 'network-map-scheduler' }, 'Scheduler disabled in settings');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logBuffer.setMaxLines(settings.logMaxLines);
|
logBuffer.setMaxLines(settings.logMaxLines);
|
||||||
const intervalMs = settings.intervalMinutes * 60 * 1000;
|
const intervalMs = settings.intervalMinutes * 60 * 1000;
|
||||||
|
activeIntervalMs = intervalMs;
|
||||||
|
const now = Date.now();
|
||||||
|
const persisted = settings.persistedNextRunAt;
|
||||||
|
let firstDelayMs = intervalMs;
|
||||||
|
if (typeof persisted === 'number' && Number.isFinite(persisted)) {
|
||||||
|
if (persisted > now) {
|
||||||
|
// Плановая дата ещё не наступила — сохраняем исходный план.
|
||||||
|
firstDelayMs = Math.max(1000, persisted - now);
|
||||||
|
} else {
|
||||||
|
// Плановая дата уже прошла (контейнер был перезапущен/простаивал) —
|
||||||
|
// первый запуск через 10 минут от старта контейнера.
|
||||||
|
firstDelayMs = RESTART_RECOVERY_DELAY_MS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nextScheduledRunAt = now + firstDelayMs;
|
||||||
|
persistNextRunAt(nextScheduledRunAt);
|
||||||
log.info(
|
log.info(
|
||||||
{ component: 'network-map-scheduler', intervalMinutes: settings.intervalMinutes },
|
{
|
||||||
'Network map scheduler started (runs every %d min by interval only)',
|
component: 'network-map-scheduler',
|
||||||
|
intervalMinutes: settings.intervalMinutes,
|
||||||
|
firstDelayMs,
|
||||||
|
recoveryDelayMs: RESTART_RECOVERY_DELAY_MS,
|
||||||
|
},
|
||||||
|
'Network map scheduler started (first run in %d sec, then every %d min)',
|
||||||
|
Math.round(firstDelayMs / 1000),
|
||||||
settings.intervalMinutes
|
settings.intervalMinutes
|
||||||
);
|
);
|
||||||
schedulerTimer = setInterval(() => tick('interval'), intervalMs);
|
schedulerStartTimeout = setTimeout(() => {
|
||||||
|
tick('interval');
|
||||||
|
schedulerTimer = setInterval(() => tick('interval'), intervalMs);
|
||||||
|
}, firstDelayMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
schedule();
|
schedule();
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopNetworkMapScheduler() {
|
function stopNetworkMapScheduler() {
|
||||||
|
if (schedulerStartTimeout) {
|
||||||
|
clearTimeout(schedulerStartTimeout);
|
||||||
|
schedulerStartTimeout = null;
|
||||||
|
}
|
||||||
if (schedulerTimer) {
|
if (schedulerTimer) {
|
||||||
clearInterval(schedulerTimer);
|
clearInterval(schedulerTimer);
|
||||||
schedulerTimer = null;
|
schedulerTimer = null;
|
||||||
}
|
}
|
||||||
|
activeIntervalMs = null;
|
||||||
|
nextScheduledRunAt = null;
|
||||||
|
persistNextRunAt(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLogs() {
|
function getLogs() {
|
||||||
@@ -438,9 +499,19 @@ function pushLog(msg) {
|
|||||||
*/
|
*/
|
||||||
function getNextRunAt(settings) {
|
function getNextRunAt(settings) {
|
||||||
if (!settings || !settings.enabled) return null;
|
if (!settings || !settings.enabled) return null;
|
||||||
const intervalMs = (settings.intervalMinutes || DEFAULT_INTERVAL_MINUTES) * 60 * 1000;
|
if (typeof nextScheduledRunAt === 'number' && Number.isFinite(nextScheduledRunAt)) {
|
||||||
const from = lastScheduledRunAt || Date.now();
|
return nextScheduledRunAt;
|
||||||
return from + intervalMs;
|
}
|
||||||
|
if (typeof lastScheduledRunAt === 'number' && Number.isFinite(lastScheduledRunAt)) {
|
||||||
|
const intervalMs = activeIntervalMs || (settings.intervalMinutes || DEFAULT_INTERVAL_MINUTES) * 60 * 1000;
|
||||||
|
return lastScheduledRunAt + intervalMs;
|
||||||
|
}
|
||||||
|
if (typeof settings?.persistedNextRunAt === 'number' && Number.isFinite(settings.persistedNextRunAt)) {
|
||||||
|
const now = Date.now();
|
||||||
|
if (settings.persistedNextRunAt > now) return settings.persistedNextRunAt;
|
||||||
|
return now + RESTART_RECOVERY_DELAY_MS;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
Reference in New Issue
Block a user