-
Notifications
You must be signed in to change notification settings - Fork 830
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Bg sync replay fix #601
Bg sync replay fix #601
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are a few things to address from the inline comments, but a larger question relates to tests: is there an existing test that should have caught the bug in the original behavior? If that test already exists, and wasn't catching the bug, that sounds like it needs to be updated. If there wasn't a test for this behavior at all, then it would be ideal if you could add in a test to accompany this PR that will confirm that things work as expected.
@@ -42,49 +42,58 @@ class RequestManager { | |||
} | |||
|
|||
/** | |||
* function to play one single request | |||
* given its hash | |||
* @param {String=} hash |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you using {String=}
to indicate that this is an optional parameter? If so, we've been using [hash]
elsewhere in the docs (the first option listed in http://usejsdoc.org/tags-param.html#optional-parameters-and-default-values)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nah, its not an optional comment, removed the =
* function to play one single request | ||
* given its hash | ||
* @param {String=} hash | ||
* @return {Promise} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you provide some context about what the promise represents? I.e. "Resolves when..., rejects when..."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
response: response.clone(), | ||
idbQDb: this._queue.idbQDb, | ||
}); | ||
this._globalCallbacks.onResponse |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you switch to if(this._globalCallbacks.onResponse) {...}
. We've been using that syntax rather than &&
elsewhere in the project.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
&& this._globalCallbacks.onResponse(hash, response); | ||
} | ||
} catch(err) { | ||
this._globalCallbacks.onRetryFailure |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the previous comment about preferring if()
to &&
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
idbRequestObject: reqData.request}); | ||
const response = await fetch(request); | ||
if(!response.ok) { | ||
return Promise.reject(response.status); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to confirm: you're not invoking the this._globalCallbacks.onRetryFailure
callback here. Is that intentional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed this, moved this to parent function to catch all kind of rejections at one place
await this.replayRequest(hash); | ||
} catch (err) { | ||
failedItems.push(err); | ||
failedItems++; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not clear as to why you'd increment an array. Is this a typo?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sorry, yeah typo
}, Promise.resolve()); | ||
async replayRequests() { | ||
let failedItems = []; | ||
for (let hash of this._queue.queue) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this._globalCallbacks.onRetryFailure
end up adding in the failed entries to the end of this._queue.queue
? If so, it seems like you may end up processing the same entry more than once while iterating through this._queue.queue
if one of the earlier replay requests fails.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am a little unclear as of what do you mean by end up adding in failed entries?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The values in this._queue.queue
are set somewhere—I'm not sure where. I'm asking whether they're set inside of this._globalCallbacks.onRetryFailure
, and if they are, how they're being set.
What I'm worried about is the potential for an infinite loop while you iterate over this._queue.queue
. I could imagine this happening if you're adding a new entry to the end of this._queue.queue
every time there's a failure inside the loop.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh ok I guess what you are asking, so this._globalCallbacks.onRetryFailure
is actually just a callback from user land so that we can notify the user that hey request X has been failed,
user can then show a notification, do a console log or anything he/she wish for. This has nothing to do with requests getting stored, that portion is completely de-coupled from this.
bg-sync calls this._globalCallbacks.onRetryFailure
every-time it fails replaying the request and calls this._globalCallbacks.onResponse
whenever we successfully fetch a request
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So what's the code that removes items from this._queue.queue
? Don't you have to remove them within one of the callbacks? And not remove them if there's a failure?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't the request remain in the queue if it is failed, so that it can be replayed at later point?
And for the removal part, I remove them with a separate . cleanupQueue
method, which is also auto-called while initialization.
* function to start playing requests | ||
* in sequence | ||
* @return {void} | ||
* @return {Promise} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you provide some context as to what this Promise represents?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
failedItems++; | ||
} | ||
} | ||
return failedItems.length > 0 ? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is an async function, can you just throw an error?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure about this, so rejecting the promise like this gives me an opportunity to pass the entire array of failes Requests and their causes. just in case developer decides to do something with it.
My only comment on this PR, this feels like a fairly big change, without any test changes or new tests to come with it. It sounds like something was broken, which meant it wasn't covered by a test, is there anyway we can add a test for it? |
@jeffposnick @gauntface yeah this code never had a test backing it up, I will be adding new tests for this change. |
84bc46d
to
7a58572
Compare
replayRequests() { | ||
return this._queue.queue.reduce((promise, hash) => { | ||
async replayRequests() { | ||
let allRequestsStatus = []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be a const
actually.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
7a58572
to
1f9f5bf
Compare
@jeffposnick @gauntface the new test for failing promise is added |
I wonder... Can we get confirmation from Chrome team about this? @addyosmani
On 7. June 2017 at 16:34:41, Prateek Bhatnagar (notifications@github.com<mailto:notifications@github.com>) wrote:
@jeffposnick<https://github.com/jeffposnick> @gauntface<https://github.com/gauntface> the new test for failing promise is added
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub<#601 (comment)>, or mute the thread<https://github.com/notifications/unsubscribe-auth/AAuASDAKnUd9bJay2kqxQpkOqV1eCC-Hks5sBrUBgaJpZM4NxMLE>.
|
@anilanar confirmation about? |
About what does passing a rejected promise do to the sync event. That blog post I shared in the original issue is too old, perhaps things have changed since then? |
@anilanar i just confirmed it is working like that, will re confirm before submitting |
async replayRequest(hash) { | ||
try { | ||
const reqData = await this._queue.getRequestFromQueue({hash}); | ||
if(reqData.response) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jeffposnick this prevents replaying an already "successfully" played request
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha.
The code changes look 👍 There are various CI test failures. I think some of them have to do with node no longer being happy with |
}); | ||
}, Promise.resolve()); | ||
if (this._globalCallbacks.onResponse) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not to be address in this PR, but I can't help but feel like this should be an event that is emitted rather than a callback function.
this._globalCallbacks.onResponse(hash, response); | ||
} | ||
} catch(err) { | ||
return Promise.reject(err.stack); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are you returning just the stack here? Why not the whole error which will include the message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done!
} | ||
|
||
/** | ||
* function to start playing requests in sequence |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you beef up this message up a little as it'll be in the reference docs.
i.e. When you need to replay all of the currently queued requests, call this method, it'll do XYZ, ....
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried adding some description though this one is internally called. The one available to the user to call is in background-sync-queue
* @return {Promise} Resolves if all requests are played successfully, | ||
* rejects if any of the request fails during the replay | ||
* | ||
* @memberOf RequestManager |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This member of isn't needed for individual functions. The function is part of the class and whatever the class is a memberof is the important bit.
All of this is unnecessary because you've marked this class as private.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done!
@@ -71,17 +74,29 @@ describe('background sync queue test', () => { | |||
|
|||
it('check push proxy', async () => { | |||
const currentLen = backgroundSyncQueue._queue.queue.length; | |||
await backgroundSyncQueue.pushIntoQueue({request: new Request('http://lipsum.com')}); | |||
await backgroundSyncQueue.pushIntoQueue({request: new Request('http://localhost:3000/__echo/counter')}); | |||
await backgroundSyncQueue.replayRequests(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are you replaying requests here but not before? My hunch from the code is that you are only checking the queue length, so whats the point of replaying?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cleaned this up, made bgSyncQueue
to be reinitiated before every test, so that replay is not needed anymore.
@@ -71,17 +74,29 @@ describe('background sync queue test', () => { | |||
|
|||
it('check push proxy', async () => { | |||
const currentLen = backgroundSyncQueue._queue.queue.length; | |||
await backgroundSyncQueue.pushIntoQueue({request: new Request('http://lipsum.com')}); | |||
await backgroundSyncQueue.pushIntoQueue({request: new Request('http://localhost:3000/__echo/counter')}); | |||
await backgroundSyncQueue.replayRequests(); | |||
chai.assert.equal(backgroundSyncQueue._queue.queue.length, currentLen + 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The 'currentLen + 1' always concerned me a bit here, is this test reliant on other tests passing (i.e. if one test is incorrect and the length is wrong or currentLen is not updated, will this test fail?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cleaned this up, made bgSyncQueue
to be reinitiated before every test, so that no test is interdependent on each other
await backgroundSyncQueue.replayRequests(); | ||
chai.assert.equal(responseAchieved, 2); | ||
}); | ||
|
||
it('check replay failure with rejected promise', async function() { | ||
await backgroundSyncQueue.pushIntoQueue({request: new Request('http://localhost:3000/__echo/counter')}); | ||
await backgroundSyncQueue.pushIntoQueue({request: new Request('http://localhost:3002/__echo/counter')}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be better to have an end point that just always fails. It's not clear from this test that you are referencing a failing URL.
Would '/__test/404/' endpoint work for your needs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done, thaks its much cleaner now :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are a few testing comments I've raised that would be good to address, specifically the invalid URL
Let's aim to address Matt's feedback and make sure Travis is happy before we merge this. |
21f41aa
to
028f335
Compare
028f335
to
280e3cb
Compare
@@ -15,6 +15,10 @@ | |||
/* global chai, workbox */ | |||
|
|||
'use strict'; | |||
const testServerGen = require('../../../../utils/test-server-generator.js'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test makes no sense I'm afraid.
test files under **test/browser/ are unit tests that run in the browser. There is no require, there is no need to start a server and this code just fails :(
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sorry, for the goof up, I managed to fix all the test cases. lets sync up on this sometime today
5036599
to
fc1e741
Compare
= new workbox.backgroundSync.test.BackgroundSyncQueue({ | ||
let backgroundSyncQueue; | ||
|
||
beforeEach(async ()=>{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use function() {}
here. This is because mocha does some weird binding stuff that means fat arrows have unexpected results if you tried to do this.timeout or this.retries.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cool
callbacks: CALLBACKS, | ||
}); | ||
}); | ||
|
||
afterEach(async ()=> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above - switch to function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two last comments and then I'm happy - sorry Prateek.
fc1e741
to
8a66894
Compare
@@ -140,7 +140,8 @@ class RequestQueue { | |||
assert.isType({hash}, 'string'); | |||
|
|||
if(this._queue.includes(hash)) { | |||
return await this._idbQDb.get(hash); | |||
const req = await this._idbQDb.get(hash); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gauntface just wanted to discuss this small change.
actually, this small change fixed everything failing on firefox, do you have any context?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nope nothing from me. cc @jeffposnick as FYI
@jeffposnick can you approve if all looks good? |
R: @jeffposnick
Fixes #597
After this PR if during the entire replay process any request fails, we'll be passing a rejected promise to
event.waitUntil
ofsync
event, so that browser can replay the failed requests