cullimorer
cullimorer

Reputation: 755

C# - Timer ticks shorter than specified in the interval causing duplicate jobs to run in parallel

I have a windows service that ticks over every 4 minutes. When the timer ticks if runs a DataImporter, the DataImporter has a number of "Jobs" that it can run on each tick, for example, there is a ProcessData job and a RetreiveData job:

As soon as the DataImporter is run it checks a DB table called ScheduledJob - this has a number of scheduling functionality such as FrequencyInterval, ActiveStart/Stop times, StartedLastRun time. The ScheduledJob table has flag called "InProgress", this flag will stop the DataImport picking up that job when it's already running.

There is a continuous issue where a job is picked up twice, a few seconds apart from each other and then both run simultaneously which cause a number of DB constraints when trying to insert identical records. I am not really sure how it can pick two jobs up at the same time, the tick is 4 minutes apart, so in theory it shouldn't be able to even look at the potential jobs to run, how can it run them both a few seconds apart?

Both the RetrieveData and ProcessData jobs need to be able to run in parallel so I can't pause the Timer whilst I execute the job.

Service:

public partial class DataImport : ServiceBase
{
    private int _eventId = 0;
    readonly Timer _serviceTimer = new Timer(240000);

    public DataImport()
    {
        InitializeComponent();
        ImportServiceEventLog.Source = ServiceSource.DATA_IMPORT_SERVICE.ToString() + Global.ReleaseModeSource(); ;
    }

    protected override void OnStart(string[] args)
    {
        ImportServiceEventLog.WriteEntry(ServiceSource.DATA_IMPORT_SERVICE.ToString() + Global.ReleaseModeSource() + " started", EventLogEntryType.Information, _eventId++);
        _serviceTimer.AutoReset = true;
        ImportServiceEventLog.WriteEntry(ServiceSource.DATA_IMPORT_SERVICE.ToString() + Global.ReleaseModeSource() + " timer interval = " + _serviceTimer.Interval / 1000 + " seconds", EventLogEntryType.Information, _eventId++);
        _serviceTimer.Elapsed += new ElapsedEventHandler(OnTimer);
        _serviceTimer.Start();
    }

    protected override void OnStop()
    {
        ImportServiceEventLog.WriteEntry(ServiceSource.DATA_IMPORT_SERVICE.ToString() + Global.ReleaseModeSource() + " stopped", EventLogEntryType.Information, _eventId++);
    }

    public void OnTimer(object sender, ElapsedEventArgs args)
    {
        try
        {
            Run();
        }
        catch (System.Exception ex)
        {
            ImportServiceEventLog.WriteEntry(ServiceSource.DATA_IMPORT_SERVICE.ToString() + Global.ReleaseModeSource() + " error: " + ex.ToString(), EventLogEntryType.Information, _eventId++);
        }
    }

    public void Run()
    {
        using (var dataImportController = new DataImportController())
        {
            dataImportController.Run();
        }                
    }
}

DataImportController:

public class DataImportController
{
    public void Run()
    {
        // Gets all the jobs from the ScheduledJob table in the DB
        var jobs = GetJobsToRun();

        //Get all Processes (from DB)
        foreach (var job in jobs)
        {
            //Check the time it was last run - do this for each process
            if (RunJob(job))
            {
                _messaging.EventMessage("Process " + job.Name + " finished : " + DateTime.Now, ServiceSource.DATA_IMPORT_SERVICE);
            }
        }
    }

    public bool RunJob(ScheduledJob job)
    {
        // Checks if the job is ready to run, i.e. is the InProgress flag set to false and the interval long enough since the StartedLastRun DateTime
        if (!job.IsReadyToRun())
        {
            return false;
        }

        // Set job to in progress
        job.InProgress = true;
        job.StartedLastRun = DateTime.Now;
        _scheduledJobRepository.Update(job);
        _scheduledJobRepository.SaveChanges();

        try
        {
            switch (job.Name.ToUpper())
            {
                case "RetreiveData":
                    // RUN JOB
                    break;
                case "ProcessData":
                    // RUN JOB
                    break;                    
            }

            job.InProgress = false;
            job.EndedLastRun = DateTime.Now;
            _scheduledJobRepository.Update(job);
            _scheduledJobRepository.SaveChanges();
        }
        catch (Exception exception)
        {
            _messaging.ReportError("Error occured whilst checking we are ready to run " + exception.Message, exception, null, 0, ServiceSource.DATA_IMPORT_SERVICE);
        }

        return true;
    }   
}

EDIT:

Include Program.cs

static void Main()
{
    if (!Environment.UserInteractive)
    {
        ServiceBase[] ServicesToRun;
        ServicesToRun = new ServiceBase[]
        {
        new DataImport()
        };
        ServiceBase.Run(ServicesToRun);
    }
}

Upvotes: 0

Views: 502

Answers (3)

lilo0
lilo0

Reputation: 1025

You subsribe to timer event in OnStart, and didn't unsubscribe in OnStop.

Move _serviceTimer.Elapsed += new ElapsedEventHandler(OnTimer); and initialization of AutoReset to constructor. Stop timer in OnStop. That should fix your issue. I believe your service is started (restarted) more than once.

Upvotes: 0

spender
spender

Reputation: 120380

If overlapping is a concern, ditch the timer and make an async loop, leveraging Task.Delay:

async Task SomeFunc(CancellationToken token)
{
    while(!token.IsCancellationRequested)
    {
        DoWork();
        await Task.Delay(timeInterval, token);
    }
}

Upvotes: 1

Mer
Mer

Reputation: 129

try to stop the timer inside the OnTimer function then re-start timer after it has finished executing your task.

Upvotes: 0

Related Questions