-
Notifications
You must be signed in to change notification settings - Fork 645
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
Improve email sending #6154
Improve email sending #6154
Conversation
zivkan
commented
Jul 10, 2018
- Retry failed sends, to improve reliability
- Send App Insights telemetry, so we have visibility into the extent of the problem (plus the quantity of messages sent)
- NuGetGallery to send emails in the background, so slow/retried emails don't slow HTTP responses
* Retry failed sends, to improve reliability * Send App Insights telemetry, so we have visibility into the extent of the problem (plus the quantity of messages sent) * NuGetGallery to send emails in the background, so slow/retried emails don't slow HTTP responses
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.
Looks great, welcome to the team!
{ | ||
SendMessage(mailMessage, copySender: false); | ||
int attempt = 0; | ||
for (;;) |
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 sure exactly what this means. I assume it means "keep looping infinitely".
I'd rather see while (true)
than this. Even better, I'd prefer to see this loop rewritten with a clearer condition, e.g. while (attempt < RetryDelays.Count)
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.
Yes, it's an infinite loop. You're right, while (true)
is easier to read. I guess some habits die hard.
I'll change the loop to while (!success)
. The issue with looping until the retry count is exceeded, is that if we want to rethrow the exception and keep the stack trace, we need to keep the exception in a variable, then outside the exception handler do ExceptionDispatchInfo.Capture(exception).Throw()
. I think it's much cleaner to just throw;
in the exception handler.
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.
Looks good.
{ | ||
SendMessage(mailMessage, copySender: false); | ||
int attempt = 0; | ||
for (;;) |
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.
we don't really use much infinite loops, but I think a couple of places where we do use while (true)
, may be use the same for consistency?
// Log but swallow the exception | ||
QuietLog.LogHandledException(ex); | ||
sw.Stop(); | ||
telemetryClient.TrackDependency("SMTP", smtpUri, "SendMessage", null, now, sw.Elapsed, null, success); | ||
} |
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 pattern we use for this kind of thing is a TrackMetric
, through some sort of descriptive wrapper method. For example: https://github.com/NuGet/NuGet.Jobs/blob/master/src/Validation.PackageSigning.ProcessSignature/Telemetry/TelemetryService.cs#L61
Also, this out of scope for this PR, but we really should merge the Gallery's and ServerCommon's telemetry APIs...
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.
Also, it may be worth tracking whether the operation was a success or not so that we measure how good/bad our mail system is.
In reply to: 201454393 [](ancestors = 201454393)
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 wanted to use TrackDependency for a few reasons.
- I wanted to have an event which told us if it was successful or failed. Keeping the duration is just a side-effect of using the dependency event type
- AI's Smart Monitoring can automatically alert us when failed dependencies increase. Using metrics we would have to create custom alerts, assuming we use App Insights for alerting. Perhaps if we export App Insights logs to something else and do alerting there, using metrics doesn't lose this advantage.
- Dependencies are shown in AI's Application Map tab. If all dependencies are recorded in AI, then the application map is an auto-generated, self-updating, service architecture diagram, which is great for newbies like me 😄 (side note, we must be using an old App Insights SDK, because newer ones automatically capture outgoing HTTP calls, including Blob Storage, but ours does not)
- Sending emails to a SMTP server really is a service dependency, and not an application specific metric. Therefore, using the dependency event type, rather than metric, seems more semantically correct.
I'm happy to change it to a metric, but those are the reasons I added TrackDependency
to ITelemetryClient
. Let me know what you think.
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.
Those are very good points! It sounds like TrackDependency
is indeed the right pattern here.
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.
Looks great!
{ | ||
// Send email as background task, as we don't want to delay the HTTP response. | ||
// Particularly when sending email fails and needs to be retried with a delay. | ||
Task.Run(() => |
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 have a concerth that we might run into thread pool depletion if we try to send a lot of email while SMTP server is unresponsive and causes retries. base.SendMessage
uses Thread.Sleep
which blocks thread preventing other tasks from running on it (including processing the web requests? not sure if asp.net uses the same thread pool as Task.Run
for request processing).
if (attempt < RetryDelays.Count) | ||
{ | ||
Thread.Sleep(RetryDelays[attempt]); | ||
attempt++; |
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.
We probably need to track the amount of retries we do.
Hey @zivkan, it looks like we should make the message service asynchronous to avoid blocking threads during retries. I believe that converting and then testing the code may take 1-2 days. After discussing it with @skofman1, we think it'd be best if I hand off this work back to you for when you return. |
I pushed changes to address all the comments. Please have another look. Thanks. |
…ures # Conflicts: # src/NuGetGallery.Core/Services/CoreMessageService.cs # src/NuGetGallery.Core/Services/ICoreMessageService.cs # src/NuGetGallery/Services/IMessageService.cs # tests/NuGetGallery.Facts/Controllers/ApiControllerFacts.cs # tests/NuGetGallery.Facts/Controllers/PackagesControllerFacts.cs
@@ -57,13 +57,13 @@ protected override void SendNewAccountEmail(User account) | |||
{ | |||
var confirmationUrl = Url.ConfirmOrganizationEmail(account.Username, account.EmailConfirmationToken, relativeUrl: false); | |||
|
|||
MessageService.SendNewAccountEmail(account, confirmationUrl); | |||
MessageService.SendNewAccountEmailAsync(account, confirmationUrl); | |||
} |
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 you await this? Same with line 66.
@@ -892,7 +892,7 @@ private void NotifyReportMyPackageSupportRequest(ReportMyPackageViewModel report | |||
CopySender = reportForm.CopySender | |||
}; | |||
|
|||
_messageService.ReportMyPackage(request); | |||
_messageService.ReportMyPackageAsync(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.
Is this missing an await?
@@ -989,7 +989,7 @@ public virtual ActionResult ContactOwners(string id, string version, ContactOwne | |||
|
|||
var user = GetCurrentUser(); | |||
var fromAddress = new MailAddress(user.EmailAddress, user.Username); | |||
_messageService.SendContactOwnersMessage( | |||
_messageService.SendContactOwnersMessageAsync( | |||
fromAddress, |
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 this missing an await?
@@ -1065,7 +1065,7 @@ private ActionResult SendPasswordResetEmail(User user, bool forgotPassword) | |||
user.PasswordResetToken, | |||
forgotPassword, | |||
relativeUrl: false); | |||
MessageService.SendPasswordResetInstructions(user, resetPasswordUrl, forgotPassword); | |||
MessageService.SendPasswordResetInstructionsAsync(user, resetPasswordUrl, forgotPassword); | |||
|
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.
Should this be awaited?
@@ -74,13 +74,13 @@ protected override void SendNewAccountEmail(User account) | |||
{ | |||
var confirmationUrl = Url.ConfirmEmail(account.Username, account.EmailConfirmationToken, relativeUrl: false); | |||
|
|||
MessageService.SendNewAccountEmail(account, confirmationUrl); | |||
MessageService.SendNewAccountEmailAsync(account, confirmationUrl); |
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.
SendNewAccountEmailAsync [](start = 27, length = 24)
Should this line and line 83 be awaited?
@@ -1424,7 +1424,7 @@ private void SendAddPackageOwnerNotification(PackageRegistration package, User n | |||
|
|||
// Notify existing owners | |||
var notNewOwners = package.Owners.Where(notNewOwner).ToList(); | |||
notNewOwners.ForEach(owner => _messageService.SendPackageOwnerAddedNotice(owner, newOwner, package, packageUrl)); | |||
notNewOwners.ForEach(owner => _messageService.SendPackageOwnerAddedNoticeAsync(owner, newOwner, package, packageUrl)); | |||
} |
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 think this works with async methods :(
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.
Did another pass, looks great! 👍