fix: retry transient pickup checks
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.
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -1196,6 +1196,13 @@ function scheduleRegularPickupRefresh(settings) {
|
|||||||
function scheduleFallbackPickupChecks(sessionId, settings) {
|
function scheduleFallbackPickupChecks(sessionId, settings) {
|
||||||
const cronExpression = settings.pickupFallbackCron || DEFAULT_SETTINGS.pickupFallbackCron;
|
const cronExpression = settings.pickupFallbackCron || DEFAULT_SETTINGS.pickupFallbackCron;
|
||||||
if (!cronExpression) {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
const job = cron.schedule(
|
const job = cron.schedule(
|
||||||
@@ -1203,19 +1210,32 @@ function scheduleFallbackPickupChecks(sessionId, settings) {
|
|||||||
() => {
|
() => {
|
||||||
const session = sessionStore.get(sessionId);
|
const session = sessionStore.get(sessionId);
|
||||||
if (!session?.profile?.id) {
|
if (!session?.profile?.id) {
|
||||||
|
console.warn(`[PICKUP] Fallback-Check übersprungen: Session ${sessionId} nicht gefunden.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const delay = randomDelayMs(settings.randomDelayMinSeconds, settings.randomDelayMaxSeconds);
|
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(() => {
|
setTimeout(() => {
|
||||||
const config = readConfig(session.profile.id);
|
const config = readConfig(session.profile.id);
|
||||||
runImmediatePickupCheck(sessionId, config, settings).catch((error) => {
|
runImmediatePickupCheck(sessionId, config, settings)
|
||||||
console.error('[PICKUP] Fallback-Check fehlgeschlagen:', error.message);
|
.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);
|
}, delay);
|
||||||
},
|
},
|
||||||
{ timezone: TIME_ZONE }
|
{ timezone: TIME_ZONE }
|
||||||
);
|
);
|
||||||
sessionStore.attachJob(sessionId, job);
|
sessionStore.attachJob(sessionId, job);
|
||||||
|
console.log(`[PICKUP] Fallback-Check geplant für Session ${sessionId} (Cron: ${cronExpression}).`);
|
||||||
return job;
|
return job;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1311,8 +1331,13 @@ async function runImmediatePickupCheck(sessionId, config, settings) {
|
|||||||
const entries = Array.isArray(config) ? config : [];
|
const entries = Array.isArray(config) ? config : [];
|
||||||
const activeEntries = entries.filter((entry) => entry?.active);
|
const activeEntries = entries.filter((entry) => entry?.active);
|
||||||
if (activeEntries.length === 0) {
|
if (activeEntries.length === 0) {
|
||||||
|
console.log(`[PICKUP] Sofortprüfung für Session ${sessionId}: keine aktiven Einträge.`);
|
||||||
return { checked: 0 };
|
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) {
|
for (const entry of activeEntries) {
|
||||||
await checkEntry(sessionId, entry, resolvedSettings);
|
await checkEntry(sessionId, entry, resolvedSettings);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,14 @@ const sessionStore = require('./sessionStore');
|
|||||||
const notificationService = require('./notificationService');
|
const notificationService = require('./notificationService');
|
||||||
|
|
||||||
const ADMIN_SESSION_ERROR_COOLDOWN_MS = 6 * 60 * 60 * 1000;
|
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();
|
const adminSessionErrorCooldowns = new Map();
|
||||||
|
|
||||||
|
function wait(ms) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
function shouldNotifyAdminSessionError(key) {
|
function shouldNotifyAdminSessionError(key) {
|
||||||
if (!key) {
|
if (!key) {
|
||||||
return false;
|
return false;
|
||||||
@@ -51,6 +57,26 @@ function isCsrfError(error) {
|
|||||||
return message.toLowerCase().includes('csrf');
|
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 } = {}) {
|
async function refreshSession(session, { label } = {}) {
|
||||||
if (!session?.credentials?.email || !session?.credentials?.password) {
|
if (!session?.credentials?.email || !session?.credentials?.password) {
|
||||||
console.warn(
|
console.warn(
|
||||||
@@ -110,7 +136,7 @@ async function ensureSession(session) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function withSessionRetry(session, action, { label } = {}) {
|
async function withSessionRetry(session, action, { label, transientRetries } = {}) {
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw new Error('Session fehlt');
|
throw new Error('Session fehlt');
|
||||||
}
|
}
|
||||||
@@ -120,22 +146,42 @@ async function withSessionRetry(session, action, { label } = {}) {
|
|||||||
throw new Error('Session konnte nicht erneuert werden');
|
throw new Error('Session konnte nicht erneuert werden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
const retryDelays = NON_RETRIABLE_TRANSIENT_LABELS.has(label)
|
||||||
return await action();
|
? []
|
||||||
} catch (error) {
|
: DEFAULT_TRANSIENT_RETRY_DELAYS_MS.slice(
|
||||||
if (!isUnauthorizedError(error) && !isCsrfError(error)) {
|
0,
|
||||||
throw error;
|
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 = {
|
module.exports = {
|
||||||
ensureSession,
|
ensureSession,
|
||||||
|
isTransientNetworkError,
|
||||||
refreshSession,
|
refreshSession,
|
||||||
withSessionRetry
|
withSessionRetry
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user