I'm using System.Timers.Timer to backup my SQL Server Express Database once a day. It seems to work fine, most of the time. Occasionally, the ElapsedEventHandler gets called multiple times at 1 or 4 minute intervals. It should just be hit once a day. I have AutoReset as false and I call Start at the end of the ElapsedEventHandler. Also, possibly relevant is that I do recalculate the interval so that the timer always goes off as close to 1 am. as possible. The backing up of the database can take a few minutes, and if I didn't change the interval, the time might drift unacceptably. I mention this because these links suggest there might be a problem with resetting the interval:
See in particular the answers by Hans Passant
However, I don't see how I can avoid resetting the interval. Also, I looked into the code for System.Timers.Timer. It didn't seem like just resetting the interval would Start the timer again. I'm not opposed to using a different timer (System.Threading.Timer?) but I'd like to know what is going on first.
I've pasted all the code below. I would think the truly relevant part is the method: DatabaseBackupTimerOnElapsed
Finally, I'll mention that the program is sometimes stopped and restarted (if there are uncaught exceptions in other parts of the code). I would assume though that all timers are killed at the point of exiting the program even if Dispose is not called? That is Timers don't live on in the operating system?
EDIT I was requested to put down a small, complete, verifiable example. I do so here. I've kept the full example as someone might claim (quite correctly!) that I took out an important detail. I've run this code and have NOT seen the problem but then, it only happens very occasionally with the original code.
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
}
}
END EDIT
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));
}
}
}