Harden source cleanup durability barriers
Separate provisional upload success from persistently confirmed cleanup state, invalidate prior confirmations before retries, and promote completion only inside the final queue persistence handshake. Preserve source files when history or queue persistence fails and add regression coverage for restart, retry, rollback, and real Electron finalization paths.
This commit is contained in:
+43
-23
@@ -83,30 +83,29 @@ function createSourceFileCleanup(options) {
|
||||
|
||||
function createManifest(input, canonicalFile) {
|
||||
const requiredHosters = uniqueStrings(input.requiredHosters);
|
||||
const completedHosters = new Set(uniqueStrings(input.completedHosters));
|
||||
const confirmedHosters = new Set(
|
||||
uniqueStrings(input.confirmedHosters).filter((hoster) => requiredHosters.includes(hoster))
|
||||
);
|
||||
const jobs = new Map();
|
||||
|
||||
for (const job of Array.isArray(input.jobs) ? input.jobs : []) {
|
||||
if (!job || typeof job.jobId !== 'string' || typeof job.hoster !== 'string') continue;
|
||||
const status = completedHosters.has(job.hoster) ? 'done' : normalizeStatus(job.status);
|
||||
const currentRound = job.currentRound !== false;
|
||||
const status = currentRound ? 'pending' : normalizeStatus(job.status);
|
||||
jobs.set(job.jobId, Object.freeze({
|
||||
jobId: job.jobId,
|
||||
hoster: job.hoster,
|
||||
status
|
||||
status,
|
||||
currentRound
|
||||
}));
|
||||
}
|
||||
|
||||
for (const hoster of completedHosters) {
|
||||
if (requiredHosters.includes(hoster)) continue;
|
||||
completedHosters.delete(hoster);
|
||||
}
|
||||
|
||||
return {
|
||||
token: input.token || input.sourceCleanupToken,
|
||||
file: path.resolve(input.file),
|
||||
canonicalFile,
|
||||
requiredHosters: Object.freeze(requiredHosters),
|
||||
completedHosters,
|
||||
confirmedHosters,
|
||||
jobs,
|
||||
suppliedFingerprint: isFingerprint(input.fingerprint) ? cloneFingerprint(input.fingerprint) : null,
|
||||
fingerprint: null,
|
||||
@@ -152,16 +151,32 @@ function createSourceFileCleanup(options) {
|
||||
...existing.requiredHosters,
|
||||
...input.requiredHosters
|
||||
]));
|
||||
for (const hoster of uniqueStrings(input.completedHosters)) {
|
||||
if (existing.requiredHosters.includes(hoster)) existing.completedHosters.add(hoster);
|
||||
const incomingJobs = Array.isArray(input.jobs) ? input.jobs : [];
|
||||
const currentRoundHosters = new Set(
|
||||
incomingJobs
|
||||
.filter((job) => job && typeof job.hoster === 'string' && job.currentRound !== false)
|
||||
.map((job) => job.hoster)
|
||||
);
|
||||
for (const hoster of currentRoundHosters) existing.confirmedHosters.delete(hoster);
|
||||
for (const hoster of uniqueStrings(input.confirmedHosters)) {
|
||||
if (existing.requiredHosters.includes(hoster) && !currentRoundHosters.has(hoster)) {
|
||||
existing.confirmedHosters.add(hoster);
|
||||
}
|
||||
}
|
||||
for (const job of Array.isArray(input.jobs) ? input.jobs : []) {
|
||||
for (const job of incomingJobs) {
|
||||
if (!job || typeof job.jobId !== 'string' || typeof job.hoster !== 'string') continue;
|
||||
const previous = existing.jobs.get(job.jobId);
|
||||
const status = existing.completedHosters.has(job.hoster)
|
||||
? 'done'
|
||||
const incomingCurrentRound = job.currentRound !== false;
|
||||
const currentRound = Boolean((previous && previous.currentRound) || incomingCurrentRound);
|
||||
const status = incomingCurrentRound
|
||||
? 'pending'
|
||||
: normalizeStatus(previous ? previous.status : job.status);
|
||||
existing.jobs.set(job.jobId, Object.freeze({ jobId: job.jobId, hoster: job.hoster, status }));
|
||||
existing.jobs.set(job.jobId, Object.freeze({
|
||||
jobId: job.jobId,
|
||||
hoster: job.hoster,
|
||||
status,
|
||||
currentRound
|
||||
}));
|
||||
}
|
||||
fingerprints[token] = cloneFingerprint(existing.fingerprint);
|
||||
continue;
|
||||
@@ -194,10 +209,9 @@ function createSourceFileCleanup(options) {
|
||||
const manifest = groups.get(token);
|
||||
if (!manifest || manifest.finalizationPromise) return false;
|
||||
const job = manifest.jobs.get(event.jobId);
|
||||
if (!job || job.hoster !== event.hoster) return false;
|
||||
if (!job || !job.currentRound || job.hoster !== event.hoster) return false;
|
||||
if (typeof event.file === 'string' && canonicalize(event.file) !== manifest.canonicalFile) return false;
|
||||
manifest.jobs.set(event.jobId, Object.freeze({ ...job, status: event.status }));
|
||||
if (event.status === 'done') manifest.completedHosters.add(event.hoster);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -206,7 +220,7 @@ function createSourceFileCleanup(options) {
|
||||
for (const manifest of groups.values()) {
|
||||
if (manifest.finalizationPromise) continue;
|
||||
const job = manifest.jobs.get(jobId);
|
||||
if (!job) continue;
|
||||
if (!job || !job.currentRound) continue;
|
||||
manifest.jobs.set(jobId, Object.freeze({ ...job, status: 'skipped' }));
|
||||
changed = true;
|
||||
}
|
||||
@@ -216,11 +230,17 @@ function createSourceFileCleanup(options) {
|
||||
function blockingStatuses(manifest) {
|
||||
const blocking = [];
|
||||
for (const hoster of manifest.requiredHosters) {
|
||||
if (manifest.completedHosters.has(hoster)) continue;
|
||||
const statuses = [...manifest.jobs.values()]
|
||||
.filter((job) => job.hoster === hoster)
|
||||
.map((job) => job.status);
|
||||
if (statuses.includes('done')) continue;
|
||||
const jobs = [...manifest.jobs.values()].filter((job) => job.hoster === hoster);
|
||||
const currentJobs = jobs.filter((job) => job.currentRound);
|
||||
if (currentJobs.length > 0) {
|
||||
const currentBlocker = currentJobs.find((job) => job.status !== 'done');
|
||||
if (!currentBlocker) continue;
|
||||
const status = currentJobs.map((job) => job.status).find((value) => value !== 'pending') || 'pending';
|
||||
blocking.push({ hoster, status });
|
||||
continue;
|
||||
}
|
||||
if (manifest.confirmedHosters.has(hoster)) continue;
|
||||
const statuses = jobs.map((job) => job.status).filter((status) => status !== 'done');
|
||||
const status = statuses.find((value) => value !== 'pending') || 'pending';
|
||||
blocking.push({ hoster, status });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user