feat(ack): activity-driven check loop + prune — geen 30s-polling in rust
This commit is contained in:
parent
a6cfcfc219
commit
1f6789b9cf
3 changed files with 59 additions and 6 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
18
src/index.js
18
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()
|
||||
Loading…
Reference in a new issue