Skip to content

Commit 8d753cb

Browse files
committed
Skip deleted objects in lifecycle requeue batch instead of aborting
When replaying failed archive batches, a deleted object causes ObjNotFound in handleBatch which aborts async.reduce and silently drops all remaining objects. Log the error and continue processing. Issue: BB-749
1 parent 19ddda6 commit 8d753cb

File tree

3 files changed

+112
-4
lines changed

3 files changed

+112
-4
lines changed

extensions/lifecycle/tasks/LifecycleRequeueTask.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,18 @@ class LifecycleRequeueTask extends BackbeatTask {
140140
rest.try,
141141
log,
142142
(err, res) => {
143-
if (err) {
144-
return nextObject(err);
143+
if (!err) {
144+
return nextObject(null, res + objsPerBucket);
145145
}
146146

147-
return nextObject(null, res + objsPerBucket);
147+
if (err.name === 'ObjNotFound') {
148+
log.error('object not found, skipping', {
149+
bucketName, objectKey, objectVersion,
150+
});
151+
return nextObject(null, objsPerBucket);
152+
}
153+
154+
return nextObject(err);
148155
}
149156
),
150157
(err, res) => next(err, res)

tests/unit/lifecycle/LifecycleResetTransitionInProgressTask.spec.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,88 @@ describe('LifecycleResetTransitionInProgressTask', () => {
9090
done();
9191
});
9292
});
93+
94+
describe('ObjNotFound handling in batch', () => {
95+
const makeMd = () => new ObjectMD()
96+
.setContentMd5('etag1')
97+
.setTransitionInProgress(true)
98+
.setUserMetadata({
99+
'x-amz-meta-scal-s3-transition-attempt': 0,
100+
});
101+
102+
it('should skip deleted objects and continue processing valid ones', done => {
103+
const batchEntry = ActionQueueEntry.create('requeueTransition')
104+
.setAttribute('target', {
105+
byAccount: {
106+
123: {
107+
bucket1: [
108+
{ objectKey: 'obj1', objectVersion: 'v1', eTag: '"etag1"', try: 1 },
109+
{ objectKey: 'obj2', objectVersion: 'v2', eTag: '"etag1"', try: 1 },
110+
{ objectKey: 'obj3', objectVersion: 'v3', eTag: '"etag1"', try: 1 },
111+
],
112+
},
113+
},
114+
});
115+
116+
backbeatMetadataProxyClient.setMdObjForKey('obj1', makeMd());
117+
backbeatMetadataProxyClient.setErrorForKey('obj2', { name: 'ObjNotFound' });
118+
backbeatMetadataProxyClient.setMdObjForKey('obj3', makeMd());
119+
120+
task.processActionEntry(batchEntry, err => {
121+
assert.ifError(err);
122+
assert.deepStrictEqual(
123+
backbeatMetadataProxyClient._putCalls.sort(),
124+
['obj1', 'obj3'],
125+
);
126+
done();
127+
});
128+
});
129+
130+
it('should complete without error when all objects are deleted', done => {
131+
const batchEntry = ActionQueueEntry.create('requeueTransition')
132+
.setAttribute('target', {
133+
byAccount: {
134+
123: {
135+
bucket1: [
136+
{ objectKey: 'obj1', objectVersion: 'v1', eTag: '"etag1"', try: 1 },
137+
{ objectKey: 'obj2', objectVersion: 'v2', eTag: '"etag1"', try: 1 },
138+
],
139+
},
140+
},
141+
});
142+
143+
backbeatMetadataProxyClient.setErrorForKey('obj1', { name: 'ObjNotFound' });
144+
backbeatMetadataProxyClient.setErrorForKey('obj2', { name: 'ObjNotFound' });
145+
146+
task.processActionEntry(batchEntry, err => {
147+
assert.ifError(err);
148+
assert.deepStrictEqual(backbeatMetadataProxyClient._putCalls, []);
149+
done();
150+
});
151+
});
152+
153+
it('should abort batch on non-ObjNotFound errors', done => {
154+
const batchEntry = ActionQueueEntry.create('requeueTransition')
155+
.setAttribute('target', {
156+
byAccount: {
157+
123: {
158+
bucket1: [
159+
{ objectKey: 'obj1', objectVersion: 'v1', eTag: '"etag1"', try: 1 },
160+
{ objectKey: 'obj2', objectVersion: 'v2', eTag: '"etag1"', try: 1 },
161+
],
162+
},
163+
},
164+
});
165+
166+
backbeatMetadataProxyClient.setErrorForKey('obj1', { name: 'InternalError' });
167+
backbeatMetadataProxyClient.setMdObjForKey('obj2', makeMd());
168+
169+
task.processActionEntry(batchEntry, err => {
170+
assert.ok(err);
171+
assert.strictEqual(err.name, 'InternalError');
172+
assert.deepStrictEqual(backbeatMetadataProxyClient._putCalls, []);
173+
done();
174+
});
175+
});
176+
});
93177
});

tests/unit/mocks.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,20 +86,37 @@ class BackbeatMetadataProxyMock {
8686
this.indexesObj = null;
8787
this.receivedIdxObj = null;
8888
this.error = null;
89+
this._mdByKey = {};
90+
this._errorByKey = {};
91+
this._putCalls = [];
8992
}
9093

9194
setMdObj(mdObj) {
9295
this.mdObj = mdObj;
9396
}
9497

98+
setMdObjForKey(objectKey, mdObj) {
99+
this._mdByKey[objectKey] = mdObj;
100+
}
101+
102+
setErrorForKey(objectKey, error) {
103+
this._errorByKey[objectKey] = error;
104+
}
105+
95106
getMetadata(params, log, cb) {
107+
const keyError = this._errorByKey[params.objectKey];
108+
if (keyError) {
109+
return cb(keyError);
110+
}
96111
if (this.error) {
97112
return cb(this.error);
98113
}
99-
return cb(null, { Body: this.mdObj.getSerialized() });
114+
const md = this._mdByKey[params.objectKey] || this.mdObj;
115+
return cb(null, { Body: md.getSerialized() });
100116
}
101117

102118
putMetadata(params, log, cb) {
119+
this._putCalls.push(params.objectKey);
103120
this.receivedMd = JSON.parse(params.mdBlob);
104121
this.mdObj = ObjectMD.createFromBlob(params.mdBlob).result;
105122
return cb();

0 commit comments

Comments
 (0)