System.Timers.Timer 的问题。偶尔触发不止一次

Posted

技术标签:

【中文标题】System.Timers.Timer 的问题。偶尔触发不止一次【英文标题】:Problems with System.Timers.Timer. Firing more than once occasionally 【发布时间】:2017-12-23 18:13:58 【问题描述】:

我每天使用一次 System.Timers.Timer 备份我的 SQL Server Express 数据库。大多数时候,它似乎工作正常。有时,ElapsedEventHandler 会以 1 或 4 分钟的间隔多次调用。它应该每天打一次。我将 AutoReset 设置为 false,并在 ElapsedEventHandler 结束时调用 Start。此外,可能相关的是我确实重新计算了间隔,以便计时器总是在接近凌晨 1 点时关闭。尽可能。备份数据库可能需要几分钟,如果我不更改间隔,时间可能会漂移到无法接受的程度。我提到这一点是因为这些链接表明重置间隔可能存在问题:

Thread-safety of System.Timers.Timer vs System.Threading.Timer Multiple timer elapsed issue

请特别查看 Hans Passant 的答案

但是,我不知道如何避免重置间隔。另外,我查看了 System.Timers.Timer 的代码。似乎只是重置间隔不会再次启动计时器。我不反对使用不同的计时器(System.Threading.Timer?),但我想先知道发生了什么。

我已经粘贴了下面的所有代码。我认为真正相关的部分是方法:DatabaseBackupTimerOnElapsed

最后,我会提到程序有时会停止并重新启动(如果代码的其他部分存在未捕获的异常)。我会假设所有计时器在退出程序时都会被杀死,即使没有调用 Dispose ?那就是定时器不存在于操作系统中?

编辑 我被要求写一个小的、完整的、可验证的例子。我在这里这样做。我保留了完整的示例,因为有人可能会声称(非常正确!)我取出了一个重要的细节。我已经运行了这段代码并没有发现问题,但是,它只是在原始代码中偶尔发生。

public class DatabaseCleanupManager 

    private const int MaxRetries = 5;
    private const int DatabaseBackupHourOneAm = 1;


    private Timer _databaseBackupTimer;

    public DatabaseCleanupManager()
     

    public void Initialize()
    
        Console.WriteLine("Initialize");
        TimeSpan spanTimer = GetDBBackupTimeSpan(1);

        _databaseBackupTimer = new Timer(spanTimer.TotalMilliseconds)
        
            AutoReset = false,
        ;

        _databaseBackupTimer.Elapsed += DatabaseBackupTimerOnElapsed;
        _databaseBackupTimer.Start();
    

    private TimeSpan GetDBBackupTimeSpan(int databaseBackupFrequencyInDays)
    
        Console.WriteLine("GetDBBackupTimeSpan");
        DateTime dt1 = DateTime.Now;
        DateTime dt2 = new DateTime(dt1.Year, dt1.Month,
            dt1.Day, 1, 0, 0);
        // I'm really interested in a timer once a day.  I'm just trying to get it to happen quicker!
        //dt2 = dt2.AddDays(databaseBackupFrequencyInDays);
        dt2 = dt1.AddMinutes(4);

        TimeSpan spanTimer = dt2 - dt1;

        if (spanTimer.TotalMilliseconds < 0) // This could conceivably happen if the have 0 or a negative number (erroneously) for DatabaseBackupFrequencyInDays 
        
            dt2 = new DateTime(dt1.Year, dt1.Month,
                dt1.Day, 1, 0, 0);
            //dt2 = dt2.AddDays(databaseBackupFrequencyInDays);
            dt2 = dt1.AddMinutes(4);
            spanTimer = dt2 - dt1;
        
        return spanTimer;
    
    public void PerformDatabaseMaintenance()
    
        if (BackupCurrentDatabase())
        
            var success = CleanupExpiredData();

            if (success)
            
                Console.WriteLine("Database Maintenance Finished");
            
        

    

    public void Dispose()
    
        _databaseBackupTimer.Elapsed -= DatabaseBackupTimerOnElapsed;
        _databaseBackupTimer.Stop();
        _databaseBackupTimer.Dispose();
    

    private void DatabaseBackupTimerOnElapsed(object sender, ElapsedEventArgs elapsedEventArgs)
    
        try
        
            Console.WriteLine("DatabaseBackupTimerOnElapsed at: " + DateTime.Now);
            PerformDatabaseMaintenance();

            TimeSpan spanTimer = GetDBBackupTimeSpan(1);

            // NOTICE I'm calculating Interval again.  Some posts suggested that this restarts timer
            _databaseBackupTimer.Interval = Math.Max(spanTimer.TotalMilliseconds, TimeSpan.FromMinutes(1).TotalMilliseconds);
            _databaseBackupTimer.Start();

        
        catch (Exception )
        
            // something went wrong - log problem and start timer again.
            _databaseBackupTimer.Start();
        

    

    private bool BackupCurrentDatabase()
    
        // actually backup database but here I'll just sleep for 1 minute...
        Thread.Sleep(1000);
        Console.WriteLine("Backed up DB at: " + DateTime.Now);
        return true;
    

    private bool CleanupExpiredData()
    
        // Actually remove old SQL Server Express DB .bak files but here just sleep
        Thread.Sleep(1000);
        Console.WriteLine("Cleaned up old .Bak files at: " + DateTime.Now);
        return true;
    




 class Program

    static void Main(string[] args)
    
        DatabaseCleanupManager mgr = new DatabaseCleanupManager();
        mgr.Initialize();

        // here we'd normally be running other threads etc., but for here...
        Thread.Sleep(24*60*60*1000);  // sleep for 1 day
    

结束编辑

public class DatabaseCleanupManager : IDatabaseCleanupManager

    private const int MaxRetries = 5;
    private const int DatabaseBackupHourOneAm = 1;

    private readonly ISystemConfiguration _systemConfiguration;
    private readonly IPopsicleRepository _repository;
    private readonly ISystemErrorFactory _systemErrorFactory;
    private readonly IAuthorizationManager _authorizationManager;
    private readonly IReportRobotState _robotStateReporter;
    private Timer _databaseBackupTimer;

    public DatabaseCleanupManager(
        IPopsicleRepository repository,
        ISystemConfiguration configuration,
        ISystemErrorFactory systemErrorFactory,
        IAuthorizationManager authorizationManager,
        IReportRobotState robotStateReporter)
    
        if (repository == null)
            throw new ArgumentNullException("repository");

        if (configuration == null)
            throw new ArgumentNullException("configuration");

        if (systemErrorFactory == null)
            throw new ArgumentNullException("systemErrorFactory");

        if (authorizationManager == null)
            throw new ArgumentNullException("authorizationManager");

        if (robotStateReporter == null)
            throw new ArgumentNullException("robotStateReporter");

        _repository = repository;
        _systemConfiguration = configuration;
        _systemErrorFactory = systemErrorFactory;
        _authorizationManager = authorizationManager;
        _robotStateReporter = robotStateReporter;
    

    public event EventHandler<SystemErrorEventArgs> SystemError;

    public event EventHandler<SystemErrorClearedEventArgs> SystemErrorCleared;


    public void Initialize()
    

        TimeSpan spanTimer = GetDBBackupTimeSpan(_systemConfiguration.DatabaseBackupFrequencyInDays);

        _databaseBackupTimer = new Timer(spanTimer.TotalMilliseconds)
        
            AutoReset = false,
        ;

        _databaseBackupTimer.Elapsed += DatabaseBackupTimerOnElapsed;
        _databaseBackupTimer.Start();
    

    private TimeSpan GetDBBackupTimeSpan(int databaseBackupFrequencyInDays)
    
        DateTime dt1 = DateTime.Now;
        DateTime dt2 = new DateTime(dt1.Year, dt1.Month,
            dt1.Day, 1, 0, 0);
        dt2 = dt2.AddDays(_systemConfiguration.DatabaseBackupFrequencyInDays);

        TimeSpan spanTimer = dt2 - dt1;

        if (spanTimer.TotalMilliseconds < 0) // This could conceivably happen if the have 0 or a negative number (erroneously) for DatabaseBackupFrequencyInDays in configuration.json
        
            dt2 = new DateTime(dt1.Year, dt1.Month,
                dt1.Day, 1, 0, 0);
            dt2 = dt2.AddDays(1);
            spanTimer = dt2 - dt1;
        
        return spanTimer;
    
    public void PerformDatabaseMaintenance()
    
        if (BackupCurrentDatabase())
        
            var success = CleanupExpiredData();

            if (success)
            
                Logger.Log(LogLevel.Info, string.Format("Database Maintenance succeeded"));
                NotifySystemError(ErrorLevel.Log, ErrorCode.DatabaseBackupComplete, "Database backup completed");
            
        

    

    public void Dispose()
    
        _databaseBackupTimer.Elapsed -= DatabaseBackupTimerOnElapsed;
        _databaseBackupTimer.Stop();
        _databaseBackupTimer.Dispose();
    

    private void DatabaseBackupTimerOnElapsed(object sender, ElapsedEventArgs elapsedEventArgs)
    

        try
        

            PerformDatabaseMaintenance();

            TimeSpan spanTimer = GetDBBackupTimeSpan(_systemConfiguration.DatabaseBackupFrequencyInDays);

            _databaseBackupTimer.Interval = Math.Max(spanTimer.TotalMilliseconds, TimeSpan.FromMinutes(10).TotalMilliseconds);
            _databaseBackupTimer.Start();

        
        catch (Exception e)
        
            Logger.Log(LogLevel.Warning,
                string.Format("Database Backup Failed: 0, ",
                    e.Message));
            NotifySystemError(ErrorLevel.Log, ErrorCode.DatabaseBackupFailed,
                "Database backup failed ");

            _databaseBackupTimer.Start();
        


    



    private bool BackupCurrentDatabase()
    
        try
        
            _repository.Alerts.Count();
        
        catch (Exception ex)
        
            NotifySystemError(ErrorLevel.Log, ErrorCode.DatabaseBackupFailed, "Database backup failed - the database server does not respond or the database does not exist");
            throw new InvalidOperationException(string.Format("The DB does not exist : 0 Error 1", _systemConfiguration.LocalDbPath, ex.Message));
        

        if (!Directory.Exists(_systemConfiguration.LocalBackupFolderPath))
            Directory.CreateDirectory(_systemConfiguration.LocalBackupFolderPath);

        var tries = 0;
        var success = false;

        while (!success && tries < MaxRetries)
        
            try
            
                _repository.BackupDatabase(_systemConfiguration.LocalBackupFolderPath);
                success = true;
            
            catch (Exception e)
            
                Logger.Log(LogLevel.Warning, string.Format("Database Backup Failed: 0, retrying backup", e.Message));
                Thread.Sleep(TimeSpan.FromSeconds(1));
                tries++;

                if (tries == MaxRetries)
                
                    NotifySystemError(ErrorLevel.Log, ErrorCode.DatabaseBackupFailed, string.Format("Database backup failed - 0", e.Message));
                
            
        

        var backupDirectory = new DirectoryInfo(_systemConfiguration.LocalBackupFolderPath);
        var files = backupDirectory.GetFiles().OrderBy(f => f.CreationTime).ToArray();

        if (files.Length > _systemConfiguration.MaxDatabaseBackups)
        
            for (var i = 0; i < (files.Length - _systemConfiguration.MaxDatabaseBackups); i++)
            
                try
                
                    files[i].Delete();
                
                catch (Exception e)
                
                    Logger.Log(LogLevel.Warning, string.Format("Failed to delete old backup: 0", e.Message));
                
            
        

        Logger.Log(LogLevel.Info, success ?
            "Database Backup succeeded" :
            string.Format("Database Backup failed after 0 retries", MaxRetries));

        return success;
    

    private bool CleanupExpiredData()
    
        var success = false;
        try
        
            var expirationTime = DateTime.Now - TimeSpan.FromDays(_systemConfiguration.DatabaseDataExpirationInDays);
            _repository.DeleteTemperatureReadingsBeforeDate(expirationTime);
            _repository.DeleteTransactionsBeforeDate(expirationTime);
            success = true;
        
        catch (Exception e)
        
            Logger.Log(LogLevel.Warning, string.Format("Failed to cleanup expired data: 0", e.Message));
            NotifySystemError(ErrorLevel.Log, ErrorCode.DatabaseBackupFailed, string.Format("Database cleanup of expired data failed - 0", e.Message));
        

        Logger.Log(LogLevel.Info, success ?
            string.Format("Database clean up expired data succeeded") :
            string.Format("Database clean up expired data failed"));

        return success;
    

    private void NotifySystemError(ErrorLevel errorLevel, ErrorCode errorCode, string description)
    
        var handler = SystemError;
        if (handler != null)
        
            var systemError = _systemErrorFactory.CreateSystemError(errorLevel, errorCode, description);
            handler(this, new SystemErrorEventArgs(systemError));
        
    

【问题讨论】:

请将您的问题编辑成minimal reproducible example。里面有很多不相关的代码 第二。编辑你的问题。这里有太多不相关的代码。 我会编辑下来。我把它全部留下来,因为我认为无关紧要的东西可能不是这样。感谢您的快速反馈。 附注:您从_systemConfiguration 读取了一个值,然后将其传递给GetDBBackupTimeSpan,但您忽略了该参数并再次从_systemConfiguration 读取。 向我们展示调用 public void Initialize() 的代码 【参考方案1】:

我怀疑Initialize 被多次调用,您可以做的是在创建新实例之前检查_databaseBackupTimer 是否为空

如果它不为空,则跳过该方法中的整个代码

【讨论】:

感谢您的快速回复。我会检查一下(使用一些日志语句),但我认为这不是发生的事情,因为 DatabaseCleanupManager 只是在程序启动时实例化一次。 尤里卡!就是这样。每当屏幕保护程序启动、用户注销时以及可能的其他时间时,都会调用 Initialize。为什么这样做是另一个问题,但我非常感谢你发现它!【参考方案2】:

我认为解决方案过于复杂。

    计时器间隔将触发 Elapsed 事件,与上一次运行是否完成无关,除非您在事件期间明确停止计时器。这不应该是必要的。您可以在输入方法时简单地跟踪您是否正在运行。

    将时间间隔设置为 59999。这比一分钟短一毫秒。然后在事件处理程序的入口处检查当前小时和分钟是否对应于您要备份的时间。

    private bool running = false;
    private Timer timer = new Timer();
    //other code.
    private void Initialize()
    
        timer.Interval = 59999;
        myTimer.Elapsed += TimerElapsed;
        timer.Start();
    
    
    public void TimerElapsed(object sender, ElapsedEventArgs e)
    
       if (running) return;
       DateTime dt = DateTime.Now();
    
       if (!(dt.Hour.Equals(1) && dt.Minute.Equals(0))) return;
    
       running = true;
       //other code
       running = false;
    
    

此外,我会将运行备份的时间保存在配置文件或注册表中,这样如果我想更改它,我可以在不重新编译我的服务的情况下这样做。

【讨论】:

以上是关于System.Timers.Timer 的问题。偶尔触发不止一次的主要内容,如果未能解决你的问题,请参考以下文章

System.Timers.Timer 严重不准确

为啥 xmlignore 不能与 System.Timers.Timer 类一起使用

是否可以在 Timer 被处理后生成触发 System.Timers.Timer 的 Elapsed 事件的示例?

System.Timers.Timer 几毫秒后第二次触发

如何在 C# 中创建计时器而不使用 System.Timers.Timer 或 System.Threading.Timer

如何防止 System.Timers.Timer 在线程池上排队执行?