diff --git a/.commitmessage b/.commitmessage index 55c0478..a99fb6e 100644 --- a/.commitmessage +++ b/.commitmessage @@ -1,3 +1,3 @@ -docs: document git and docker workflow +fix: retry transient pickup checks -Record the requirement to start from a clean worktree, preserve the running Docker state as production reference, commit and push every completed change, and deploy features and fixes live via Docker. +Retry transient Foodsharing network failures such as socket hang up for safe read/check actions and add explicit pickup fallback scheduling, tick, and result logs so missed fallback checks can be diagnosed. diff --git a/services/pickupScheduler.js b/services/pickupScheduler.js index 8b3eb5d..e907059 100644 --- a/services/pickupScheduler.js +++ b/services/pickupScheduler.js @@ -1196,6 +1196,13 @@ function scheduleRegularPickupRefresh(settings) { function scheduleFallbackPickupChecks(sessionId, settings) { const cronExpression = settings.pickupFallbackCron || DEFAULT_SETTINGS.pickupFallbackCron; if (!cronExpression) { + console.log(`[PICKUP] Fallback-Check für Session ${sessionId} nicht geplant: kein Cron-Ausdruck konfiguriert.`); + return null; + } + if (!cron.validate(cronExpression)) { + console.warn( + `[PICKUP] Fallback-Check für Session ${sessionId} nicht geplant: ungültiger Cron-Ausdruck "${cronExpression}".` + ); return null; } const job = cron.schedule( @@ -1203,19 +1210,32 @@ function scheduleFallbackPickupChecks(sessionId, settings) { () => { const session = sessionStore.get(sessionId); if (!session?.profile?.id) { + console.warn(`[PICKUP] Fallback-Check übersprungen: Session ${sessionId} nicht gefunden.`); return; } const delay = randomDelayMs(settings.randomDelayMinSeconds, settings.randomDelayMaxSeconds); + console.log( + `[PICKUP] Fallback-Check ausgelöst für Session ${sessionId} ` + + `(Profil ${session.profile.id}, Delay ${delay}ms, Cron: ${cronExpression}).` + ); setTimeout(() => { const config = readConfig(session.profile.id); - runImmediatePickupCheck(sessionId, config, settings).catch((error) => { - console.error('[PICKUP] Fallback-Check fehlgeschlagen:', error.message); - }); + runImmediatePickupCheck(sessionId, config, settings) + .then((result) => { + console.log( + `[PICKUP] Fallback-Check abgeschlossen für Session ${sessionId}: ` + + `${result.checked} aktive Einträge geprüft.` + ); + }) + .catch((error) => { + console.error('[PICKUP] Fallback-Check fehlgeschlagen:', error.message); + }); }, delay); }, { timezone: TIME_ZONE } ); sessionStore.attachJob(sessionId, job); + console.log(`[PICKUP] Fallback-Check geplant für Session ${sessionId} (Cron: ${cronExpression}).`); return job; } @@ -1311,8 +1331,13 @@ async function runImmediatePickupCheck(sessionId, config, settings) { const entries = Array.isArray(config) ? config : []; const activeEntries = entries.filter((entry) => entry?.active); if (activeEntries.length === 0) { + console.log(`[PICKUP] Sofortprüfung für Session ${sessionId}: keine aktiven Einträge.`); return { checked: 0 }; } + console.log( + `[PICKUP] Sofortprüfung für Session ${sessionId}: ${activeEntries.length} aktive Einträge ` + + `(${activeEntries.map((entry) => entry.id).join(', ')}).` + ); for (const entry of activeEntries) { await checkEntry(sessionId, entry, resolvedSettings); } diff --git a/services/sessionRefresh.js b/services/sessionRefresh.js index 603e1c0..ac646fc 100644 --- a/services/sessionRefresh.js +++ b/services/sessionRefresh.js @@ -3,8 +3,14 @@ const sessionStore = require('./sessionStore'); const notificationService = require('./notificationService'); const ADMIN_SESSION_ERROR_COOLDOWN_MS = 6 * 60 * 60 * 1000; +const DEFAULT_TRANSIENT_RETRY_DELAYS_MS = [1000, 3000]; +const NON_RETRIABLE_TRANSIENT_LABELS = new Set(['bookSlot']); const adminSessionErrorCooldowns = new Map(); +function wait(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + function shouldNotifyAdminSessionError(key) { if (!key) { return false; @@ -51,6 +57,26 @@ function isCsrfError(error) { return message.toLowerCase().includes('csrf'); } +function isTransientNetworkError(error) { + if (!error) { + return false; + } + if (error.response) { + return false; + } + const code = String(error.code || '').toUpperCase(); + if (['ECONNRESET', 'ECONNABORTED', 'ETIMEDOUT', 'EPIPE', 'ENOTFOUND', 'EAI_AGAIN'].includes(code)) { + return true; + } + const message = String(error.message || '').toLowerCase(); + return ( + message.includes('socket hang up') || + message.includes('timeout') || + message.includes('network error') || + message.includes('connection reset') + ); +} + async function refreshSession(session, { label } = {}) { if (!session?.credentials?.email || !session?.credentials?.password) { console.warn( @@ -110,7 +136,7 @@ async function ensureSession(session) { return true; } -async function withSessionRetry(session, action, { label } = {}) { +async function withSessionRetry(session, action, { label, transientRetries } = {}) { if (!session) { throw new Error('Session fehlt'); } @@ -120,22 +146,42 @@ async function withSessionRetry(session, action, { label } = {}) { throw new Error('Session konnte nicht erneuert werden'); } } - try { - return await action(); - } catch (error) { - if (!isUnauthorizedError(error) && !isCsrfError(error)) { - throw error; + const retryDelays = NON_RETRIABLE_TRANSIENT_LABELS.has(label) + ? [] + : DEFAULT_TRANSIENT_RETRY_DELAYS_MS.slice( + 0, + Number.isFinite(transientRetries) ? Math.max(0, transientRetries) : DEFAULT_TRANSIENT_RETRY_DELAYS_MS.length + ); + let transientAttempt = 0; + while (true) { + try { + return await action(); + } catch (error) { + if (isTransientNetworkError(error) && transientAttempt < retryDelays.length) { + transientAttempt += 1; + const delay = retryDelays[transientAttempt - 1]; + console.warn( + `[SESSION] Transienter Fehler${label ? ` (${label})` : ''}: ${error.message}. ` + + `Retry ${transientAttempt}/${retryDelays.length} in ${delay}ms.` + ); + await wait(delay); + continue; + } + if (!isUnauthorizedError(error) && !isCsrfError(error)) { + throw error; + } + const refreshed = await refreshSession(session, { label }); + if (!refreshed) { + throw error; + } + return action(); } - const refreshed = await refreshSession(session, { label }); - if (!refreshed) { - throw error; - } - return action(); } } module.exports = { ensureSession, + isTransientNetworkError, refreshSession, withSessionRetry };