From ecc51205e3f8478aa0d4c060613df2ced29ad98c Mon Sep 17 00:00:00 2001 From: Thomas Carmet <8408330+tcarmet@users.noreply.github.com> Date: Tue, 23 Jun 2026 15:55:30 -0700 Subject: [PATCH] CLDSRV-938: poll for MPU cleanup in abort race tests The "Abort MPU - Race Conditions" functional tests asserted that no MPU metadata remained using a single immediate ListMultipartUploads read after racing Complete/Abort operations. That cleanup is eventually consistent, so the read could observe a transient state and fail with "1 !== 0". Poll ListMultipartUploads until the uploadId disappears (bounded 10s timeout), preserving the original assertion on timeout so a genuine orphan-MPU leak is still detected. --- .../aws-node-sdk/test/object/abortMPU.js | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/tests/functional/aws-node-sdk/test/object/abortMPU.js b/tests/functional/aws-node-sdk/test/object/abortMPU.js index 7c3a836548..71e04cf37a 100644 --- a/tests/functional/aws-node-sdk/test/object/abortMPU.js +++ b/tests/functional/aws-node-sdk/test/object/abortMPU.js @@ -56,6 +56,26 @@ async function cleanupVersionedBucket(bucketUtil, bucketName) { await bucketUtil.deleteOne(bucketName); } +// Poll ListMultipartUploads until the given uploadId no longer appears. +// After a racing Complete/Abort, MPU metadata cleanup can be eventually +// consistent, so a single immediate read may still observe the upload and +// fail spuriously (CLDSRV-938). On timeout this returns the still-present +// uploads so the caller's assertion fails with the original message, which +// preserves detection of a genuine orphan-MPU leak. +async function waitForMpuCleanup(s3, bucketName, uploadId, { timeoutMs = 10000, intervalMs = 250 } = {}) { + const deadline = Date.now() + timeoutMs; + let remainingUploads = []; + do { + const listResult = await s3.send(new ListMultipartUploadsCommand({ Bucket: bucketName })); + remainingUploads = (listResult.Uploads || []).filter(upload => upload.UploadId === uploadId); + if (remainingUploads.length === 0) { + return remainingUploads; + } + await scheduler.wait(intervalMs); + } while (Date.now() < deadline); + return remainingUploads; +} + describe('Abort MPU', () => { withV4(sigCfg => { let bucketUtil; @@ -811,9 +831,10 @@ describe('Abort MPU - Race Conditions', function testSuite() { // If both operations encountered errors, that's also acceptable // as long as the system remains consistent - // Verify no MPU metadata remains - const listResult = await s3.send(new ListMultipartUploadsCommand({ Bucket: bucketName })); - const remainingUploads = (listResult.Uploads || []).filter(upload => upload.UploadId === uploadId); + // Verify no MPU metadata remains. Cleanup after a racing + // Complete/Abort can be eventually consistent, so poll rather than + // reading once (CLDSRV-938). + const remainingUploads = await waitForMpuCleanup(s3, bucketName, uploadId); assert.strictEqual(remainingUploads.length, 0, 'No MPU metadata should remain'); }); @@ -880,9 +901,10 @@ describe('Abort MPU - Race Conditions', function testSuite() { } } - // Verify no MPU metadata remains - const listResult = await s3.send(new ListMultipartUploadsCommand({ Bucket: bucketName })); - const remainingUploads = (listResult.Uploads || []).filter(upload => upload.UploadId === uploadId); + // Verify no MPU metadata remains. Cleanup after concurrent aborts + // can be eventually consistent, so poll rather than reading once + // (CLDSRV-938). + const remainingUploads = await waitForMpuCleanup(s3, bucketName, uploadId); assert.strictEqual(remainingUploads.length, 0, 'No MPU metadata should remain after concurrent aborts'); });