Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions src/storage/version/put.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,16 @@ function buildInput({
};
}

const MAX_PUT_ATTEMPTS = 5;

export async function putObjectWithVersion(
env,
daCtx,
update,
body,
guid,
clientConditionals = null,
putAttempt = 0,
) {
const config = getS3Config(env);
const current = await getObject(env, update, false);
Expand Down Expand Up @@ -186,14 +189,15 @@ export async function putObjectWithVersion(
const status = e.$metadata?.httpStatusCode || 500;
if (status === 412) {
// Only retry if no client conditionals (internal operation) and under retry limit
if (!effectiveConditionals?.ifNoneMatch) {
if (!effectiveConditionals?.ifNoneMatch && putAttempt < MAX_PUT_ATTEMPTS) {
return putObjectWithVersion(
env,
daCtx,
update,
body,
guid,
clientConditionals,
putAttempt + 1,
);
}
// Client conditional failed or max retries exceeded, return 412
Expand Down Expand Up @@ -311,8 +315,16 @@ export async function putObjectWithVersion(
// A specific ETag means the client explicitly requires that version, so propagate 412.
const shouldRetry = !effectiveConditionals?.ifMatch
|| effectiveConditionals.ifMatch === '*';
if (shouldRetry) {
return putObjectWithVersion(env, daCtx, update, body, guid, clientConditionals);
if (shouldRetry && putAttempt < MAX_PUT_ATTEMPTS) {
return putObjectWithVersion(
env,
daCtx,
update,
body,
guid,
clientConditionals,
putAttempt + 1,
);
}
return { status: 412, metadata: { id: ID } };
}
Expand Down
66 changes: 66 additions & 0 deletions test/storage/version/put.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,72 @@ describe('Version Put', () => {
assert.equal(1, sendCalls.length, 'Should not retry when client sent a specific ETag');
});

it('putObjectWithVersion stops retrying new document after MAX_PUT_ATTEMPTS', async () => {
const getObjectCalls = [];
const mockGetObject = async (e, u, nb) => {
getObjectCalls.push(1);
return { status: 404, metadata: {} };
};

const sendCalls = [];
const mockS3Client = {
async send(cmd) {
sendCalls.push(cmd);
const err = new Error('PreconditionFailed');
err.$metadata = { httpStatusCode: 412 };
throw err;
},
};

const { putObjectWithVersion } = await esmock('../../../src/storage/version/put.js', {
'../../../src/storage/object/get.js': { default: mockGetObject },
'../../../src/storage/utils/version.js': { ifNoneMatch: () => mockS3Client },
});

const resp = await putObjectWithVersion({}, { users: [] }, {}, false);

assert.equal(412, resp.status);
assert.equal(6, sendCalls.length, 'Should attempt exactly MAX_PUT_ATTEMPTS+1 (5+1) times');
assert.equal(6, getObjectCalls.length, 'Should re-fetch object on each retry');
});

it('putObjectWithVersion stops retrying existing document after MAX_PUT_ATTEMPTS', async () => {
const getObjectCalls = [];
const mockGetObject = async () => {
getObjectCalls.push(1);
return { status: 200, metadata: {}, etag: 'etag-x' };
};

const sendCalls = [];
const mockIfMatchClient = {
async send(cmd) {
sendCalls.push(cmd);
const err = new Error('PreconditionFailed');
err.$metadata = { httpStatusCode: 412 };
throw err;
},
};
const mockIfNoneMatchClient = {
async send() {
return { $metadata: { httpStatusCode: 200 } };
},
};

const { putObjectWithVersion } = await esmock('../../../src/storage/version/put.js', {
'../../../src/storage/object/get.js': { default: mockGetObject },
'../../../src/storage/utils/version.js': {
ifMatch: () => mockIfMatchClient,
ifNoneMatch: () => mockIfNoneMatchClient,
},
});

const resp = await putObjectWithVersion({}, { users: [] }, {}, false);

assert.equal(412, resp.status);
assert.equal(6, sendCalls.length, 'Should attempt exactly MAX_PUT_ATTEMPTS+1 (5+1) times');
assert.equal(6, getObjectCalls.length, 'Should re-fetch object on each retry');
});

it('Put Object With Version store content', async () => {
// eslint-disable-next-line consistent-return
const mockGetObject = async (e, u, h) => {
Expand Down
Loading