From 1f6789b9cf7f68abd373c758536e76f23d895474 Mon Sep 17 00:00:00 2001 From: Eddy de Vink Date: Thu, 24 Sep 2026 22:44:10 +0200 Subject: [PATCH] =?UTF-8?q?feat(ack):=20activity-driven=20check=20loop=20+?= =?UTF-8?q?=20prune=20=E2=80=94=20geen=2030s-polling=20in=20rust?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 3 ++- src/ackTracker.js | 44 ++++++++++++++++++++++++++++++++++++++++---- src/index.js | 18 +++++++++++++++++- 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index 5eca407..21102fb 100644 --- a/.env.example +++ b/.env.example @@ -23,7 +23,8 @@ MAX_REQUESTS_PER_DAY=5 # Ack tracking (ghost-send detectie; vereist message_ack aangezet in wwebjs-api .env) ACK_STUCK_TIMEOUT_MS=300000 # 5 min zonder ack 2 = stuck -ACK_CHECK_INTERVAL_MS=30000 # checkinterval +ACK_PRUNED_AFTER_MS=1800000 # 30 min: stale trackings opruimen (loop stopt dan) +ACK_CHECK_INTERVAL_MS=30000 # checkinterval (alleen actief als er iets pending is) # Admin notifications ADMIN_NUMBER=31612345678@c.us diff --git a/src/ackTracker.js b/src/ackTracker.js index 0c0d372..d02bf55 100644 --- a/src/ackTracker.js +++ b/src/ackTracker.js @@ -6,17 +6,31 @@ // Findings (2026-09-24): an ack-1 hang is a bestand×ontvanger interaction, NOT // the session. The same file hung (ack 1) for one recipient yet delivered // (ack 3) to another in the same fresh session. A session restart does NOT fix -// an ack-1 hang (the same file kept hanging after restart) and needlessly -// disrupts production — so we deliberately do NOT auto-restart here. We only -// detect + notify + name the file, so it can be re-rendered and re-tested. +// an ack-1 hang and needlessly disrupts production — so we deliberately do NOT +// auto-restart here. We only detect + notify + name the file, so it can be +// re-rendered and re-tested. +// +// Activity-driven: the caller (index.js) is notified via the pending listener +// when the tracked set becomes non-empty, and starts a check loop only then. +// No polling when nothing is tracked. // // Requires the bridge (wwebjs-api) to emit message_ack: remove 'message_ack' // from DISABLED_CALLBACKS in the wwebjs-api .env and restart the container. const STUCK_TIMEOUT_MS = parseInt(process.env.ACK_STUCK_TIMEOUT_MS, 10) || 5 * 60 * 1000 +const PRUNE_AFTER_MS = parseInt(process.env.ACK_PRUNED_AFTER_MS, 10) || 30 * 60 * 1000 // messageId -> { chatId, filename, sentAt, ack, notifiedStuck } const sends = new Map() +let pendingListener = null + +function setPendingListener(fn) { + pendingListener = typeof fn === 'function' ? fn : null +} + +function notifyPendingChange() { + if (pendingListener) pendingListener(sends.size) +} function registerSend(messageId, chatId, filename) { if (!messageId) return @@ -28,6 +42,7 @@ function registerSend(messageId, chatId, filename) { notifiedStuck: false }) console.log(`[ack] track ${messageId} (${filename} → ${chatId})`) + notifyPendingChange() } function handleAck(messageId, ack) { @@ -38,6 +53,25 @@ function handleAck(messageId, ack) { if (ack >= 2) { sends.delete(messageId) // delivered; nothing left to track console.log(`[ack] delivered ack=${ack} ${messageId} (${entry.filename})`) + notifyPendingChange() + } +} + +// Remove stale entries (older than PRUNE_AFTER_MS). Stuck entries were notified +// once around STUCK_TIMEOUT_MS; keeping them longer only leaks memory and keeps +// the check-loop alive, so they are dropped here. +function prune() { + const now = Date.now() + let removed = 0 + for (const [id, entry] of sends) { + if (now - entry.sentAt > PRUNE_AFTER_MS) { + sends.delete(id) + removed++ + } + } + if (removed > 0) { + console.log(`[ack] pruned ${removed} stale entr${removed === 1 ? 'y' : 'ies'} (leeftijd > ${Math.round(PRUNE_AFTER_MS / 60000)} min)`) + notifyPendingChange() } } @@ -65,6 +99,8 @@ module.exports = { registerSend, handleAck, stuckEntries, + prune, markNotified, - pendingCount + pendingCount, + setPendingListener } \ No newline at end of file diff --git a/src/index.js b/src/index.js index bb3f395..162da5c 100644 --- a/src/index.js +++ b/src/index.js @@ -116,6 +116,7 @@ async function startServer() { } async function checkStuckAcks() { + ackTracker.prune() const stuck = ackTracker.stuckEntries() if (stuck.length === 0) return for (const s of stuck) { @@ -130,7 +131,22 @@ async function checkStuckAcks() { } } +// Lazy, activity-driven check loop: only runs while there is a tracked send +// pending, and stops as soon as the tracked set is empty. No 30s polling in +// idle. Re-arms itself after every tick while pendingCount() > 0. const ackCheckIntervalMs = parseInt(process.env.ACK_CHECK_INTERVAL_MS, 10) || 30000 -setInterval(checkStuckAcks, ackCheckIntervalMs) +let ackTimer = null + +function scheduleAckCheck() { + if (ackTimer) return + if (ackTracker.pendingCount() === 0) return + ackTimer = setTimeout(() => { + ackTimer = null + checkStuckAcks() + if (ackTracker.pendingCount() > 0) scheduleAckCheck() + }, ackCheckIntervalMs) +} + +ackTracker.setPendingListener(() => scheduleAckCheck()) startServer() \ No newline at end of file