3

I need to run a background thread for my MVC 4 app, where the thread wakes up every hour or so to delete old files in database, then goes back to sleep. This method is below:

//delete old files from database
public void CleanDB()
{
    while (true)
    {
        using (UserZipDBContext db = new UserZipDBContext())
        {
            //delete old files
            DateTime timePoint = DateTime.Now.AddHours(-24);
            foreach (UserZip file in db.UserFiles.Where(f => f.UploadTime < timePoint))
            {
                db.UserFiles.Remove(file);
            }
            db.SaveChanges();
        }
        //sleep for 1 hour
        Thread.Sleep(new TimeSpan(1, 0, 0));
    }
}

but where should I start this thread? The answer in this question creates a new Thread and start it in Global.asax, but this post also mentions that "ASP.NET is not designed for long running tasks". My app would run on a shared host where I don't have admin privilege, so I don't think i can install a seperate program for this task.

in short,

  1. Is it okay to start the thread in Global.asax given my thread doesn't do much (sleep most of the time and small db)?

  2. I read the risk of this approach is that the thread might get killed (though not sure why). How can i detect when the thread is killed and what can i do?

  3. If this is a VERY bad idea, what else can I do on a shared host?

Thanks!

UPDATE

@usr mentioned that methods in Application_Start can be called more than once and suggested using Lazy. Before I read up on that topic, I thought of this approach. Calling SimplePrint.startSingletonThread() multiple times would only instantiate a single thread (i think). Is that correct?

public class SimplePrint
{
    private static Thread tInstance = null;

    private SimplePrint()
    {
    }

    public static void startSingletonThread()
    {
        if (tInstance == null)
        {
            tInstance = new Thread(new ThreadStart(new SimplePrint().printstuff));
            tInstance.Start();
        }
    }

    private void printstuff()
    {
        DateTime d = DateTime.Now;
        while (true)
        {
            Console.WriteLine("thread started at " + d);
            Thread.Sleep(2000);
        }
    }
} 
Community
  • 1
  • 1
totoro
  • 3,257
  • 5
  • 39
  • 61
  • Just to answer #2 - your thread might be killed, because of [IIS restarts](http://stackoverflow.com/questions/2137894/iis-7-restarts-automatically), not sure if there is easy way (if any) to detect that. – Michael Nov 19 '14 at 09:23
  • ASP.NET worker processes will be recycled from time to time... and I think there´s no way to get a notification. When this is going to happen depends on the app pool´s regular recycle time setting, which is 1740 minutes by default, I guess... – Matze Nov 19 '14 at 09:25
  • Is it possible to simply create a standalone application that just runs this loop? – muddymess Nov 19 '14 at 09:26
  • 1
    What about implementing the clean-up as an action, and invoke that regularly using a Windows scheduler task? Could be a simple and pragmatic solution... – Matze Nov 19 '14 at 09:27
  • 3
    Might be a good read [The dangers of implementing recurring background tasks in asp net](http://haacked.com/archive/2011/10/16/the-dangers-of-implementing-recurring-background-tasks-in-asp-net.aspx/) – Michael Nov 19 '14 at 09:27
  • @Matze does this scheduler task run on the server or a client? – totoro Nov 19 '14 at 09:29
  • thanks for the pointers! I think i'll try Quartz.NET, a .NET scheduler for now – totoro Nov 19 '14 at 09:46

2 Answers2

4

I think you should try Hangfire.

Incredibly easy way to perform fire-and-forget, delayed and recurring tasks inside ASP.NET applications. No Windows Service required.

Backed by Redis, SQL Server, SQL Azure, MSMQ, RabbitMQ.

So you don't need admin priveleges.

RecurringJob.AddOrUpdate(
() => 
{
    using (UserZipDBContext db = new UserZipDBContext())
    {
        //delete old files
        DateTime timePoint = DateTime.Now.AddHours(-24);
        foreach (UserZip file in db.UserFiles.Where(f => f.UploadTime < timePoint))
        {
        db.UserFiles.Remove(file);
        }
        db.SaveChanges();
    }    
}
Cron.Hourly);
giacomelli
  • 7,287
  • 2
  • 27
  • 31
  • I have really liked Hangfire so far. One issue to be aware of though is the way it handles DateTime (which the OP seems to do). When it deserializes DateTime, it seems to lose it's DateTimeKind setting - which is a big pain when you've passed in UTC and it plays the event as Local. – Chris Rogers Nov 26 '14 at 05:18
  • The code above will not work well if there are thousands of files to remove in UserFiles, it may have a DB timeout. I usually chunk the removals and commit after each chunk. – ThisGuy Aug 31 '17 at 06:22
2

ASP.NET is not designed for long-running tasks, yes. But only because their work and data can be lost at any time when the worker process restarts.

You do not keep any state between iterations of your task. The task can safely abort at any time. This is safe to run in ASP.NET.

Starting the thread in Application_Start is a problem because that function can be called multiple times (surprisingly). I suggest you make sure to only start the deletion task once, for example by using Lazy<T> and accessing its Value property in Application_Start.

static readonly Lazy<object> workerFactory =
     new Lazy<object>(() => { StartThread(); return null; });

Application_Start:
  var dummy = workerFactory.Value;

For some reason I cannot think of a better init-once pattern right now. Nothing without locks, volatile or Interlocked which are solutions of last resort.

usr
  • 168,620
  • 35
  • 240
  • 369
  • you mentioned "safely abort at any time", is it because the deletion in database is some kind of all-or-nothing (atomic)? could you be more specific on using `Lazy` and accessing `Value`? thanks! – totoro Nov 19 '14 at 10:11
  • Yes, pretty much. When the worker is shut down all threads are aborted (or the entire process is killed sometimes). Either way all state of your code is deleted. This leaves no potential for data corruption. The database makes sure that mid-way changes are all or nothing. You can use a TransactionScope to make that more explicit.; Lazy: see edit. – usr Nov 19 '14 at 10:16
  • thanks for the reply. I tried `Lazy` but haven't quite got it to work. I've tried another approach in the same spirit as updated in the question. do you think it would work? – totoro Nov 20 '14 at 02:33
  • startSingletonThread is not thread-safe. You might end up starting multiple threads. Add a lock. – usr Nov 20 '14 at 08:42