Skip to content

Commit 4ab6f8e

Browse files
authored
fix(gridstore-upload): ZMSA-17: fix gridstore upload deadlock (#918)
* fix gridstore upload deadlock * releaseLock add callback if no calculatedFileContentHash * gridstore-storage improve flow, refactor * gridstore create handle case if lock variable is set but lock was not acquired * gridstore attachmentCallback refactor * refactor already returned logic in gristore create
1 parent 7264833 commit 4ab6f8e

File tree

1 file changed

+71
-68
lines changed

1 file changed

+71
-68
lines changed

lib/attachments/gridstore-storage.js

Lines changed: 71 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -154,36 +154,40 @@ class GridstoreStorage {
154154
calculatedFileContentHash = args[2];
155155
}
156156

157+
const finalizeCallback = () => {
158+
if (returned) {
159+
// might be already finished if retrying after delay
160+
return;
161+
}
162+
163+
returned = true;
164+
if (calculatedFileContentHash) {
165+
this.updateFileWithContentHashMetadata(args, hash, calculatedFileContentHash, callback);
166+
return;
167+
}
168+
169+
callback(...args);
170+
};
171+
157172
if (storeLock) {
158173
log.silly('GridStore', '[%s] UNLOCK lock=%s status=%s', instance, lockId, storeLock.success ? 'locked' : 'empty');
174+
159175
if (storeLock.success) {
160-
this.lock.releaseLock(storeLock, () => {
161-
if (returned) {
162-
// might be already finished if retrying after delay
163-
return;
164-
}
165-
if (calculatedFileContentHash) {
166-
// locked upload, new file
167-
this.updateFileWithContentHashMetadata(args, hash, calculatedFileContentHash, callback);
168-
return; // return from attachmentCallback. Top level callback will be ran after hash update
169-
}
170-
});
171-
// unset variable to prevent double releasing
172-
storeLock = false;
173-
return;
176+
// lock acquired
177+
this.lock.releaseLock(storeLock, finalizeCallback);
174178
}
179+
180+
if (!storeLock.success) {
181+
// lock was not acquired
182+
finalizeCallback();
183+
}
184+
185+
// unset variable to prevent double releasing
175186
storeLock = false;
176-
}
177-
if (returned) {
178-
// might be already finished if retrying after delay
179187
return;
180188
}
181-
if (calculatedFileContentHash) {
182-
// no lock upload, new file
183-
this.updateFileWithContentHashMetadata(args, hash, calculatedFileContentHash, callback);
184-
return; // return from attachmentCallback. Top level callback will be ran after hash update
185-
}
186-
callback(...args);
189+
190+
finalizeCallback();
187191
};
188192

189193
let tryCount = 0;
@@ -245,7 +249,6 @@ class GridstoreStorage {
245249
if (returned) {
246250
return;
247251
}
248-
returned = true;
249252
return attachmentCallback(err);
250253
}
251254

@@ -266,56 +269,56 @@ class GridstoreStorage {
266269
if (returned) {
267270
return;
268271
}
269-
if (err.code === 11000) {
270-
// most probably a race condition, try again
271-
if (tryCount++ < 5) {
272-
if (/\.chunks /.test(err.message)) {
273-
// Partial chunks detected. Might be because of:
274-
// * another process is inserting the same attachment and thus no "files" entry yet (should not happened though due to locking)
275-
// * previously deleted attachment that has not been properly removed
276-
277-
// Load data for an existing chunk to see the age of it
278-
return this.gridfs.collection(this.bucketName + '.chunks').findOne(
279-
{
280-
files_id: hash
281-
},
282-
{
283-
projection: {
284-
_id: true
285-
}
286-
},
287-
(err, data) => {
288-
if (err) {
289-
// whatever
290-
return setTimeout(tryStore, 100 + 200 * Math.random());
291-
}
292-
293-
if (!data || !data._id) {
294-
// try again, no chunks found
295-
return setTimeout(tryStore, 10);
296-
}
272+
// most probably a race condition, try again
273+
if (tryCount++ < 5) {
274+
if (/\.chunks /.test(err.message)) {
275+
// Partial chunks detected. Might be because of:
276+
// * another process is inserting the same attachment and thus no "files" entry yet (should not happened though due to locking)
277+
// * previously deleted attachment that has not been properly removed
278+
279+
// Load data for an existing chunk to see the age of it
280+
return this.gridfs.collection(this.bucketName + '.chunks').findOne(
281+
{
282+
files_id: hash
283+
},
284+
{
285+
projection: {
286+
_id: true
287+
}
288+
},
289+
(err, data) => {
290+
if (err) {
291+
// whatever
292+
return setTimeout(tryStore, 100 + 200 * Math.random());
293+
}
297294

298-
// Check how old is the previous chunk
299-
let timestamp = data._id.getTimestamp();
300-
if (timestamp && typeof timestamp.getTime === 'function' && timestamp.getTime() >= Date.now() - 5 * 60 * 1000) {
301-
// chunk is newer than 5 minutes, assume race condition and try again after a while
302-
return setTimeout(tryStore, 300 + 200 * Math.random());
303-
}
295+
if (!data || !data._id) {
296+
// try again, no chunks found
297+
return setTimeout(tryStore, 10);
298+
}
304299

305-
// partial chunks for a probably deleted message detected, try to clean up
306-
setTimeout(() => {
307-
if (returned) {
308-
return;
309-
}
310-
this.cleanupGarbage(id, tryStore);
311-
}, 100 + 200 * Math.random());
300+
// Check how old is the previous chunk
301+
let timestamp = data._id.getTimestamp();
302+
if (timestamp && typeof timestamp.getTime === 'function' && timestamp.getTime() >= Date.now() - 5 * 60 * 1000) {
303+
// chunk is newer than 5 minutes, assume race condition and try again after a while
304+
return setTimeout(tryStore, 300 + 200 * Math.random());
312305
}
313-
);
314-
}
306+
307+
// partial chunks for a probably deleted message detected, try to clean up
308+
setTimeout(() => {
309+
if (returned) {
310+
return;
311+
}
312+
this.cleanupGarbage(id, tryStore);
313+
}, 100 + 200 * Math.random());
314+
}
315+
);
316+
} else {
315317
return setTimeout(tryStore, 10);
316318
}
319+
} else {
320+
attachmentCallback(err);
317321
}
318-
attachmentCallback(err);
319322
});
320323

321324
store.once('finish', () => attachmentCallback(null, id, fileHashCalculator.hash));

0 commit comments

Comments
 (0)