关于 SlidingInvisibilityTimeout 的 Hangfire 澄清

Tim*_*Mac 8 hangfire

hangfire 文档指出:

原始 SQL Server 作业存储实现的主要缺点之一——它使用轮询技术来获取新作业。从 Hangfire 1.7.0 开始,当设置了 SlidingInvisibilityTimeout 选项时,可以使用 TimeSpan.Zero 作为轮询间隔。

我按照建议使用这些 SqlServerStorageOptions:

var options = new SqlServerStorageOptions
{
    SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
    QueuePollInterval = TimeSpan.Zero
};
Run Code Online (Sandbox Code Playgroud)

它没有说明 SlidingInvisibilityTimeout 的实际含义,谁能澄清一下?
我的情况是,我每天早上有大约 1000 封电子邮件要发送,而且我一直在突破每分钟 30 封邮件的 Office365 限制并被拒绝,所以我使用 Hangfire 将它们排在一个工作线程中,并添加了一个每个任务结束时 2 秒 Thread.Sleep。这很好用,但我的 CPU 使用率增加了大约 20% 由 hangfire 引起(如此处报告的那样),并且在服务器繁忙时导致频繁超时。

我试图实现的行为是:

  1. 在每项工作结束时,立即检查是否还有另一项工作并进行下一项任务。
  2. 如果队列中没有作业,请在 5 分钟后回来检查,直到那时才接触 SQL 服务器。

感谢您的帮助。

Tim*_*Mac 0

最后,我编写了一种轻量级替代方法,使用 while 循环中的线程来监视文件夹中是否有包含带有电子邮件参数的序列化对象的文本文件。它几乎没有任何开销,并且符合office365的限制策略。发布在这里,以防对其他人有用,它是为 ASP.NET 构建的,并且应该很容易适应其他场景。

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  // /sf/ask/127745341/
        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; }
    }
}
Run Code Online (Sandbox Code Playgroud)