railway-worker: set auto-log 30m and cleanup retention 10 days
This commit is contained in:
parent
6c3b2f3112
commit
b361463b9d
|
|
@ -6,11 +6,11 @@ Background worker service untuk sistem otomasi IoT ApsGo. Service ini berjalan 2
|
|||
|
||||
- ✅ **Waktu Mode**: Penjadwalan berdasarkan waktu (cron-based)
|
||||
- ✅ **Sensor Mode**: Otomasi berdasarkan threshold kelembapan tanah
|
||||
- ✅ **Auto History Logging**: Record data sensor setiap 10 menit
|
||||
- ✅ **Auto History Logging**: Record data sensor setiap 30 menit
|
||||
- ✅ **Redis Queue**: Prevent race conditions dan manage concurrent tasks
|
||||
- ✅ **Graceful Shutdown**: Clean shutdown dengan safety turn-off semua aktuator
|
||||
- ✅ **Health Monitoring**: Auto health check setiap 5 menit
|
||||
- ✅ **Auto Cleanup**: Hapus history lama otomatis (retain 30 hari)
|
||||
- ✅ **Auto Cleanup**: Hapus history lama otomatis (retain 10 hari)
|
||||
|
||||
## Tech Stack
|
||||
|
||||
|
|
@ -124,7 +124,7 @@ await queue.obliterate();
|
|||
|
||||
### Database Cleanup
|
||||
|
||||
History otomatis di-cleanup setiap hari jam 2 pagi, hanya retain 30 hari terakhir.
|
||||
History otomatis di-cleanup setiap hari jam 2 pagi, hanya retain 10 hari terakhir.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
|
|
|||
|
|
@ -65,6 +65,8 @@ const config = {
|
|||
concurrency: 1, // Process 1 job at a time (prevent race condition)
|
||||
checkInterval: 60000, // Check jadwal setiap 60 detik (reduced from 30s)
|
||||
sensorDebounce: 120000, // 2 menit minimum antar penyiraman per pot
|
||||
scheduleGraceMs: parseInt(process.env.SCHEDULE_GRACE_MS || '15000', 10), // Toleransi keterlambatan trigger
|
||||
scheduleMaxCatchupMs: parseInt(process.env.SCHEDULE_MAX_CATCHUP_MS || '300000', 10), // Maksimal catch-up 5 menit
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -332,6 +334,7 @@ wateringWorker.on('failed', (job, err) => {
|
|||
// ==================== WAKTU MODE (TIME SCHEDULER) ====================
|
||||
|
||||
let lastScheduleCheck = {};
|
||||
let lastSchedulerTickAt = null;
|
||||
|
||||
// Counter untuk tracking berapa kali check dilakukan
|
||||
let checkCounter = 0;
|
||||
|
|
@ -620,6 +623,36 @@ async function readFirebaseSmart(path) {
|
|||
}
|
||||
}
|
||||
|
||||
// Parse HH:mm menjadi Date hari ini (timezone proses mengikuti process.env.TZ)
|
||||
function parseScheduleTimeToday(scheduleTime, now) {
|
||||
if (typeof scheduleTime !== 'string' || !/^\d{2}:\d{2}$/.test(scheduleTime)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [hourStr, minuteStr] = scheduleTime.split(':');
|
||||
const hour = Number(hourStr);
|
||||
const minute = Number(minuteStr);
|
||||
|
||||
if (Number.isNaN(hour) || Number.isNaN(minute) || hour < 0 || hour > 23 || minute < 0 || minute > 59) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const scheduledAt = new Date(now);
|
||||
scheduledAt.setHours(hour, minute, 0, 0);
|
||||
return scheduledAt;
|
||||
}
|
||||
|
||||
function isScheduleInTriggerWindow(scheduleTime, now, windowStartMs, windowEndMs) {
|
||||
const scheduleDate = parseScheduleTimeToday(scheduleTime, now);
|
||||
if (!scheduleDate) {
|
||||
return { match: false, scheduleMs: null };
|
||||
}
|
||||
|
||||
const scheduleMs = scheduleDate.getTime();
|
||||
const match = scheduleMs >= windowStartMs && scheduleMs <= windowEndMs;
|
||||
return { match, scheduleMs };
|
||||
}
|
||||
|
||||
async function checkScheduledWatering() {
|
||||
checkCounter++;
|
||||
console.log(`\n🔎 [DEBUG] checkScheduledWatering() called - Counter: ${checkCounter}`);
|
||||
|
|
@ -637,10 +670,20 @@ async function checkScheduledWatering() {
|
|||
}
|
||||
|
||||
const now = new Date();
|
||||
const checkEndMs = now.getTime();
|
||||
const currentTime = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
|
||||
const currentSeconds = now.getSeconds();
|
||||
const dateKey = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')}`;
|
||||
|
||||
// Trigger window untuk mencegah jadwal terlewat karena delay polling/API.
|
||||
const graceMs = config.worker.scheduleGraceMs;
|
||||
const maxCatchupMs = config.worker.scheduleMaxCatchupMs;
|
||||
const fallbackStartMs = checkEndMs - config.worker.checkInterval;
|
||||
const baselineStartMs = lastSchedulerTickAt || fallbackStartMs;
|
||||
const boundedStartMs = Math.max(baselineStartMs, checkEndMs - maxCatchupMs);
|
||||
const triggerWindowStartMs = boundedStartMs - graceMs;
|
||||
const triggerWindowEndMs = checkEndMs;
|
||||
|
||||
// 🔍 VERBOSE LOG: Log setiap check untuk memastikan fungsi berjalan
|
||||
console.log(`\n⏱️ CHECK #${checkCounter}: ${currentTime}:${currentSeconds.toString().padStart(2, '0')} | Mode: ${kontrolConfig?.waktu ? '✅' : '❌'}`);
|
||||
|
||||
|
|
@ -651,6 +694,7 @@ async function checkScheduledWatering() {
|
|||
if (checkCounter % 3 === 0 || now.getMinutes() % 5 === 0) {
|
||||
console.log(` 📅 Date: ${dateKey}`);
|
||||
console.log(` 🕐 Current: ${currentTime} (${now.toLocaleString('id-ID', {timeZone: 'Asia/Jakarta'})})`);
|
||||
console.log(` 🪟 Trigger window: ${new Date(triggerWindowStartMs).toLocaleTimeString('id-ID')} - ${new Date(triggerWindowEndMs).toLocaleTimeString('id-ID')}`);
|
||||
console.log(` Mode Waktu: ${kontrolConfig?.waktu ? '✅ ENABLED' : '❌ DISABLED'}`);
|
||||
console.log(` 📊 API Stats: SDK=${sdkSuccessCount} | REST=${restFallbackCount} | Errors=${consecutiveFirebaseErrors}`);
|
||||
console.log(` 📋 Total Jadwal: ${allSchedules.length}`);
|
||||
|
|
@ -699,8 +743,22 @@ async function checkScheduledWatering() {
|
|||
|
||||
// Check if time matches
|
||||
const scheduleWaktu = schedule.waktu;
|
||||
if (!scheduleWaktu || scheduleWaktu !== currentTime) {
|
||||
continue; // Not time yet
|
||||
const { match: isInWindow, scheduleMs } = isScheduleInTriggerWindow(
|
||||
scheduleWaktu,
|
||||
now,
|
||||
triggerWindowStartMs,
|
||||
triggerWindowEndMs
|
||||
);
|
||||
|
||||
if (!isInWindow) {
|
||||
continue; // Belum/terlalu lama lewat untuk window ini
|
||||
}
|
||||
|
||||
if (scheduleMs !== null) {
|
||||
const delayedSec = Math.max(0, Math.floor((triggerWindowEndMs - scheduleMs) / 1000));
|
||||
if (delayedSec > 0) {
|
||||
console.log(` ⏱️ ${scheduleKey}: Triggered with ${delayedSec}s delay (within tolerance)`);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract schedule config
|
||||
|
|
@ -716,7 +774,8 @@ async function checkScheduledWatering() {
|
|||
}
|
||||
|
||||
// Create unique job key
|
||||
const jobKey = `${scheduleKey}_${dateKey}_${currentTime.replace(':', '_')}`;
|
||||
const normalizedScheduleTime = scheduleWaktu.replace(':', '_');
|
||||
const jobKey = `${scheduleKey}_${dateKey}_${normalizedScheduleTime}`;
|
||||
|
||||
if (!lastScheduleCheck[jobKey]) {
|
||||
console.log(`\n🕐 ${scheduleKey.toUpperCase()} TRIGGERED: ${currentTime}`);
|
||||
|
|
@ -757,8 +816,16 @@ async function checkScheduledWatering() {
|
|||
}
|
||||
|
||||
// LEGACY SUPPORT: Check old format (waktu_1, waktu_2) untuk backward compatibility
|
||||
if (kontrolConfig.waktu_1 && kontrolConfig.waktu_1 === currentTime) {
|
||||
const scheduleKey = `legacy_jadwal_1_${dateKey}_${currentTime.replace(':', '_')}`;
|
||||
const legacy1Window = isScheduleInTriggerWindow(
|
||||
kontrolConfig.waktu_1,
|
||||
now,
|
||||
triggerWindowStartMs,
|
||||
triggerWindowEndMs
|
||||
);
|
||||
|
||||
if (legacy1Window.match) {
|
||||
const legacyTimeKey = kontrolConfig.waktu_1.replace(':', '_');
|
||||
const scheduleKey = `legacy_jadwal_1_${dateKey}_${legacyTimeKey}`;
|
||||
|
||||
if (!lastScheduleCheck[scheduleKey]) {
|
||||
console.log(`\n🕐 [LEGACY] JADWAL 1 TRIGGERED: ${currentTime}`);
|
||||
|
|
@ -789,8 +856,16 @@ async function checkScheduledWatering() {
|
|||
}
|
||||
}
|
||||
|
||||
if (kontrolConfig.waktu_2 && kontrolConfig.waktu_2 === currentTime) {
|
||||
const scheduleKey = `legacy_jadwal_2_${dateKey}_${currentTime.replace(':', '_')}`;
|
||||
const legacy2Window = isScheduleInTriggerWindow(
|
||||
kontrolConfig.waktu_2,
|
||||
now,
|
||||
triggerWindowStartMs,
|
||||
triggerWindowEndMs
|
||||
);
|
||||
|
||||
if (legacy2Window.match) {
|
||||
const legacyTimeKey = kontrolConfig.waktu_2.replace(':', '_');
|
||||
const scheduleKey = `legacy_jadwal_2_${dateKey}_${legacyTimeKey}`;
|
||||
|
||||
if (!lastScheduleCheck[scheduleKey]) {
|
||||
console.log(`\n🕑 [LEGACY] JADWAL 2 TRIGGERED: ${currentTime}`);
|
||||
|
|
@ -822,7 +897,6 @@ async function checkScheduledWatering() {
|
|||
}
|
||||
|
||||
// Cleanup old schedule checks (> 2 menit)
|
||||
const twoMinutesAgo = Date.now() - 120000;
|
||||
for (const key in lastScheduleCheck) {
|
||||
if (key.includes(dateKey)) continue; // Keep today's
|
||||
delete lastScheduleCheck[key];
|
||||
|
|
@ -841,6 +915,8 @@ async function checkScheduledWatering() {
|
|||
}
|
||||
|
||||
// Continue running - don't crash worker
|
||||
} finally {
|
||||
lastSchedulerTickAt = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1097,8 +1173,8 @@ async function logHistory(type, potNumbers, duration) {
|
|||
|
||||
// ==================== PERIODIC HISTORY LOGGING ====================
|
||||
|
||||
// Auto-log sensor data setiap 10 menit (independent from watering)
|
||||
const autoLogJob = new cron.CronJob('*/10 * * * *', async () => {
|
||||
// Auto-log sensor data setiap 30 menit (independent from watering)
|
||||
const autoLogJob = new cron.CronJob('*/30 * * * *', async () => {
|
||||
try {
|
||||
const sensorData = await readFirebaseSmart('data');
|
||||
|
||||
|
|
@ -1121,7 +1197,7 @@ const autoLogJob = new cron.CronJob('*/10 * * * *', async () => {
|
|||
});
|
||||
|
||||
autoLogJob.start();
|
||||
console.log('✅ Auto history logging started (every 10 minutes)');
|
||||
console.log('✅ Auto history logging started (every 30 minutes)');
|
||||
|
||||
// ==================== CLEANUP OLD HISTORY (DAILY) ====================
|
||||
|
||||
|
|
@ -1129,7 +1205,7 @@ const cleanupJob = new cron.CronJob('0 2 * * *', async () => {
|
|||
// Run daily at 2 AM
|
||||
try {
|
||||
console.log('\n🧹 Running history cleanup...');
|
||||
const daysToKeep = 30;
|
||||
const daysToKeep = 10;
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
|
||||
|
||||
|
|
@ -1361,7 +1437,7 @@ console.log('\n✨ ApsGo Railway Worker is running!');
|
|||
console.log('📊 Features enabled:');
|
||||
console.log(' • Waktu Mode (Time-based scheduling)');
|
||||
console.log(' • Sensor Mode (Threshold-based automation)');
|
||||
console.log(' • Auto History Logging (every 10 min)');
|
||||
console.log(' • Auto History Logging (every 30 min)');
|
||||
console.log(' • History Cleanup (daily at 2 AM)');
|
||||
console.log(' • Health Check (every 5 min)');
|
||||
console.log('\n🎯 Worker is ready to process jobs...\n');
|
||||
|
|
|
|||
Loading…
Reference in New Issue