Tim_Mac
Tim_Mac

Reputation: 166

Hangfire Clarification on SlidingInvisibilityTimeout

the hangfire docs state:

One of the main disadvantage of raw SQL Server job storage implementation – it uses the polling technique to fetch new jobs. Starting from Hangfire 1.7.0 it’s possible to use TimeSpan.Zero as a polling interval, when SlidingInvisibilityTimeout option is set.

and i'm using these SqlServerStorageOptions as suggested:

var options = new SqlServerStorageOptions
{
    SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
    QueuePollInterval = TimeSpan.Zero
};

Nowhere does it say what the SlidingInvisibilityTimeout actually means, can anyone clarify?
My scenario is that i have approx 1000 emails to send every morning and i've been tripping the Office365 throttling limitations of 30 messages per minute and getting rejected, so i'm using Hangfire to queue them up in a single workerthread, and adding a 2 second Thread.Sleep at the end of each task. this is working great but i'm getting increased CPU usage of about 20% caused by hangfire (as reported here) and it's causing frequent timeouts when the server is busy.

the behaviour i'm trying to achieve is:

  1. at the end of each job, check is there another one straight away and take the next task.
  2. if there are no jobs in the queue, check back in 5 minutes and don't touch the SQL server until then.

Thanks for any assistance.

Upvotes: 10

Views: 4830

Answers (1)

Tim_Mac
Tim_Mac

Reputation: 166

in the end i wrote a lightweight alternative approach using a thread in a while loop to monitor a folder for text files containing a serialized object with the email parameters. it has almost no overhead and complies with the office365 throttling policy. posting here in case it is of use to anyone else, built for asp.net and should be easy to adapt for other scenarios.

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Web.Hosting;

namespace Whatever
{
    public class EmailerThread
    {        
        public delegate void Worker();
        private static Thread worker;   // one worker thread for this application  // https://stackoverflow.com/questions/1824933/right-way-to-create-thread-in-asp-net-web-application
        public static string emailFolder;
        public static int ScanIntervalMS = 2000;     // office365 allows for 30 messages in a 60 second window, a 2 second delay plus the processing time required to connect & send each message should safely avoid the throttling restrictions.

        /// <summary>
        /// Must be invoked from Application_Start to ensure the thread is always running, if the applicationpool recycles etc
        /// </summary>
        public static void Init()
        {
            // create the folder used to store serialized files for each email to be sent
            emailFolder = Path.Combine(HostingEnvironment.ApplicationPhysicalPath, "App_Data", "_EmailOutbox");
            Directory.CreateDirectory(emailFolder);

            worker = new Thread(new ThreadStart(new Worker(ScanForEmails)));
            worker.Start();
        }

        /// <summary>
        /// Serialize an object containing all the email parameters to a text file
        /// Call this object 
        /// </summary>
        /// <param name="e"></param>
        public static void QueueEmail(EmailParametersContainer e)
        {
            string filename = Guid.NewGuid().ToString() + ".txt";
            File.WriteAllText(Path.Combine(emailFolder, filename), JsonConvert.SerializeObject(e));
        }

        public static void ScanForEmails()
        {
            var client = new System.Net.Mail.SmtpClient(Settings.SmtpServer, 587);
            client.EnableSsl = true;
            client.UseDefaultCredentials = false;
            client.DeliveryMethod = SmtpDeliveryMethod.Network;
            client.Credentials = new System.Net.NetworkCredential(Settings.smtpUser, Settings.smtpPass);
            client.Timeout = 5 * 60 * 1000;    // 5 minutes

            // infinite loop to keep scanning for files
            while (true)
            {
                // take the oldest file in the folder and process it for sending
                var nextFile = new DirectoryInfo(emailFolder).GetFiles("*.txt", SearchOption.TopDirectoryOnly).OrderBy(z => z.CreationTime).FirstOrDefault();
                if (nextFile != null)
                {
                    // deserialize the file
                    EmailParametersContainer e = JsonConvert.DeserializeObject<EmailParametersContainer>(File.ReadAllText(nextFile.FullName));
                    if (e != null)
                    {
                        try
                        {
                            MailMessage msg = new MailMessage();
                            AddEmailRecipients(msg, e.To, e.CC, e.BCC);
                            msg.From = new MailAddress(smtpUser);
                            msg.Subject = e.Subject;
                            msg.IsBodyHtml = e.HtmlFormat;
                            msg.Body = e.MessageText;

                            if (e.FilePaths != null && e.FilePaths.Count > 0)
                                foreach (string file in e.FilePaths)
                                    if (!String.IsNullOrEmpty(file) && File.Exists(file))
                                        msg.Attachments.Add(new Attachment(file));

                            client.Send(msg);
                            msg.Dispose();

                            // delete the text file now that the job has successfully completed
                            nextFile.Delete();
                        }
                        catch (Exception ex)
                        {
                            // Log the error however suits...

                            // rename the .txt file to a .fail file so that it stays in the folder but will not keep trying to send a problematic email (e.g. bad recipients or attachment size rejected)
                            nextFile.MoveTo(nextFile.FullName.Replace(".txt", ".fail"));
                        }
                    }
                }
                Thread.Sleep(ScanIntervalMS);   // wait for the required time before looking for another job
            }            
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="msg"></param>
        /// <param name="Recipients">Separated by ; or , or \n or space</param>
        public static void AddEmailRecipients(MailMessage msg, string To, string CC, string BCC)
        {
            string[] list;
            if (!String.IsNullOrEmpty(To))
            {
                list = To.Split(";, \n".ToCharArray());
                foreach (string email in list)
                    if (email.Trim() != "" && ValidateEmail(email.Trim()))
                        msg.To.Add(new MailAddress(email.Trim()));
            }
            if (!String.IsNullOrEmpty(CC))
            {
                list = CC.Split(";, \n".ToCharArray());
                foreach (string email in list)
                    if (email.Trim() != "" && ValidateEmail(email.Trim()))
                        msg.CC.Add(new MailAddress(email.Trim()));
            }
            if (!String.IsNullOrEmpty(BCC))
            {
                list = BCC.Split(";, \n".ToCharArray());
                foreach (string email in list)
                    if (email.Trim() != "" && ValidateEmail(email.Trim()))
                        msg.Bcc.Add(new MailAddress(email.Trim()));
            }
        }


        public static bool ValidateEmail(string email)
        {
            if (email.Contains(" ")) { return false; }

            try
            {
                // rely on the .Net framework to validate the email address, rather than attempting some crazy regex
                var m = new MailAddress(email);
                return true;                
            }
            catch
            {
                return false;
            }
        }
    }

    public class EmailParametersContainer
    {
        public string To { get; set; }
        public string Cc { get; set; }
        public string Bcc { get; set; }
        public string Subject { get; set; }
        public string MessageText { get; set; }
        public List<string> FilePaths { get; set; }
        public bool HtmlFormat { get; set; }
    }
}

Upvotes: 0

Related Questions