Skip to content

Commit 715d7c0

Browse files
S3UTILS-139 repairDuplicateVersionIds: safer repair
Add safety to repair operation: - check the state of the master key before repairing, to ensure there is still a duplicate "versionId" field - repair using the "versionId" just read from the metadata, rather than the one from the log, to make sure we repair an up-to-date version (or be reasonably sure about it)
1 parent 7dbe763 commit 715d7c0

File tree

1 file changed

+77
-19
lines changed

1 file changed

+77
-19
lines changed

repairDuplicateVersionIds.js

Lines changed: 77 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,24 @@ const async = require('async');
33
const readline = require('readline');
44
const { Logger } = require('werelogs');
55
const {
6-
fetchObjectMetadata,
6+
httpRequest,
77
putObjectMetadata,
88
} = require('./repairDuplicateVersionsSuite');
99

1010
const log = new Logger('s3utils:repairDuplicateVersionIds');
1111

1212
const {
1313
OBJECT_REPAIR_BUCKETD_HOSTPORT,
14+
OBJECT_REPAIR_TLS_KEY_PATH,
15+
OBJECT_REPAIR_TLS_CERT_PATH,
16+
OBJECT_REPAIR_TLS_CA_PATH,
1417
} = process.env;
1518

19+
const useHttps = (OBJECT_REPAIR_TLS_KEY_PATH !== undefined
20+
&& OBJECT_REPAIR_TLS_KEY_PATH !== ''
21+
&& OBJECT_REPAIR_TLS_CERT_PATH !== undefined
22+
&& OBJECT_REPAIR_TLS_CERT_PATH !== '');
23+
1624
const USAGE = `
1725
repairDuplicateVersionIds.js
1826
@@ -51,10 +59,13 @@ const objectsToRepair = [];
5159

5260
const status = {
5361
logLinesRead: 0,
62+
objectsSkipped: 0,
5463
objectsRepaired: 0,
5564
objectsErrors: 0,
5665
};
5766

67+
const errorMetadataUpdated = new Error('metadata updated');
68+
5869
function logProgress(message) {
5970
log.info(message, { ...status, objectsToRepair: objectsToRepair.length });
6071
}
@@ -93,9 +104,26 @@ function readVerifyLog(cb) {
93104
});
94105
}
95106

107+
function fetchRawObjectMetadata(objectUrl, cb) {
108+
if (!objectUrl.startsWith('s3://')) {
109+
return cb(new Error(`malformed object URL ${objectUrl}: must start with "s3://"`));
110+
}
111+
const bucketAndObject = objectUrl.slice(5);
112+
const url = `${useHttps ? 'https' : 'http'}://${OBJECT_REPAIR_BUCKETD_HOSTPORT}/default/bucket/${bucketAndObject}`;
113+
return httpRequest('GET', url, null, (err, res) => {
114+
if (err) {
115+
return cb(err);
116+
}
117+
if (res.statusCode !== 200) {
118+
return cb(new Error(`GET ${url} returned status ${res.statusCode}`));
119+
}
120+
return cb(null, res.body);
121+
});
122+
}
123+
96124
function repairObject(objInfo, cb) {
97125
async.waterfall([
98-
next => fetchObjectMetadata(objInfo.objectUrl, (err, md) => {
126+
next => fetchRawObjectMetadata(objInfo.objectUrl, (err, md) => {
99127
if (err) {
100128
log.error('error fetching object location', {
101129
objectUrl: objInfo.objectUrl,
@@ -105,10 +133,32 @@ function repairObject(objInfo, cb) {
105133
}
106134
return next(null, md);
107135
}),
108-
(md, next) => {
136+
(rawMD, next) => {
137+
const reVersionIds = /"versionId":"([^"]*)"/g;
138+
const versionIds = [];
139+
// eslint-disable-next-line no-constant-condition
140+
while (true) {
141+
const reVersionIdMatch = reVersionIds.exec(rawMD);
142+
if (!reVersionIdMatch) {
143+
break;
144+
}
145+
versionIds.push(reVersionIdMatch[1]);
146+
}
147+
if (versionIds.length < 2) {
148+
log.info('skipping repair: master key has no more duplicate "versionId"', {
149+
objectUrl: objInfo.objectUrl,
150+
versionId: versionIds.length > 0 ? versionIds[0] : undefined,
151+
});
152+
return next(errorMetadataUpdated);
153+
}
154+
const md = JSON.parse(rawMD);
155+
// use "versionId" from the parsed metadata instead of
156+
// `objInfo.firstVersionId`, since it may have changed
157+
// since the scan ran
158+
//
109159
// eslint-disable-next-line no-param-reassign
110-
md.versionId = objInfo.firstVersionId;
111-
putObjectMetadata(objInfo.objectUrl, md, err => {
160+
[md.versionId] = versionIds;
161+
return putObjectMetadata(objInfo.objectUrl, md, err => {
112162
if (err) {
113163
log.error('error putting object metadata to master key', {
114164
objectUrl: objInfo.objectUrl,
@@ -119,27 +169,35 @@ function repairObject(objInfo, cb) {
119169
return next(null, md);
120170
});
121171
},
122-
(md, next) => putObjectMetadata(objInfo.versionedKeyUrl, md, err => {
123-
if (err) {
124-
log.error('error putting object metadata to versioned key', {
172+
(md, next) => {
173+
const versionedKeyUrl = `${objInfo.objectUrl}${encodeURIComponent(`\0${md.versionId}`)}`;
174+
putObjectMetadata(versionedKeyUrl, md, err => {
175+
if (err) {
176+
log.error('error putting object metadata to versioned key', {
177+
objectUrl: objInfo.objectUrl,
178+
versionedKeyUrl,
179+
error: { message: err.message },
180+
});
181+
return next(err);
182+
}
183+
return next(null, md);
184+
});
185+
},
186+
], (err, md) => {
187+
if (err) {
188+
if (err === errorMetadataUpdated) {
189+
status.objectsSkipped += 1;
190+
} else {
191+
log.error('an error occurred repairing object', {
125192
objectUrl: objInfo.objectUrl,
126-
versionedKeyUrl: objInfo.versionedKeyUrl,
127193
error: { message: err.message },
128194
});
129-
return next(err);
195+
status.objectsErrors += 1;
130196
}
131-
return next();
132-
}),
133-
], err => {
134-
if (err) {
135-
log.error('an error occurred repairing object', {
136-
objectUrl: objInfo.objectUrl,
137-
error: { message: err.message },
138-
});
139-
status.objectsErrors += 1;
140197
} else {
141198
log.info('repaired object metadata', {
142199
objectUrl: objInfo.objectUrl,
200+
versionId: md.versionId,
143201
});
144202
status.objectsRepaired += 1;
145203
}

0 commit comments

Comments
 (0)