railway-worker: set auto-log 30m and cleanup retention 10 days

This commit is contained in:
Wizznu 2026-03-24 23:05:20 +07:00
parent 6c3b2f3112
commit b361463b9d
2 changed files with 92 additions and 16 deletions

View File

@ -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

View File

@ -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');