Harden batch-wide recovery identity claims

Scope remote code ownership to normalized hoster and account identities while retaining title-only recovery serialization and canonical Unicode title matching.

Mark post-upload ambiguity and duplicate identities as uncertain so retries, account fallback, and later same-title jobs fail closed instead of reporting unsafe success.

Acquire recovery title leases before hoster and global semaphores, revalidate failed-account overrides before upload, and clear claim state at batch boundaries.

Add deterministic concurrent coverage for same-code rejection, distinct-code parallel success, uncertainty propagation, semaphore fairness, account isolation, Unicode equivalence, and registry lifetime.
This commit is contained in:
Sucukdeluxe
2026-08-13 23:09:03 +02:00
parent dd14381e43
commit c78160a521
7 changed files with 640 additions and 141 deletions
+43
View File
@@ -187,3 +187,46 @@ test('Vidmoly concurrent same-name recovery accepts distinct remote codes', asyn
assert.deepEqual(results.map(result => result.file_code), ['VIDFIRST0001', 'VIDSECOND001']);
});
for (const scenario of [
{
label: 'VOE',
hoster: 'voe.sx',
Uploader: VoeUploader,
sharedCode: 'VOE_UNCERTAIN',
lateCode: 'VOE_LATE_CODE',
build(uploader, code) {
return uploader._buildUrls(code);
}
},
{
label: 'Vidmoly',
hoster: 'vidmoly.me',
Uploader: VidmolyUploader,
sharedCode: 'VIDUNCERTAIN',
lateCode: 'VIDLATECODE1',
build(uploader, code) {
return uploader._buildUrlsFromCode(code);
}
}
]) {
test(`${scenario.label} marks a duplicate direct identity uncertain and blocks a later title match`, async () => {
const registry = createRecoveryClaimRegistry();
const firstClaim = registry.forUpload(scenario.hoster, 'ACCOUNT', 'First Episode.mkv');
const uncertainClaim = registry.forUpload(scenario.hoster, 'ACCOUNT', 'Second Episode.mkv');
const first = new scenario.Uploader(firstClaim);
const uncertain = new scenario.Uploader(uncertainClaim);
scenario.build(first, scenario.sharedCode);
assert.throws(
() => scenario.build(uncertain, scenario.sharedCode),
err => err.remoteIdentityClaimed === true && err.remoteCommitUncertain === true
);
const laterClaim = registry.forUpload(scenario.hoster, 'ACCOUNT', 'Second Episode.mp4');
await assert.rejects(
() => laterClaim.runExclusive(async () => scenario.build(new scenario.Uploader(laterClaim), scenario.lateCode)),
err => err.remoteCommitUncertain === true
);
});
}
+72 -1
View File
@@ -1,7 +1,7 @@
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const { __test } = require('../lib/hosters');
const { __test, createRecoveryClaimRegistry } = require('../lib/hosters');
describe('hosters helpers', () => {
it('extracts VOE file_code from nested result payloads', () => {
@@ -94,3 +94,74 @@ describe('hosters helpers', () => {
assert.equal(r.embed_url, 'https://byse.sx/e/GOOD123');
});
});
describe('recovery claim registry', () => {
it('claims remote codes across every title of one normalized hoster account', () => {
const registry = createRecoveryClaimRegistry();
const first = registry.forUpload(' VOE.SX ', 'ACCOUNT', 'First Episode.mkv');
const differentTitle = registry.forUpload('voe.sx', 'ACCOUNT', 'Second Episode.mp4');
const differentAccount = registry.forUpload('voe.sx', 'ACCOUNT-B', 'Second Episode.mp4');
assert.equal(first.reserve('REMOTE-CODE'), true);
assert.equal(differentTitle.reserve('REMOTE-CODE'), false);
assert.equal(differentAccount.reserve('REMOTE-CODE'), true);
});
it('serializes canonically equivalent titles without blocking an independent title', async () => {
const registry = createRecoveryClaimRegistry();
const composed = registry.forUpload('voe.sx', 'ACCOUNT', 'Café.mkv');
const decomposed = registry.forUpload('voe.sx', 'ACCOUNT', 'Cafe\u0301.mp4');
const independent = registry.forUpload('voe.sx', 'ACCOUNT', 'Other Episode.mkv');
const events = [];
let releaseFirst;
const firstGate = new Promise(resolve => {
releaseFirst = resolve;
});
assert.equal(composed.reserve('UNICODE-CODE'), true);
assert.equal(decomposed.reserve('UNICODE-CODE'), false);
const first = composed.runExclusive(async () => {
events.push('first-started');
await firstGate;
events.push('first-finished');
});
await new Promise(resolve => setImmediate(resolve));
const equivalent = decomposed.runExclusive(async () => {
events.push('equivalent-started');
});
const other = independent.runExclusive(async () => {
events.push('independent-started');
});
await new Promise(resolve => setImmediate(resolve));
assert.deepEqual(events, ['first-started', 'independent-started']);
releaseFirst();
await Promise.all([first, equivalent, other]);
assert.deepEqual(events, ['first-started', 'independent-started', 'first-finished', 'equivalent-started']);
});
it('fails closed for later jobs after a title becomes uncertain', async () => {
const registry = createRecoveryClaimRegistry();
const first = registry.forUpload('vidmoly.me', 'ACCOUNT', 'Shared Episode.mkv');
const later = registry.forUpload('vidmoly.me', 'ACCOUNT', 'shared-episode.mp4');
const error = first.markUncertain(new Error('Remote commit could not be confirmed'));
assert.equal(error.remoteCommitUncertain, true);
assert.equal(error.hosterTransient, true);
await assert.rejects(
() => later.runExclusive(async () => 'unsafe-success'),
err => err.remoteCommitUncertain === true && err.hosterTransient === true
);
});
it('drops every claim when the registry is cleared', () => {
const registry = createRecoveryClaimRegistry();
const first = registry.forUpload('voe.sx', 'ACCOUNT', 'Episode.mkv');
assert.equal(first.reserve('REMOTE-CODE'), true);
registry.clear();
const nextBatch = registry.forUpload('voe.sx', 'ACCOUNT', 'Episode.mkv');
assert.equal(nextBatch.reserve('REMOTE-CODE'), true);
});
});
+174 -2
View File
@@ -56,12 +56,12 @@ function settings(hoster, parallelCount) {
};
}
async function runBatch(manager, tasks) {
async function runBatch(manager, tasks, options) {
let summary;
manager.once('batch-done', value => {
summary = value;
});
await manager.startBatch(tasks);
await manager.startBatch(tasks, options);
return summary;
}
@@ -88,6 +88,56 @@ function waitFor(promise, timeoutMs, message) {
]).finally(() => clearTimeout(timer));
}
async function assertTitleWaiterLeavesSlotAvailable(hosterParallel, globalSettings = {}) {
let releaseFirst;
let markFirstStarted;
let markIndependentStarted;
const firstGate = new Promise(resolve => {
releaseFirst = resolve;
});
const firstStarted = new Promise(resolve => {
markFirstStarted = resolve;
});
const independentStarted = new Promise(resolve => {
markIndependentStarted = resolve;
});
let sequence = 0;
loadManager(async (hoster, file) => {
if (file === firstPath) {
markFirstStarted();
await firstGate;
}
if (file === distinctPath) markIndependentStarted();
sequence++;
return {
file_code: `ADMISSION_${sequence}`,
download_url: `https://byse.sx/d/ADMISSION_${sequence}`
};
});
const manager = new UploadManager(settings('byse.sx', hosterParallel), globalSettings);
const batch = runBatch(manager, [
{ jobId: 'admission-first', file: firstPath, hoster: 'byse.sx', apiKey: 'ACCOUNT_KEY' }
]);
await waitFor(firstStarted, 500, 'First upload did not start');
const added = manager.addJobs([
{ jobId: 'admission-waiter', file: secondPath, hoster: 'byse.sx', apiKey: 'ACCOUNT_KEY' },
{ jobId: 'admission-independent', file: distinctPath, hoster: 'byse.sx', apiKey: 'ACCOUNT_KEY' }
]);
assert.equal(added.added, 2);
let admissionError = null;
try {
await waitFor(independentStarted, 500, 'Independent title was blocked behind a title-lock waiter');
} catch (err) {
admissionError = err;
} finally {
releaseFirst();
}
const summary = await batch;
if (admissionError) throw admissionError;
assert.equal(summary.succeeded, 3);
}
test('a batch shares recovery claims across normalized same-name jobs', async () => {
let unsafeCalls = 0;
loadManager(async (hoster, file, apiKey, onProgress, signal, throttle, options) => {
@@ -172,6 +222,93 @@ test('normalized same-name recovery sections never overlap', async () => {
assert.equal(maximumActive, 1);
});
test('a title-lock waiter does not consume a scarce upload slot', async () => {
await assertTitleWaiterLeavesSlotAvailable(2);
});
test('a title-lock waiter does not consume a scarce global upload slot', async () => {
await assertTitleWaiterLeavesSlotAvailable(3, { parallelUploadCount: 2 });
});
test('an uncertain remote commit blocks retries, account fallback, and later same-title success', async () => {
const calls = [];
let markFirstStarted;
let releaseUncertain;
const firstStarted = new Promise(resolve => {
markFirstStarted = resolve;
});
const uncertainGate = new Promise(resolve => {
releaseUncertain = resolve;
});
loadManager(async (hoster, file, apiKey) => {
calls.push({ file, apiKey });
if (file === firstPath) {
markFirstStarted();
await uncertainGate;
const error = new Error('Remote commit could not be confirmed');
error.remoteCommitUncertain = true;
throw error;
}
return {
file_code: 'LATE_REMOTE_CODE',
download_url: 'https://byse.sx/d/LATE_REMOTE_CODE'
};
});
const hosterSettings = settings('byse.sx', 2);
hosterSettings['byse.sx'].retries = 2;
const manager = new UploadManager(hosterSettings);
const fallback = { id: 'ACCOUNT_B', apiKey: 'ACCOUNT_KEY_B' };
const batch = runBatch(manager, [
{
jobId: 'uncertain-first',
file: firstPath,
hoster: 'byse.sx',
accountId: 'ACCOUNT_A',
apiKey: 'ACCOUNT_KEY_A'
}
], { primeOverrides: [['byse.sx', fallback]] });
await waitFor(firstStarted, 500, 'Uncertain predecessor did not start');
const added = manager.addJobs([
{
jobId: 'uncertain-later',
file: secondPath,
hoster: 'byse.sx',
accountId: 'ACCOUNT_A',
apiKey: 'ACCOUNT_KEY_A'
}
]);
assert.equal(added.added, 1);
releaseUncertain();
const summary = await batch;
assert.equal(summary.succeeded, 0);
assert.equal(summary.failed, 2);
assert.deepEqual(calls, [{ file: firstPath, apiKey: 'ACCOUNT_KEY_A' }]);
});
test('recovery claims do not leak into a later batch on the same manager', async () => {
loadManager(async (hoster, file, apiKey, onProgress, signal, throttle, options) => {
if (!options.recoveryClaim.reserve('REUSED_BATCH_CODE')) {
const error = new Error('Remote recovery candidate already claimed');
error.hosterTransient = true;
throw error;
}
return {
file_code: 'REUSED_BATCH_CODE',
download_url: 'https://byse.sx/d/REUSED_BATCH_CODE'
};
});
const manager = new UploadManager(settings('byse.sx', 1));
const task = { jobId: 'batch-one', file: firstPath, hoster: 'byse.sx', apiKey: 'ACCOUNT_KEY' };
const first = await runBatch(manager, [task]);
const second = await runBatch(manager, [{ ...task, jobId: 'batch-two' }]);
assert.equal(first.succeeded, 1);
assert.equal(second.succeeded, 1);
});
for (const scenario of [
{
label: 'VOE',
@@ -263,6 +400,41 @@ for (const scenario of [
);
});
test(`${scenario.label} uploader instances reject one direct remote code across different titles`, async () => {
await withUploaderMethods(
scenario.Uploader,
async function () {
await new Promise(resolve => setImmediate(resolve));
return scenario.buildResult(this, scenario.sharedCode);
},
async () => {
loadManager();
const manager = new UploadManager(settings(scenario.hoster, 2));
const summary = await runBatch(manager, [
{
jobId: `${scenario.label}-different-title-a`,
file: firstPath,
hoster: scenario.hoster,
accountId: 'LOGIN_ACCOUNT',
username: 'account@example.test',
password: 'password'
},
{
jobId: `${scenario.label}-different-title-b`,
file: distinctPath,
hoster: scenario.hoster,
accountId: 'LOGIN_ACCOUNT',
username: 'account@example.test',
password: 'password'
}
]);
assert.equal(summary.succeeded, 1);
assert.equal(summary.failed, 1);
}
);
});
test(`${scenario.label} uploader instances preserve parallel success for distinct remote identities`, async () => {
let active = 0;
let maximumActive = 0;