Skip to content
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

Merged
merged 11 commits into from
Aug 6, 2018
Merged

Improve email sending #6154

merged 11 commits into from
Aug 6, 2018

Conversation

zivkan
Copy link
Member

@zivkan 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
Copy link
Contributor

@scottbommarito scottbommarito left a 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 (;;)
Copy link
Contributor

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)

Copy link
Member Author

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.

Copy link
Contributor

@shishirx34 shishirx34 left a 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 (;;)
Copy link
Contributor

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);
}
Copy link
Contributor

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...

Copy link
Contributor

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)

Copy link
Member Author

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.

Copy link
Contributor

@loic-sharma loic-sharma Jul 12, 2018

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.

Copy link
Contributor

@loic-sharma loic-sharma left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!

@loic-sharma loic-sharma self-assigned this Jul 12, 2018
{
// 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(() =>
Copy link
Contributor

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++;
Copy link
Contributor

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.

@loic-sharma
Copy link
Contributor

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.

@zivkan
Copy link
Member Author

zivkan commented Aug 1, 2018

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);
}
Copy link
Contributor

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);

Copy link
Contributor

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,
Copy link
Contributor

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);

Copy link
Contributor

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);
Copy link
Contributor

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));
}
Copy link
Contributor

@loic-sharma loic-sharma Aug 3, 2018

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 :(

Copy link
Contributor

@loic-sharma loic-sharma left a 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! 👍

@zivkan zivkan merged commit 2616a99 into dev Aug 6, 2018
@zivkan zivkan deleted the zivkan-retry-smtp-failures branch August 6, 2018 21:05
zivkan added a commit that referenced this pull request Aug 9, 2018
zivkan added a commit that referenced this pull request Aug 9, 2018
* Revert "Don't throw when SmtpUri is not set (#6278)"

This reverts commit e60e75f.

* Revert "Fix logging exceptions from background thread (#6273)"

This reverts commit d1610bb.

* Revert "Improve email sending (#6154)"

This reverts commit 2616a99.
zivkan added a commit that referenced this pull request Aug 10, 2018
* Revert "Don't throw when SmtpUri is not set (#6278)"

This reverts commit e60e75f.

* Revert "Fix logging exceptions from background thread (#6273)"

This reverts commit d1610bb.

* Revert "Improve email sending (#6154)"

This reverts commit 2616a99.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants