amalgm 0.1.241 → 0.1.243
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const test = require('node:test');
|
|
4
4
|
const assert = require('node:assert/strict');
|
|
5
|
+
const crypto = require('crypto');
|
|
5
6
|
const fs = require('fs');
|
|
6
7
|
const os = require('os');
|
|
7
8
|
const path = require('path');
|
|
@@ -268,8 +269,8 @@ test('failed uploads yield the bulk lane to later durable objects', () => {
|
|
|
268
269
|
assert.notEqual(second.sha256, first.sha256);
|
|
269
270
|
const terminal = failUpload(second.resourceId, second.sha256, {
|
|
270
271
|
retryable: false,
|
|
271
|
-
code: '
|
|
272
|
-
message: '
|
|
272
|
+
code: 'invalid_repo_object',
|
|
273
|
+
message: 'this exact object is invalid',
|
|
273
274
|
});
|
|
274
275
|
assert.equal(terminal.changed, true);
|
|
275
276
|
assert.equal(terminal.terminal, true);
|
|
@@ -284,3 +285,61 @@ test('failed uploads yield the bulk lane to later durable objects', () => {
|
|
|
284
285
|
acknowledgeUpload(third.resourceId, third.sha256);
|
|
285
286
|
assert.equal(claimUpload(), null);
|
|
286
287
|
});
|
|
288
|
+
|
|
289
|
+
test('legacy shared-resource uploads can terminate without starving current projects', () => {
|
|
290
|
+
const legacyResourceId = 'shr_legacyobjectqueue0001';
|
|
291
|
+
const legacyChunks = [
|
|
292
|
+
Buffer.from('first stale object chunk from a legacy rail'),
|
|
293
|
+
Buffer.from('second stale object chunk from a legacy rail'),
|
|
294
|
+
].map((bytes) => ({
|
|
295
|
+
bytes,
|
|
296
|
+
sha256: crypto.createHash('sha256').update(bytes).digest('hex'),
|
|
297
|
+
}));
|
|
298
|
+
const objectRoot = path.join(process.env.AMALGM_DIR, 'state', 'repo-objects');
|
|
299
|
+
const manifests = path.join(objectRoot, 'manifests');
|
|
300
|
+
const chunks = path.join(objectRoot, 'chunks');
|
|
301
|
+
for (const chunk of legacyChunks) {
|
|
302
|
+
fs.writeFileSync(path.join(chunks, `${chunk.sha256}.bin`), chunk.bytes);
|
|
303
|
+
}
|
|
304
|
+
fs.writeFileSync(path.join(manifests, '000-legacy-shared-resource.json'), JSON.stringify({
|
|
305
|
+
contract: 'git-bundle-chunks@2',
|
|
306
|
+
resourceId: legacyResourceId,
|
|
307
|
+
repoKey: 'legacy-repo',
|
|
308
|
+
oid: '1'.repeat(40),
|
|
309
|
+
ref: `refs/amalgm/exports/${'1'.repeat(40)}`,
|
|
310
|
+
checksum: locatorChecksum(legacyChunks.map((chunk) => ({
|
|
311
|
+
sha256: chunk.sha256,
|
|
312
|
+
bytes: chunk.bytes.length,
|
|
313
|
+
}))),
|
|
314
|
+
bytes: legacyChunks.reduce((total, chunk) => total + chunk.bytes.length, 0),
|
|
315
|
+
chunks: legacyChunks.map((chunk) => ({
|
|
316
|
+
sha256: chunk.sha256,
|
|
317
|
+
bytes: chunk.bytes.length,
|
|
318
|
+
uploaded: false,
|
|
319
|
+
})),
|
|
320
|
+
createdAt: new Date().toISOString(),
|
|
321
|
+
}));
|
|
322
|
+
|
|
323
|
+
assert.equal(
|
|
324
|
+
publishPayload('current-after-legacy', Buffer.from('current project object'), REPO_KEY).kind,
|
|
325
|
+
'pending',
|
|
326
|
+
);
|
|
327
|
+
const legacy = claimUpload();
|
|
328
|
+
assert.equal(legacy.resourceId, legacyResourceId);
|
|
329
|
+
assert.equal(legacy.sha256, legacyChunks[0].sha256);
|
|
330
|
+
const terminal = failUpload(legacy.resourceId, legacy.sha256, {
|
|
331
|
+
retryable: false,
|
|
332
|
+
code: 'shared_resource_read_forbidden',
|
|
333
|
+
message: 'the legacy rail is no longer readable',
|
|
334
|
+
});
|
|
335
|
+
assert.equal(terminal.changed, true);
|
|
336
|
+
assert.equal(terminal.terminal, true);
|
|
337
|
+
|
|
338
|
+
const current = claimUpload();
|
|
339
|
+
assert.equal(current.resourceId, PROJECT);
|
|
340
|
+
assert.ok(
|
|
341
|
+
legacyChunks.every((chunk) => current.sha256 !== chunk.sha256),
|
|
342
|
+
'a resource-wide rejection skips every remaining chunk from the stale rail',
|
|
343
|
+
);
|
|
344
|
+
acknowledgeUpload(current.resourceId, current.sha256);
|
|
345
|
+
});
|
|
@@ -13,6 +13,7 @@ const crypto = require('crypto');
|
|
|
13
13
|
const fs = require('fs');
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const { AMALGM_DIR } = require('../config');
|
|
16
|
+
const { isSharedResourceId } = require('../state/cloud-resource-id');
|
|
16
17
|
const { appendStateEvent } = require('../state/events');
|
|
17
18
|
const durable = require('./durable');
|
|
18
19
|
const transition = require('./transition');
|
|
@@ -33,6 +34,12 @@ const REQUESTS = path.join(ROOT, 'requests');
|
|
|
33
34
|
const preparing = new Map();
|
|
34
35
|
const UPLOAD_RETRY_BASE_MS = 1_000;
|
|
35
36
|
const UPLOAD_RETRY_MAX_MS = 60_000;
|
|
37
|
+
const RESOURCE_WIDE_UPLOAD_FAILURES = new Set([
|
|
38
|
+
'invalid_repo_project',
|
|
39
|
+
'private_resource_forbidden',
|
|
40
|
+
'shared_resource_read_forbidden',
|
|
41
|
+
'shared_resource_write_forbidden',
|
|
42
|
+
]);
|
|
36
43
|
|
|
37
44
|
function isProjectId(value) {
|
|
38
45
|
return require('./tree/identity').isUuid(String(value || ''));
|
|
@@ -288,12 +295,20 @@ function acknowledgeUpload(resourceId, digest) {
|
|
|
288
295
|
}
|
|
289
296
|
|
|
290
297
|
function failUpload(resourceId, digest, failure = {}) {
|
|
291
|
-
|
|
298
|
+
// Upload work is durable across protocol generations. Match the exact
|
|
299
|
+
// identity that was accepted when the work was queued, including legacy
|
|
300
|
+
// `shr_` rails, so stale historical work can reach a terminal state instead
|
|
301
|
+
// of monopolizing every newer code project behind it.
|
|
302
|
+
if (!isSharedResourceId(resourceId) || !/^[0-9a-f]{64}$/.test(String(digest || ''))) {
|
|
292
303
|
return { changed: false, terminal: false };
|
|
293
304
|
}
|
|
294
305
|
ensureDirs();
|
|
295
306
|
const failedAt = new Date().toISOString();
|
|
296
307
|
const retryable = failure?.retryable !== false;
|
|
308
|
+
const failureCode = typeof failure?.code === 'string'
|
|
309
|
+
? failure.code
|
|
310
|
+
: 'repo_object_upload_failed';
|
|
311
|
+
const resourceWide = RESOURCE_WIDE_UPLOAD_FAILURES.has(failureCode);
|
|
297
312
|
let changed = false;
|
|
298
313
|
let terminal = false;
|
|
299
314
|
let retryAfter = null;
|
|
@@ -303,30 +318,38 @@ function failUpload(resourceId, digest, failure = {}) {
|
|
|
303
318
|
const file = path.join(directory, name);
|
|
304
319
|
const manifest = readJson(file);
|
|
305
320
|
if (manifest?.resourceId !== resourceId || !manifest?.chunks) continue;
|
|
306
|
-
const
|
|
307
|
-
entry.
|
|
321
|
+
const failedChunks = manifest.chunks.filter((entry) => (
|
|
322
|
+
!entry.uploaded && (resourceWide || entry.sha256 === digest)
|
|
308
323
|
));
|
|
309
|
-
if (
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
)
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
324
|
+
if (failedChunks.length < 1) continue;
|
|
325
|
+
for (const chunk of failedChunks) {
|
|
326
|
+
const chunkAttempts = Number(chunk.uploadAttempts || 0) + 1;
|
|
327
|
+
attempts = Math.max(attempts, chunkAttempts);
|
|
328
|
+
chunk.uploadAttempts = chunkAttempts;
|
|
329
|
+
chunk.uploadLastError = {
|
|
330
|
+
code: failureCode,
|
|
331
|
+
message: typeof failure?.message === 'string'
|
|
332
|
+
? failure.message
|
|
333
|
+
: 'repo object upload failed',
|
|
334
|
+
retryable,
|
|
335
|
+
at: failedAt,
|
|
336
|
+
};
|
|
337
|
+
if (retryable) {
|
|
338
|
+
const delayMs = Math.min(
|
|
339
|
+
UPLOAD_RETRY_MAX_MS,
|
|
340
|
+
UPLOAD_RETRY_BASE_MS * (2 ** Math.min(chunkAttempts - 1, 16)),
|
|
341
|
+
);
|
|
342
|
+
const chunkRetryAfter = new Date(Date.now() + delayMs).toISOString();
|
|
343
|
+
retryAfter = retryAfter && Date.parse(retryAfter) > Date.parse(chunkRetryAfter)
|
|
344
|
+
? retryAfter
|
|
345
|
+
: chunkRetryAfter;
|
|
346
|
+
chunk.uploadRetryAfter = chunkRetryAfter;
|
|
347
|
+
delete chunk.uploadTerminal;
|
|
348
|
+
} else {
|
|
349
|
+
chunk.uploadTerminal = { ...chunk.uploadLastError };
|
|
350
|
+
delete chunk.uploadRetryAfter;
|
|
351
|
+
terminal = true;
|
|
352
|
+
}
|
|
330
353
|
}
|
|
331
354
|
writeJson(file, manifest);
|
|
332
355
|
changed = true;
|