Skip to content

Commit c1a7347

Browse files
montymxbflovilmart
authored andcommitted
Fix for _PushStatus Stuck 'running' when Count is Off (#4319)
* Fix for _PushStatus stuck 'running' if count is off * use 'count' for batches * push worker test fix
1 parent 842343a commit c1a7347

File tree

5 files changed

+276
-9
lines changed

5 files changed

+276
-9
lines changed

spec/Parse.Push.spec.js

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,4 +203,261 @@ describe('Parse.Push', () => {
203203
done();
204204
});
205205
});
206+
207+
const successfulAny = function(body, installations) {
208+
const promises = installations.map((device) => {
209+
return Promise.resolve({
210+
transmitted: true,
211+
device: device,
212+
})
213+
});
214+
215+
return Promise.all(promises);
216+
};
217+
218+
const provideInstallations = function(num) {
219+
if(!num) {
220+
num = 2;
221+
}
222+
223+
const installations = [];
224+
while(installations.length !== num) {
225+
// add Android installations
226+
const installation = new Parse.Object("_Installation");
227+
installation.set("installationId", "installation_" + installations.length);
228+
installation.set("deviceToken","device_token_" + installations.length);
229+
installation.set("deviceType", "android");
230+
installations.push(installation);
231+
}
232+
233+
return installations;
234+
};
235+
236+
const losingAdapter = {
237+
send: function(body, installations) {
238+
// simulate having lost an installation before this was called
239+
// thus invalidating our 'count' in _PushStatus
240+
installations.pop();
241+
242+
return successfulAny(body, installations);
243+
},
244+
getValidPushTypes: function() {
245+
return ["android"];
246+
}
247+
};
248+
249+
/**
250+
* Verifies that _PushStatus cannot get stuck in a 'running' state
251+
* Simulates a simple push where 1 installation is removed between _PushStatus
252+
* count being set and the pushes being sent
253+
*/
254+
it('does not get stuck with _PushStatus \'running\' on 1 installation lost', (done) => {
255+
reconfigureServer({
256+
push: {adapter: losingAdapter}
257+
}).then(() => {
258+
return Parse.Object.saveAll(provideInstallations());
259+
}).then(() => {
260+
return Parse.Push.send(
261+
{
262+
data: {alert: "We fixed our status!"},
263+
where: {deviceType: 'android'}
264+
},
265+
{ useMasterKey: true }
266+
);
267+
}).then(() => {
268+
// it is enqueued so it can take time
269+
return new Promise((resolve) => {
270+
setTimeout(() => {
271+
resolve();
272+
}, 1000);
273+
});
274+
}).then(() => {
275+
// query for push status
276+
const query = new Parse.Query('_PushStatus');
277+
return query.find({useMasterKey: true});
278+
}).then((results) => {
279+
// verify status is NOT broken
280+
expect(results.length).toBe(1);
281+
const result = results[0];
282+
expect(result.get('status')).toEqual('succeeded');
283+
expect(result.get('numSent')).toEqual(1);
284+
expect(result.get('count')).toEqual(undefined);
285+
done();
286+
});
287+
});
288+
289+
/**
290+
* Verifies that _PushStatus cannot get stuck in a 'running' state
291+
* Simulates a simple push where 1 installation is added between _PushStatus
292+
* count being set and the pushes being sent
293+
*/
294+
it('does not get stuck with _PushStatus \'running\' on 1 installation added', (done) => {
295+
const installations = provideInstallations();
296+
297+
// add 1 iOS installation which we will omit & add later on
298+
const iOSInstallation = new Parse.Object("_Installation");
299+
iOSInstallation.set("installationId", "installation_" + installations.length);
300+
iOSInstallation.set("deviceToken","device_token_" + installations.length);
301+
iOSInstallation.set("deviceType", "ios");
302+
installations.push(iOSInstallation);
303+
304+
reconfigureServer({
305+
push: {
306+
adapter: {
307+
send: function(body, installations) {
308+
// simulate having added an installation before this was called
309+
// thus invalidating our 'count' in _PushStatus
310+
installations.push(iOSInstallation);
311+
312+
return successfulAny(body, installations);
313+
},
314+
getValidPushTypes: function() {
315+
return ["android"];
316+
}
317+
}
318+
}
319+
}).then(() => {
320+
return Parse.Object.saveAll(installations);
321+
}).then(() => {
322+
return Parse.Push.send(
323+
{
324+
data: {alert: "We fixed our status!"},
325+
where: {deviceType: {'$ne' : 'random'}}
326+
},
327+
{ useMasterKey: true }
328+
);
329+
}).then(() => {
330+
// it is enqueued so it can take time
331+
return new Promise((resolve) => {
332+
setTimeout(() => {
333+
resolve();
334+
}, 1000);
335+
});
336+
}).then(() => {
337+
// query for push status
338+
const query = new Parse.Query('_PushStatus');
339+
return query.find({useMasterKey: true});
340+
}).then((results) => {
341+
// verify status is NOT broken
342+
expect(results.length).toBe(1);
343+
const result = results[0];
344+
expect(result.get('status')).toEqual('succeeded');
345+
expect(result.get('numSent')).toEqual(3);
346+
expect(result.get('count')).toEqual(undefined);
347+
done();
348+
});
349+
});
350+
351+
/**
352+
* Verifies that _PushStatus cannot get stuck in a 'running' state
353+
* Simulates an extended push, where some installations may be removed,
354+
* resulting in a non-zero count
355+
*/
356+
it('does not get stuck with _PushStatus \'running\' on many installations removed', (done) => {
357+
const devices = 1000;
358+
const installations = provideInstallations(devices);
359+
360+
reconfigureServer({
361+
push: {adapter: losingAdapter}
362+
}).then(() => {
363+
return Parse.Object.saveAll(installations);
364+
}).then(() => {
365+
return Parse.Push.send(
366+
{
367+
data: {alert: "We fixed our status!"},
368+
where: {deviceType: 'android'}
369+
},
370+
{ useMasterKey: true }
371+
);
372+
}).then(() => {
373+
// it is enqueued so it can take time
374+
return new Promise((resolve) => {
375+
setTimeout(() => {
376+
resolve();
377+
}, 1000);
378+
});
379+
}).then(() => {
380+
// query for push status
381+
const query = new Parse.Query('_PushStatus');
382+
return query.find({useMasterKey: true});
383+
}).then((results) => {
384+
// verify status is NOT broken
385+
expect(results.length).toBe(1);
386+
const result = results[0];
387+
expect(result.get('status')).toEqual('succeeded');
388+
// expect # less than # of batches used, assuming each batch is 100 pushes
389+
expect(result.get('numSent')).toEqual(devices - (devices / 100));
390+
expect(result.get('count')).toEqual(undefined);
391+
done();
392+
});
393+
});
394+
395+
/**
396+
* Verifies that _PushStatus cannot get stuck in a 'running' state
397+
* Simulates an extended push, where some installations may be added,
398+
* resulting in a non-zero count
399+
*/
400+
it('does not get stuck with _PushStatus \'running\' on many installations added', (done) => {
401+
const devices = 1000;
402+
const installations = provideInstallations(devices);
403+
404+
// add 1 iOS installation which we will omit & add later on
405+
const iOSInstallations = [];
406+
407+
while(iOSInstallations.length !== (devices / 100)) {
408+
const iOSInstallation = new Parse.Object("_Installation");
409+
iOSInstallation.set("installationId", "installation_" + installations.length);
410+
iOSInstallation.set("deviceToken", "device_token_" + installations.length);
411+
iOSInstallation.set("deviceType", "ios");
412+
installations.push(iOSInstallation);
413+
iOSInstallations.push(iOSInstallation);
414+
}
415+
416+
reconfigureServer({
417+
push: {
418+
adapter: {
419+
send: function(body, installations) {
420+
// simulate having added an installation before this was called
421+
// thus invalidating our 'count' in _PushStatus
422+
installations.push(iOSInstallations.pop());
423+
424+
return successfulAny(body, installations);
425+
},
426+
getValidPushTypes: function() {
427+
return ["android"];
428+
}
429+
}
430+
}
431+
}).then(() => {
432+
return Parse.Object.saveAll(installations);
433+
}).then(() => {
434+
return Parse.Push.send(
435+
{
436+
data: {alert: "We fixed our status!"},
437+
where: {deviceType: {'$ne' : 'random'}}
438+
},
439+
{ useMasterKey: true }
440+
);
441+
}).then(() => {
442+
// it is enqueued so it can take time
443+
return new Promise((resolve) => {
444+
setTimeout(() => {
445+
resolve();
446+
}, 1000);
447+
});
448+
}).then(() => {
449+
// query for push status
450+
const query = new Parse.Query('_PushStatus');
451+
return query.find({useMasterKey: true});
452+
}).then((results) => {
453+
// verify status is NOT broken
454+
expect(results.length).toBe(1);
455+
const result = results[0];
456+
expect(result.get('status')).toEqual('succeeded');
457+
// expect # less than # of batches used, assuming each batch is 100 pushes
458+
expect(result.get('numSent')).toEqual(devices + (devices / 100));
459+
expect(result.get('count')).toEqual(undefined);
460+
done();
461+
});
462+
});
206463
});

spec/PushWorker.spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ describe('PushWorker', () => {
276276
'failedPerType.ios': { __op: 'Increment', amount: 1 },
277277
[`sentPerUTCOffset.${UTCOffset}`]: { __op: 'Increment', amount: 1 },
278278
[`failedPerUTCOffset.${UTCOffset}`]: { __op: 'Increment', amount: 1 },
279-
count: { __op: 'Increment', amount: -2 },
279+
count: { __op: 'Increment', amount: -1 }
280280
});
281281
const query = new Parse.Query('_PushStatus');
282282
return query.get(handler.objectId, { useMasterKey: true });
@@ -354,7 +354,7 @@ describe('PushWorker', () => {
354354
'failedPerType.ios': { __op: 'Increment', amount: 1 },
355355
[`sentPerUTCOffset.${UTCOffset}`]: { __op: 'Increment', amount: 1 },
356356
[`failedPerUTCOffset.${UTCOffset}`]: { __op: 'Increment', amount: 1 },
357-
count: { __op: 'Increment', amount: -2 },
357+
count: { __op: 'Increment', amount: -1 }
358358
});
359359
done();
360360
});

src/Controllers/SchemaController.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ const defaultColumns = Object.freeze({
8888
"failedPerType": {type:'Object'},
8989
"sentPerUTCOffset": {type:'Object'},
9090
"failedPerUTCOffset": {type:'Object'},
91-
"count": {type:'Number'}
91+
"count": {type:'Number'} // tracks # of batches queued and pending
9292
},
9393
_JobStatus: {
9494
"jobName": {type: 'String'},

src/Push/PushQueue.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export class PushQueue {
4040
if (!results || count == 0) {
4141
return Promise.reject({error: 'PushController: no results in query'})
4242
}
43-
pushStatus.setRunning(count);
43+
pushStatus.setRunning(Math.ceil(count / limit));
4444
let skip = 0;
4545
while (skip < count) {
4646
const query = { where,

src/StatusHandler.js

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -190,10 +190,18 @@ export function pushStatusHandler(config, existingObjectId) {
190190
});
191191
}
192192

193-
const setRunning = function(count) {
194-
logger.verbose(`_PushStatus ${objectId}: sending push to %d installations`, count);
195-
return handler.update({status:"pending", objectId: objectId},
196-
{status: "running", count });
193+
const setRunning = function(batches) {
194+
logger.verbose(`_PushStatus ${objectId}: sending push to installations with %d batches`, batches);
195+
return handler.update(
196+
{
197+
status:"pending",
198+
objectId: objectId
199+
},
200+
{
201+
status: "running",
202+
count: batches
203+
}
204+
);
197205
}
198206

199207
const trackSent = function(results, UTCOffset, cleanupInstallations = process.env.PARSE_SERVER_CLEANUP_INVALID_INSTALLATIONS) {
@@ -235,7 +243,6 @@ export function pushStatusHandler(config, existingObjectId) {
235243
}
236244
return memo;
237245
}, update);
238-
incrementOp(update, 'count', -results.length);
239246
}
240247

241248
logger.verbose(`_PushStatus ${objectId}: sent push! %d success, %d failures`, update.numSent, update.numFailed);
@@ -259,6 +266,9 @@ export function pushStatusHandler(config, existingObjectId) {
259266
});
260267
}
261268

269+
// indicate this batch is complete
270+
incrementOp(update, 'count', -1);
271+
262272
return handler.update({ objectId }, update).then((res) => {
263273
if (res && res.count === 0) {
264274
return this.complete();

0 commit comments

Comments
 (0)