-
Notifications
You must be signed in to change notification settings - Fork 17
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
Make sure to await all async calls #563
Conversation
background.js
Outdated
} | ||
); | ||
|
||
// TODO: Not using the return value of saveMessage() ??? |
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 returned saved message is not used. It looked strange to me, any insights?
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 don't follow your question entirely.
The assumption of the code here is that the message ID of the newly saved version of the message is identical to the message ID of the previous incarnation of the message. Does that assumption hold true?
If so, then I don't see how it matters that the saved draft is not "used." The point here is to save the message to Drafts for later delivering at the scheduled time. Once the save happens this part of the code no longer has anything it needs to do with the message. Note that the code that does the background send scans all drafts to look for messages to send, so the hand-off occurs by virtue of putting the message into the Drafts folder.
Am I misunderstanding your question?
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.
Ok, if the id of the original draft is not used at all after this call, we are good. But the next two lines re-schedule with the original draft id, not with the saved new draft id, which could be different. A check on the ID will tell you that.
I am just trying to point out edge cases, which may cause some of the observed behaviours.
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.
That's the question I'm asking above. I think @jph-sendlater believed when he wrote this code that the id
field he's using here is the message ID field, and that it will be the same in the saved draft as it was in the previous one. Is that wrong, then yeah, I think this is a bug that needs to be fixed.
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 will try to address this.
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 need help, I think this piece of code has multiple issues.
let originalMsg = await messenger.SL3U.findAssociatedDraft(window.id).then(
m => m ? messenger.messages.getFull(m.id) : null
);
if (originalMsg) {
SLTools.scheduledMsgCache.delete(originalMsg.id);
SLTools.unscheduledMsgCache.add(originalMsg.id);
// Check if original message has x-send-later headers
if (originalMsg.headers.hasOwnProperty("x-send-later-at")) {
let { preferences, scheduleCache } = await messenger.storage.local.get(
{ preferences: {}, scheduleCache: {} }
);
// Re-save message (drops x-send-later headers by default
// because they are not loaded when editing as draft).
messenger.compose.saveMessage(tab.id, {mode: "draft"}).then(
() => {
SLTools.scheduledMsgCache.delete(originalMsg.id);
SLTools.unscheduledMsgCache.add(originalMsg.id);
SendLater.updateStatusIndicator();
}
);
The originalMessage
is not a MessageHeader but a MessagePart. It does not have an id
member, so the four lines manipulating the MsgCache do not seem to do anything useful.
Why is the manipulation repeated after the message has been saved? That seems not necessary, is it?
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 updated the PR and now use the correct msgId and removed the superfluous update of the *scheduledMsgCache maps.
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 think it would improve performance slightly to put back the code that updates the two caches after the message is saved, but using the correct id for the newly saved draft rather than the old id. I don't think it hurts too much not to do it -- Send Later will just pick up that draft in the next scan and add it to the cache in possiblySendMessage
.
I know this is probably out-of-scope for this PR, but it's silly that there are two different caches. There should be just be one cache, a JavaScript dictionary whose keys are message IDs and values are either true for scheduled or false for unscheduled.
Also, as far as I can tell, right now messages that have been deleted aren't ever being removed from the caches, so that's a memory leak if TB is kept running for a long time.
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 really do not understand that piece of code. This is the current version:
SLTools.scheduledMsgCache.delete(originalMsg.id);
SLTools.unscheduledMsgCache.add(originalMsg.id);
// Check if original message has x-send-later headers
if (originalMsg.headers.hasOwnProperty("x-send-later-at")) {
let { preferences, scheduleCache } = await messenger.storage.local.get({ preferences: {}, scheduleCache: {} });
// Re-save message (drops x-send-later headers by default
// because they are not loaded when editing as draft).
messenger.compose.saveMessage(tab.id, {mode: "draft"}).then(
() => {
SLTools.scheduledMsgCache.delete(originalMsg.id);
SLTools.unscheduledMsgCache.add(originalMsg.id);
SendLater.updateStatusIndicator();
}
);
So in the first two lines we do:
SLTools.scheduledMsgCache.delete(originalMsg.id);
SLTools.unscheduledMsgCache.add(originalMsg.id);
and in the then() callback we do again:
SLTools.scheduledMsgCache.delete(originalMsg.id);
SLTools.unscheduledMsgCache.add(originalMsg.id);
SendLater.updateStatusIndicator();
That is exactly the same, the cache is not reverted or anything in the second step, we just delete the same entry from the scheduledMsgCache
map (which is not there anymore = no op) and we add the same entry to the unscheduledMsgCache
map (which is already there = no op).
It really is not doing anything. Or am I missing something? We definitely should elaborate, what these caches are used for and try to simplify them.
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.
@jperryhouts : Do you have time to help us understand what these caches are for?
SLStatic.debug( | ||
"Comparing", rcvdHdr, "to", draftHdr, | ||
inReplyTo, "?=", `<${draftHdr.headerMessageId}>` | ||
let rcvdMsg = await messenger.messages.getFull(rcvdHdr.id); |
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.
Is there a fear, that the call to getFull() could block the loop? Can we then move the entire body of the loop into an async function, which for each message is properly awaited and throw the returned Promise into an Promise.all()
?
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.
@jph-sendlater would have to comment on why he wrote the code the way he did, and @jobisoft would know better than I whether it's possible for messenger.messages.getFull
to block, but to the extent that I understand what's going on here, I think the change you propose seems reasonable.
One caveat: if getFull
could potentially trigger message downloading from the server, then is it a good idea to set off a bunch of those in parallel? Seems like it could cause a spike in network traffic, and also, in my experience, TB sometimes gets a bit unhappy when a bunch of things are all trying to download messages from an IMAP server at the same time. It might therefore be safer to keep the current behavior of processing the messages one by one instead of sticking them all in separate promises and processing them in parallel?
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 is the opposite. The original code is firing off all operations in the for loop in parallel. My change sequentialized the first getFull()
request. What I have not yet sequentialized is the call to SLTools.forAllDrafts()
, that is still scheduled for later async execution without waiting for each to have been finished before the next one is started.
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 now await the call to SLTools.forAllDrafts()
below as well.
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.
Waiting on feedback from @jph-sendlater for his opinion on the method, before resolving this conversation. I personally feel it is safer to have all run sequentially.
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 still calls with a missing await:
messenger.storage.local.set
messenger.compose.sendMessage
messenger.composeAction.openPopup
messenger.composeAction.setPopup
messenger.browserAction.setBadgeText
messenger.browserAction.enable
messenger.messageDisplayAction.setBadgeText
SendLater.updateStatusIndicator
SendLater.setQuitNotificationsEnabled
- …
As one consequence, SLStatic.nofail
must be made async
and await
ed in SendLater.disable
and there are still some calls left in the mainLoop
where that needs to be await
ed.
While there should be enough time for all calls to finish between two cycles (60 seconds), it's cleaner to await
all calls where necessary to avoid unwanted side effects.
@arndissler I don't see this used anywhere in the code. Did you mean messenger.messageDisplayAction.enable? |
Fix a logic error in the loop intended to interate over all drafts. The logic error was causing the loop to only examine the first page of results from `messenger.messages.list` and therefore only the first 100 drafts in each drafts folder. Fixes #529.
e8fcf29
to
0f7daa3
Compare
@arndissler made a bunch more changes. Please take me a look and let me know if you think this is good to merge. |
0f7daa3
to
5cbfc18
Compare
When disabling the add-on because it failed to initialize properly, instead of just firing off a bunch of promises which will execute in random order based on whenever the JS engine decides to fulfill each one, execute them all sequentially. This eliminates a bunch of unnecessary nondeterminism which could cause intermittent issues we would need to debug, and it also means we will know reliably that the disable steps, some of which may depend on earlier steps, happen in order.
5cbfc18
to
1127416
Compare
Send Later allows other extensions to get and set its preferences via the `getPreferences` and `setPreferences` actions. Unfortunately, the former was broken because it was attempting to use the JavaScript `filter` operator on on Object when it only works on Arrays. This is now fixed.
After converting the compose action click handler to an async function, the first time the compose action button is clicked it doesn't work, and an error is logged that the popup can only be opened from a user input handler, which is weird because it _is_ being opened from a user input handler. Having said that, simplifying the code by not unnecessarily setting and clearing the popup file makes this problem go away, and this change doesn't hurt anything else, so let's go with that.
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.
Great work so far – two glitches were excellently spotted and fixed (the missing last page for the message lists and the .filter
on object props).
There are still some calls that needs to be await
'ed:
- all calls to
messenger.quitter.*
- one call to
messenger.messages.update
messenger.SL3U.setLogConsoleLevel
messenger.SL3U.setLegacyPref
messenger.SL3U.getLegacyPref
messenger.SL3U.setDispositionState
While the implementation of messenger.columnHandler.setPreference
is marked as async
, it doesn't contain async code and are not await
'ed anywhere. This can cause side effects, as APIs are "promisified", AFAIK, but this should be okay-ish for this method, as it only sets some lookup prefs in a Map
.
1) I don't understand why there are two custom column handlers in the experiments directory, but it appears at some point the column handling was switched from customColumn* to legacyColumn*, which changed the argument types for `messenger.columnHandler.setColumnVisible`, but the code in `background.js` wasn't updated to reflect that change. Now it has been, so the Send Later column is now properly enabled and disabled. 2) If the Drafts folder is the currently displayed folder when Thunderbird starts up, the Send Later column is now properly enabled and made visible if it's supposed to be.
Remove an unnecessary console debug log message that's two verbose. Change several console log calls to SLStatic log calls so they'll get filtered by the configured log level.
You can't add the header row update listener before you've initialized the expanded header row!
@arndissler did more cleanup, fixed more bugs, take another look |
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.
While the latest changes are a good step forward, there are still multiple calls to async
functions or functions that return promises, that are not await'ed and can cause unforeseeable side effects.
While not await
ing calls to messenger.window.close
, messenger.runtime.openDefaultBrowser
and similar, I think it's crucial to await
calls to (and not limited to):
While there's no evidence, it's possible that the non-awaited calls cause errors that we haven't spotted yet.
I'm going to merge this and fix those on main. |
This tries to deal with the symptoms described on #554.
We need to make sure that
.then()
callbacks do not spin into their own thread and are not part of the expected execution flow.It is valid and sometimes more readable syntax to mix await and .
then()
and I did not remove all of them, but the Promise chain needs to be awaited. I did remove some wrongly nested chains.I added comments, where I think code could be improved further.