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 tracking (ghost-send detectie; vereist message_ack aangezet in wwebjs-api .env)
|
||||||
ACK_STUCK_TIMEOUT_MS=300000 # 5 min zonder ack 2 = stuck
|
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 notifications
|
||||||
ADMIN_NUMBER=31612345678@c.us
|
ADMIN_NUMBER=31612345678@c.us
|
||||||
|
|
|
||||||
|
|
@ -6,17 +6,31 @@
|
||||||
// Findings (2026-09-24): an ack-1 hang is a bestand×ontvanger interaction, NOT
|
// 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
|
// 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
|
// (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
|
// an ack-1 hang and needlessly disrupts production — so we deliberately do NOT
|
||||||
// disrupts production — so we deliberately do NOT auto-restart here. We only
|
// auto-restart here. We only detect + notify + name the file, so it can be
|
||||||
// detect + notify + name the file, so it can be re-rendered and re-tested.
|
// 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'
|
// Requires the bridge (wwebjs-api) to emit message_ack: remove 'message_ack'
|
||||||
// from DISABLED_CALLBACKS in the wwebjs-api .env and restart the container.
|
// 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 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 }
|
// messageId -> { chatId, filename, sentAt, ack, notifiedStuck }
|
||||||
const sends = new Map()
|
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) {
|
function registerSend(messageId, chatId, filename) {
|
||||||
if (!messageId) return
|
if (!messageId) return
|
||||||
|
|
@ -28,6 +42,7 @@ function registerSend(messageId, chatId, filename) {
|
||||||
notifiedStuck: false
|
notifiedStuck: false
|
||||||
})
|
})
|
||||||
console.log(`[ack] track ${messageId} (${filename} → ${chatId})`)
|
console.log(`[ack] track ${messageId} (${filename} → ${chatId})`)
|
||||||
|
notifyPendingChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAck(messageId, ack) {
|
function handleAck(messageId, ack) {
|
||||||
|
|
@ -38,6 +53,25 @@ function handleAck(messageId, ack) {
|
||||||
if (ack >= 2) {
|
if (ack >= 2) {
|
||||||
sends.delete(messageId) // delivered; nothing left to track
|
sends.delete(messageId) // delivered; nothing left to track
|
||||||
console.log(`[ack] delivered ack=${ack} ${messageId} (${entry.filename})`)
|
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,
|
registerSend,
|
||||||
handleAck,
|
handleAck,
|
||||||
stuckEntries,
|
stuckEntries,
|
||||||
|
prune,
|
||||||
markNotified,
|
markNotified,
|
||||||
pendingCount
|
pendingCount,
|
||||||
|
setPendingListener
|
||||||
}
|
}
|
||||||
18
src/index.js
18
src/index.js
|
|
@ -116,6 +116,7 @@ async function startServer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkStuckAcks() {
|
async function checkStuckAcks() {
|
||||||
|
ackTracker.prune()
|
||||||
const stuck = ackTracker.stuckEntries()
|
const stuck = ackTracker.stuckEntries()
|
||||||
if (stuck.length === 0) return
|
if (stuck.length === 0) return
|
||||||
for (const s of stuck) {
|
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
|
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()
|
startServer()
|
||||||
Loading…
Reference in a new issue