revert: verwijder ackTracker (dwaalspoor) — bridge fabriceert acks

ackTracker + message_ack-verwerking + check-loop + ACK_* env + docs uit de bot.
De bridge fabriceert acks (5219 blz 2 kreeg ack=2 zonder transmissie), dus
ack-tracking gaf verkeerde signalen. Enige betrouwbare bron: apparaat + getInfo.
Media-pacing zit nu bridge-breed (MEDIA_SEND_DELAY_MS). Poppler re-render als
fix verwijderd uit docs (wassen neus voor de race-klasse).
This commit is contained in:
Eddy de Vink 2026-09-26 22:55:28 +02:00
commit ee9b67a78e
7 changed files with 33 additions and 264 deletions

View file

@ -1,106 +0,0 @@
// ackTracker.js
// Tracks delivery acks (message_ack) of outgoing media to detect sends that
// never reach the recipient: a message returns success + a message ID but
// stays below ack 2 (one grey tick).
//
// 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 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
sends.set(messageId, {
chatId: chatId || '',
filename: filename || '',
sentAt: Date.now(),
ack: 0,
notifiedStuck: false
})
console.log(`[ack] track ${messageId} (${filename} → ${chatId})`)
notifyPendingChange()
}
function handleAck(messageId, ack) {
if (!messageId) return
const entry = sends.get(messageId)
if (!entry) return
if (ack > entry.ack) entry.ack = 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()
}
}
function stuckEntries() {
const now = Date.now()
const stuck = []
for (const [id, entry] of sends) {
if (entry.ack < 2 && now - entry.sentAt > STUCK_TIMEOUT_MS) {
stuck.push({ id, ...entry })
}
}
return stuck
}
function markNotified(messageId) {
const entry = sends.get(messageId)
if (entry) entry.notifiedStuck = true
}
function pendingCount() {
return sends.size
}
module.exports = {
registerSend,
handleAck,
stuckEntries,
prune,
markNotified,
pendingCount,
setPendingListener
}

View file

@ -6,8 +6,7 @@ const axios = require('axios')
const { handleWebhook } = require('./messageHandler')
const { loadDiensten } = require('./dienstService')
const { resetDay, getYesterdayTotal } = require('./dailyStats')
const { sendTextMessage, notifyAdmin } = require('./whatsappApi')
const ackTracker = require('./ackTracker')
const { sendTextMessage } = require('./whatsappApi')
async function notifyTelegramOnline() {
const url = process.env.TELEGRAM_BOT_URL
@ -103,38 +102,4 @@ async function startServer() {
process.on('SIGINT', () => shutdown('SIGINT'))
}
async function checkStuckAcks() {
ackTracker.prune()
const stuck = ackTracker.stuckEntries()
if (stuck.length === 0) return
for (const s of stuck) {
if (!s.notifiedStuck) {
ackTracker.markNotified(s.id)
notifyAdmin(
`[dienst-bot] hangend bericht: ${s.filename} → ${s.chatId} ` +
`(ack ${s.ack}, ${Math.round((Date.now() - s.sentAt) / 1000)}s oud). ` +
`Bestand opnieuw renderen + bezorgtest, geen sessie-herstart (die helpt hier niet).`
)
}
}
}
// 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
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()

View file

@ -2,7 +2,6 @@ const { findDienst, getPngFilePaths } = require('./dienstService')
const { sendTextMessage, sendImageMessage, notifyAdmin, sendChatSeen } = require('./whatsappApi')
const { checkLimit, increment, MAX_REQUESTS_PER_DAY } = require('./rateLimiter')
const { increment: incrementDaily } = require('./dailyStats')
const { handleAck } = require('./ackTracker')
const ALLOWED_GROUP = process.env.ALLOWED_GROUP
const ALLOWED_GROUP_NAMES = (process.env.ALLOWED_GROUP_NAME || '')
@ -24,13 +23,6 @@ function extractDienstnummer(body) {
}
async function handleWebhook(payload) {
if (payload.dataType === 'message_ack') {
if (payload.sessionId && payload.sessionId !== process.env.WHATSAPP_SESSION) return
const { message, ack } = payload.data || {}
const id = message && (message.id?.id || message._data?.id?.id)
if (id) handleAck(id, ack)
return
}
if (payload.dataType !== 'message') return
if (payload.sessionId !== process.env.WHATSAPP_SESSION) return

View file

@ -1,6 +1,5 @@
const axios = require('axios')
const fs = require('fs')
const { registerSend } = require('./ackTracker')
const API_BASE = process.env.WHATSAPP_API_ENDPOINT
const SESSION = process.env.WHATSAPP_SESSION
@ -69,8 +68,6 @@ async function sendImageMessage(chatId, filePath, filename) {
filename: filename
}
})
const messageId = response.data?.message?._data?.id?.id
registerSend(messageId, chatId, filename)
return response.data
}, `sendImageMessage ${filename} to ${chatId}`)
}