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:
parent
214893205b
commit
ee9b67a78e
7 changed files with 33 additions and 264 deletions
|
|
@ -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
|
||||
}
|
||||
37
src/index.js
37
src/index.js
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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}`)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue