revert(network): remove network-outage auto-pause entirely — false positives froze batches
This commit is contained in:
parent
8e03212554
commit
6df00a0948
@ -43,46 +43,6 @@ class UploadManager extends EventEmitter {
|
|||||||
this._accountOverrides = new Map(); // hoster -> fallback account object
|
this._accountOverrides = new Map(); // hoster -> fallback account object
|
||||||
this._doodApiKeyCache = new Map(); // accountId/username -> derived doodstream API key ('' = tried, none)
|
this._doodApiKeyCache = new Map(); // accountId/username -> derived doodstream API key ('' = tried, none)
|
||||||
this._baselineCache = new Map(); // hoster:apiKey -> Promise<Set<file_code>> (one fetch shared across all jobs in batch)
|
this._baselineCache = new Map(); // hoster:apiKey -> Promise<Set<file_code>> (one fetch shared across all jobs in batch)
|
||||||
this._networkOnline = true;
|
|
||||||
this._networkWaiters = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
setNetworkOnline(online) {
|
|
||||||
const next = !!online;
|
|
||||||
if (next === this._networkOnline) return;
|
|
||||||
this._networkOnline = next;
|
|
||||||
if (next) {
|
|
||||||
const waiters = this._networkWaiters.splice(0);
|
|
||||||
for (const resolve of waiters) { try { resolve(); } catch {} }
|
|
||||||
this._rotLog('network-online', { releasedWaiters: waiters.length });
|
|
||||||
} else {
|
|
||||||
this._rotLog('network-offline', {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isNetworkOnline() {
|
|
||||||
return this._networkOnline;
|
|
||||||
}
|
|
||||||
|
|
||||||
_waitForNetwork(signal) {
|
|
||||||
if (this._networkOnline) return Promise.resolve();
|
|
||||||
if (signal && signal.aborted) return Promise.reject(new Error('Abgebrochen'));
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let settled = false;
|
|
||||||
const onResume = () => {
|
|
||||||
if (settled) return;
|
|
||||||
settled = true;
|
|
||||||
if (signal) signal.removeEventListener('abort', onAbort);
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
const onAbort = () => {
|
|
||||||
if (settled) return;
|
|
||||||
settled = true;
|
|
||||||
reject(new Error('Abgebrochen'));
|
|
||||||
};
|
|
||||||
this._networkWaiters.push(onResume);
|
|
||||||
if (signal) signal.addEventListener('abort', onAbort, { once: true });
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switchAccount(hoster, fallbackAccount) {
|
switchAccount(hoster, fallbackAccount) {
|
||||||
@ -545,12 +505,6 @@ class UploadManager extends EventEmitter {
|
|||||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||||
if (signal.aborted || this.stopAfterActive) break;
|
if (signal.aborted || this.stopAfterActive) break;
|
||||||
|
|
||||||
if (!this._networkOnline) {
|
|
||||||
this._rotLog('network-wait', { jobId, hoster: task.hoster, fileName, attempt });
|
|
||||||
await this._waitForNetwork(signal);
|
|
||||||
if (signal.aborted || this.stopAfterActive) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attempt > 1) {
|
if (attempt > 1) {
|
||||||
this._emitProgress(uploadId, fileName, task.hoster, { accountId: task.accountId,
|
this._emitProgress(uploadId, fileName, task.hoster, { accountId: task.accountId,
|
||||||
jobId,
|
jobId,
|
||||||
@ -719,18 +673,6 @@ class UploadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lastError = err;
|
lastError = err;
|
||||||
// Network outage in progress: the failure is the outage, not the
|
|
||||||
// file or the account. Hold the job until the gate reopens and
|
|
||||||
// retry WITHOUT consuming an attempt — a 30-minute ISP flake must
|
|
||||||
// not burn through the whole retry budget.
|
|
||||||
if (this._isTransientNetworkError(err) && !this._networkOnline) {
|
|
||||||
this._rotLog('network-hold', {
|
|
||||||
jobId, hoster: task.hoster, fileName, accountId: task.accountId, attempt
|
|
||||||
});
|
|
||||||
attempt--;
|
|
||||||
try { await this._waitForNetwork(signal); } catch { lastError = new Error('Abgebrochen'); break; }
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// File-specific rejection — re-uploading won't change the server's
|
// File-specific rejection — re-uploading won't change the server's
|
||||||
// mind. Break out immediately; the outer file-rejected branch then
|
// mind. Break out immediately; the outer file-rejected branch then
|
||||||
// records the final error without burning through 5 × 3s retries.
|
// records the final error without burning through 5 × 3s retries.
|
||||||
|
|||||||
55
main.js
55
main.js
@ -229,57 +229,6 @@ function rotLog(msg, ts) {
|
|||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const NET_CHECK_HOSTS = ['one.one.one.one', 'dns.google'];
|
|
||||||
let _netMonitorTimer = null;
|
|
||||||
let _netOnline = true;
|
|
||||||
let _netFails = 0;
|
|
||||||
let _netOks = 0;
|
|
||||||
let _netHostIdx = 0;
|
|
||||||
|
|
||||||
function _dnsProbe(host) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const timer = setTimeout(() => resolve(false), 5000);
|
|
||||||
try {
|
|
||||||
require('dns').resolve(host, (err) => { clearTimeout(timer); resolve(!err); });
|
|
||||||
} catch { clearTimeout(timer); resolve(false); }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function _netCheckTick() {
|
|
||||||
const host = NET_CHECK_HOSTS[_netHostIdx++ % NET_CHECK_HOSTS.length];
|
|
||||||
const ok = await _dnsProbe(host);
|
|
||||||
if (ok) { _netOks++; _netFails = 0; } else { _netFails++; _netOks = 0; }
|
|
||||||
if (_netOnline && _netFails >= 2) {
|
|
||||||
_netOnline = false;
|
|
||||||
debugLog('network-monitor: OFFLINE (2 consecutive DNS probe failures)');
|
|
||||||
rotLog('network-monitor: offline — holding job starts + retries');
|
|
||||||
if (uploadManager && typeof uploadManager.setNetworkOnline === 'function') uploadManager.setNetworkOnline(false);
|
|
||||||
safeSend('network-status', { online: false });
|
|
||||||
} else if (!_netOnline && _netOks >= 2) {
|
|
||||||
_netOnline = true;
|
|
||||||
debugLog('network-monitor: ONLINE again (2 consecutive DNS probe successes)');
|
|
||||||
rotLog('network-monitor: online — resuming held jobs');
|
|
||||||
if (uploadManager && typeof uploadManager.setNetworkOnline === 'function') uploadManager.setNetworkOnline(true);
|
|
||||||
safeSend('network-status', { online: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function startNetworkMonitor() {
|
|
||||||
if (_netMonitorTimer) return;
|
|
||||||
_netOnline = true; _netFails = 0; _netOks = 0;
|
|
||||||
_netMonitorTimer = setInterval(() => { _netCheckTick().catch(() => {}); }, 8000);
|
|
||||||
debugLog('network-monitor: started (8s probe interval)');
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopNetworkMonitor() {
|
|
||||||
if (_netMonitorTimer) { clearInterval(_netMonitorTimer); _netMonitorTimer = null; }
|
|
||||||
if (!_netOnline && uploadManager && typeof uploadManager.setNetworkOnline === 'function') {
|
|
||||||
uploadManager.setNetworkOnline(true);
|
|
||||||
}
|
|
||||||
_netOnline = true;
|
|
||||||
debugLog('network-monitor: stopped');
|
|
||||||
}
|
|
||||||
|
|
||||||
function _sleepMs(ms) { return new Promise((r) => setTimeout(r, ms)); }
|
function _sleepMs(ms) { return new Promise((r) => setTimeout(r, ms)); }
|
||||||
|
|
||||||
async function _postWebhookWithRetry(req, maxAttempts) {
|
async function _postWebhookWithRetry(req, maxAttempts) {
|
||||||
@ -1265,7 +1214,6 @@ app.on('window-all-closed', () => {
|
|||||||
|
|
||||||
app.on('before-quit', () => {
|
app.on('before-quit', () => {
|
||||||
if (uploadManager) try { uploadManager.cancel(); } catch {}
|
if (uploadManager) try { uploadManager.cancel(); } catch {}
|
||||||
try { stopNetworkMonitor(); } catch {}
|
|
||||||
try { folderMonitor.stop(); } catch {}
|
try { folderMonitor.stop(); } catch {}
|
||||||
try {
|
try {
|
||||||
if (remoteServer) { remoteServer.stop(); remoteServer = null; }
|
if (remoteServer) { remoteServer.stop(); remoteServer = null; }
|
||||||
@ -1733,7 +1681,6 @@ ipcMain.handle('start-upload', (_event, payload) => {
|
|||||||
debugLog(`batch-done: total=${summary.total} ok=${summary.succeeded} fail=${summary.failed}`);
|
debugLog(`batch-done: total=${summary.total} ok=${summary.succeeded} fail=${summary.failed}`);
|
||||||
logMarker('BATCH END', { total: summary.total, ok: summary.succeeded, fail: summary.failed });
|
logMarker('BATCH END', { total: summary.total, ok: summary.succeeded, fail: summary.failed });
|
||||||
logMemorySnapshot('batch-done');
|
logMemorySnapshot('batch-done');
|
||||||
stopNetworkMonitor();
|
|
||||||
const _batchDurationSec = _thisManager && _thisManager.startTime
|
const _batchDurationSec = _thisManager && _thisManager.startTime
|
||||||
? Math.round((Date.now() - _thisManager.startTime) / 1000)
|
? Math.round((Date.now() - _thisManager.startTime) / 1000)
|
||||||
: 0;
|
: 0;
|
||||||
@ -1763,13 +1710,11 @@ ipcMain.handle('start-upload', (_event, payload) => {
|
|||||||
process.nextTick(() => {
|
process.nextTick(() => {
|
||||||
if (!uploadManager) { debugLog('nextTick: uploadManager was nulled before startBatch'); return; }
|
if (!uploadManager) { debugLog('nextTick: uploadManager was nulled before startBatch'); return; }
|
||||||
debugLog(`nextTick: calling startBatch now (priming ${_sessionFailedAccounts.size} failed accounts, ${_sessionAccountOverrides.size} overrides from session)`);
|
debugLog(`nextTick: calling startBatch now (priming ${_sessionFailedAccounts.size} failed accounts, ${_sessionAccountOverrides.size} overrides from session)`);
|
||||||
startNetworkMonitor();
|
|
||||||
uploadManager.startBatch(tasks, {
|
uploadManager.startBatch(tasks, {
|
||||||
primeFailedAccounts: Array.from(_sessionFailedAccounts.keys()),
|
primeFailedAccounts: Array.from(_sessionFailedAccounts.keys()),
|
||||||
primeOverrides: Array.from(_sessionAccountOverrides.entries())
|
primeOverrides: Array.from(_sessionAccountOverrides.entries())
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
debugLog(`startBatch REJECTED: ${err && err.stack ? err.stack : err}`);
|
debugLog(`startBatch REJECTED: ${err && err.stack ? err.stack : err}`);
|
||||||
stopNetworkMonitor();
|
|
||||||
const errorSummary = {
|
const errorSummary = {
|
||||||
id: 'error',
|
id: 'error',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
|
|||||||
@ -120,9 +120,6 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
resetAllSessionFailedAccounts: () => ipcRenderer.invoke('reset-all-session-failed-accounts'),
|
resetAllSessionFailedAccounts: () => ipcRenderer.invoke('reset-all-session-failed-accounts'),
|
||||||
getLogPaths: () => ipcRenderer.invoke('get-log-paths'),
|
getLogPaths: () => ipcRenderer.invoke('get-log-paths'),
|
||||||
testWebhook: (payload) => ipcRenderer.invoke('test-webhook', payload),
|
testWebhook: (payload) => ipcRenderer.invoke('test-webhook', payload),
|
||||||
onNetworkStatus: (callback) => {
|
|
||||||
ipcRenderer.on('network-status', (_event, data) => callback(data));
|
|
||||||
},
|
|
||||||
revealLogFile: (target) => ipcRenderer.invoke('reveal-log-file', target),
|
revealLogFile: (target) => ipcRenderer.invoke('reveal-log-file', target),
|
||||||
setLogVerbose: (enabled) => ipcRenderer.invoke('set-log-verbose', enabled),
|
setLogVerbose: (enabled) => ipcRenderer.invoke('set-log-verbose', enabled),
|
||||||
createSupportBundle: () => ipcRenderer.invoke('create-support-bundle'),
|
createSupportBundle: () => ipcRenderer.invoke('create-support-bundle'),
|
||||||
|
|||||||
@ -140,18 +140,6 @@ async function init() {
|
|||||||
handleStats(data);
|
handleStats(data);
|
||||||
});
|
});
|
||||||
window.api.onShutdownCountdown(handleShutdownCountdown);
|
window.api.onShutdownCountdown(handleShutdownCountdown);
|
||||||
if (window.api.onNetworkStatus) {
|
|
||||||
window.api.onNetworkStatus((data) => {
|
|
||||||
if (!data || typeof data !== 'object') return;
|
|
||||||
_networkOffline = !data.online;
|
|
||||||
if (_networkOffline) {
|
|
||||||
showCopyToast('Netzwerk-Ausfall erkannt — Uploads pausiert bis die Verbindung zurück ist.', 10000);
|
|
||||||
} else {
|
|
||||||
showCopyToast('Netzwerk wieder verfügbar — Uploads werden fortgesetzt.', 6000);
|
|
||||||
}
|
|
||||||
updateStatusBar();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
window.api.onUploadLogFallback((data) => {
|
window.api.onUploadLogFallback((data) => {
|
||||||
const path = data && data.fallbackPath ? data.fallbackPath : '(Fallback)';
|
const path = data && data.fallbackPath ? data.fallbackPath : '(Fallback)';
|
||||||
showCopyToast(`Log-Pfad nicht beschreibbar — schreibe nach: ${path}`, 8000);
|
showCopyToast(`Log-Pfad nicht beschreibbar — schreibe nach: ${path}`, 8000);
|
||||||
@ -2173,7 +2161,6 @@ function handleBatchDone(summary) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let _sessionFailedKeys = new Set();
|
let _sessionFailedKeys = new Set();
|
||||||
let _networkOffline = false;
|
|
||||||
|
|
||||||
const _autoRetryState = { round: 0, timer: null };
|
const _autoRetryState = { round: 0, timer: null };
|
||||||
function _cancelAutoRetry(resetRound) {
|
function _cancelAutoRetry(resetRound) {
|
||||||
@ -2631,15 +2618,13 @@ function updateStatusBar() {
|
|||||||
? Math.round(stats.bytesRemaining / (lastUploadStats.globalSpeedKbs * 1024))
|
? Math.round(stats.bytesRemaining / (lastUploadStats.globalSpeedKbs * 1024))
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
const stateText = (_networkOffline && (uploading || lastUploadStats.state === 'uploading'))
|
const stateText = lastUploadStats.state === 'uploading'
|
||||||
? 'Netzwerk-Ausfall — pausiert'
|
? 'Upload läuft...'
|
||||||
: lastUploadStats.state === 'uploading'
|
: lastUploadStats.state === 'stopping'
|
||||||
? 'Upload läuft...'
|
? 'Stoppt nach aktiven Uploads...'
|
||||||
: lastUploadStats.state === 'stopping'
|
: uploading
|
||||||
? 'Stoppt nach aktiven Uploads...'
|
? 'Upload vorbereitet...'
|
||||||
: uploading
|
: 'Bereit';
|
||||||
? 'Upload vorbereitet...'
|
|
||||||
: 'Bereit';
|
|
||||||
|
|
||||||
document.getElementById('sbState').textContent = stateText;
|
document.getElementById('sbState').textContent = stateText;
|
||||||
document.getElementById('sbSpeed').textContent = formatSpeed(lastUploadStats.globalSpeedKbs || 0);
|
document.getElementById('sbSpeed').textContent = formatSpeed(lastUploadStats.globalSpeedKbs || 0);
|
||||||
|
|||||||
@ -46,49 +46,6 @@ describe('UploadManager', () => {
|
|||||||
UploadManager = require('../lib/upload-manager');
|
UploadManager = require('../lib/upload-manager');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('network gate: _waitForNetwork resolves immediately when online', async () => {
|
|
||||||
const mgr = new UploadManager({});
|
|
||||||
assert.strictEqual(mgr.isNetworkOnline(), true);
|
|
||||||
await mgr._waitForNetwork();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('network gate: waiters block while offline and release on setNetworkOnline(true)', async () => {
|
|
||||||
const mgr = new UploadManager({});
|
|
||||||
mgr.setNetworkOnline(false);
|
|
||||||
assert.strictEqual(mgr.isNetworkOnline(), false);
|
|
||||||
let resolved = false;
|
|
||||||
const waiter = mgr._waitForNetwork().then(() => { resolved = true; });
|
|
||||||
await new Promise(r => setTimeout(r, 30));
|
|
||||||
assert.strictEqual(resolved, false, 'must still be blocked while offline');
|
|
||||||
mgr.setNetworkOnline(true);
|
|
||||||
await waiter;
|
|
||||||
assert.strictEqual(resolved, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('network gate: abort signal rejects a pending waiter', async () => {
|
|
||||||
const mgr = new UploadManager({});
|
|
||||||
mgr.setNetworkOnline(false);
|
|
||||||
const ac = new AbortController();
|
|
||||||
const waiter = mgr._waitForNetwork(ac.signal);
|
|
||||||
ac.abort();
|
|
||||||
await assert.rejects(waiter, /Abgebrochen/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('network gate: batch with offline gate holds queued job until resume', async () => {
|
|
||||||
const mgr = new UploadManager({});
|
|
||||||
mgr.setNetworkOnline(false);
|
|
||||||
const statuses = [];
|
|
||||||
mgr.on('progress', (d) => statuses.push(d.status));
|
|
||||||
const batch = mgr.startBatch([
|
|
||||||
{ file: '/test/video1.mp4', hoster: 'doodstream.com', apiKey: 'key1' }
|
|
||||||
]);
|
|
||||||
await new Promise(r => setTimeout(r, 60));
|
|
||||||
assert.ok(!statuses.includes('done'), 'job must not complete while gate is closed');
|
|
||||||
mgr.setNetworkOnline(true);
|
|
||||||
await batch;
|
|
||||||
assert.ok(statuses.includes('done'), 'job completes after gate reopens');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('emits progress events for each task', async () => {
|
it('emits progress events for each task', async () => {
|
||||||
const mgr = new UploadManager({});
|
const mgr = new UploadManager({});
|
||||||
const events = [];
|
const events = [];
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user