Best way to do a task looping in Windows Service
Asked Answered
A

2

17

I have a method that send some SMS to our customers that look like below:

public void ProccessSmsQueue()
{
   SmsDbContext context = new SmsDbContext();
   ISmsProvider provider = new ZenviaProvider();
   SmsManager manager = new SmsManager(context, provider);

   try
   {
      manager.ProcessQueue();
   }
   catch (Exception ex)
   {
      EventLog.WriteEntry(ex.Message, EventLogEntryType.Error);
   }
   finally
   {
      context.Dispose();
   }
}

protected override void OnStart(string[] args)
{
   Task.Factory.StartNew(DoWork).ContinueWith( ??? )
}

So, I have some issues:

  1. I don´t know how long it takes for the method run;

  2. The method can throw exceptions, that I want to write on EventLog

  3. I want to run this method in loop, every 10 min, but only after last execution finish.

How I can achieve this? I thought about using ContinueWith(), but I still have questions on how to build the entire logic.

Amanda answered 28/11, 2014 at 13:28 Comment(4)
Why do you want to do it using a Task at all? Why not just use a Timer?Chirrupy
Using a timer, I would have to control the start and stop each time the event was fired. I thought of using this solution, but did not seem the most elegant.Amanda
What about simply using a loop?Cyanohydrin
Because it will run synchronously. I need a mix of async/sync.Amanda
M
36

You should have an async method that accepts a CancellationToken so it knows when to stop, calls ProccessSmsQueue in a try-catch block and uses Task.Delay to asynchronously wait until the next time it needs to run:

public async Task DoWorkAsync(CancellationToken token)
{
    while (true)
    {
        try
        {
            ProccessSmsQueue();
        }
        catch (Exception e)
        {
            // Handle exception
        }
        await Task.Delay(TimeSpan.FromMinutes(10), token);
    }
}

You can call this method when your application starts and Task.Wait the returned task before existing so you know it completes and has no exceptions:

private Task _proccessSmsQueueTask;
private CancellationTokenSource _cancellationTokenSource;

protected override void OnStart(string[] args)
{
    _cancellationTokenSource = new CancellationTokenSource();
    _proccessSmsQueueTask = Task.Run(() => DoWorkAsync(_cancellationTokenSource.Token));
}

protected override void OnStop()
{
    _cancellationTokenSource.Cancel();
    try
    {
        _proccessSmsQueueTask.Wait();
    }
    catch (Exception e)
    {
        // handle exeption
    }
}
Monica answered 28/11, 2014 at 13:36 Comment(12)
Looks nice! How I could start the Task in this scenario under a Windows Service?Amanda
@Amanda Start it when the service loads and wait for it when it closes.Monica
The OnStop method on the WinService is not async (cannot use await). If I just cancel the token it will work or I can do this in another way? (when i use await _task, i receive: 'The await operator can only be used in a method marked with async modifier')Amanda
@Amanda This is one of the few cases where it's ok to block synchronously on an async operation. Use Task.Wait.Monica
Interesting. So, in this scenario, the delay will only run when the ProcessQueue() finishes? I don´t wanna two or more "ProccessQueues" run at same time.Amanda
you solution worked fine. But when I try to start the service with a big queue, the service deadlock while loading. I receive a "Timeout" message. I think things are happening synchronously. Any ideas?Amanda
@Amanda yes, an async method run synchronously until an await is reached which in your case means it runs ProccessSmsQueue once. You can solve that by offloading that work to a ThreadPool thread with Task.Run. I updated the answer.Monica
What happens if we put await DoWorkAsync(_cts.Token) instead of Task.Run(() => DoWorkAsync(_cts.Token)); in the OnStart method ?Biennial
@juzamn OnStart returns void so it would be an async void method which you should only use for event handlers.Monica
@juzamn when starting the process, it would hang if it takes too long to get to the first await. which is where Task.Run helps this answer has helpful information aswellDepart
@Monica is _cts a typo? I think it should be _cancellationTokenSourceSabina
@Sabina fixed.Monica
I
1

Sample Worker Class that I have used in Windows Services. It supports stopping in a 'clean' way by using a lock. You just have to add your code in DoWork, set your timer in the StartTimerAndWork method (in milliseconds), and use this class in your service.

public class TempWorker
    {
        private System.Timers.Timer _timer = new System.Timers.Timer();
        private Thread _thread = null;

        private object _workerStopRequestedLock = new object();
        private bool _workerStopRequested = false;

        private object _loopInProgressLock = new object();
        private bool _loopInProgress = false;

        bool LoopInProgress
        {
            get
            {
                bool rez = true;

                lock (_loopInProgressLock)
                    rez = _loopInProgress;

                return rez;
            }
            set
            {
                lock (_loopInProgressLock)
                    _loopInProgress = value;
            }
        }

        #region constructors
        public TempWorker()
        {
        }
        #endregion

        #region public methods
        public void StartWorker()
        {
            lock (_workerStopRequestedLock)
            {
                this._workerStopRequested = false;
            }
            _thread = new Thread(new ThreadStart(StartTimerAndWork));
            _thread.Start();
        }
        public void StopWorker()
        {
            if (this._thread == null)
                return;

            lock (_workerStopRequestedLock)
                this._workerStopRequested = true;

            int iter = 0;
            while (LoopInProgress)
            {
                Thread.Sleep(100);

                iter++;

                if (iter == 60)
                {
                    _thread.Abort();
                }
            }

            //if (!_thread.Join(60000))
            //    _thread.Abort();

        }
        #endregion


        #region private methods

        private void StartTimerAndWork()
        {
            this._timer.Elapsed += new ElapsedEventHandler(timer_Elapsed);
            this._timer.Interval = 10000;//milliseconds
            this._timer.Enabled = true;
            this._timer.Start();

        }


        #endregion


        #region event handlers
        private void timer_Elapsed(object sender, ElapsedEventArgs e)
        {
            if (!LoopInProgress)
            {
                lock (_workerStopRequestedLock)
                {
                    if (this._workerStopRequested)
                    {
                        this._timer.Stop();
                        return;
                    }
                }

                DoWork();

            }
        }

        private void DoWork()
        {
            try
            {
                this.LoopInProgress = true;

                //DO WORK HERE

            }
            catch (Exception ex)
            {
                //LOG EXCEPTION HERE
            }
            finally
            {
                this.LoopInProgress = false;
            }
        }
        #endregion

    }
Interferon answered 28/11, 2014 at 13:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.