Wednesday, June 25, 2008

Running Batch Jobs In An ASP.NET Web Application Using Application State

Download Example Files:

Sample Run Job Web Application

 

Recently while doing work for a client, I came across the need to run a job (batch) within the application while giving the user that ability to monitor the progress of the job and also being able to track how much time was remaining until the completion of the job.

Along with the aforementioned specs, the requirements were as follows:  they needed a web page that they could go to in order to start the job, they needed the website to be able to alert them if another job was being run, they needed to be able to cancel the job at any time.

I assume that this is a common need for businesses and would like to share a simple example that illustrates how one might go about solving such a problem.

image

How I Did It

Since I didn't want to create a database or something similar to handle such a task due to the overhead that it takes to create a database, I decided to store information about my 'job' in the Application State object (which is essentially a HashTable) due to the fact that Application State object could be accessed and modified across the entire application.  This way, anyone, regardless of who started a job, would be able to access the information regarding the job.

The meat of my solution lies in two things: The BatchRun class that I created (Figure 1), and the use of a special property that accesses the Applicaiton State object within my page (Figure 2).

 


 

    [Serializable]

    public class BatchRun

    {

        #region Constructors

 

        public BatchRun()

        {

        }

 

        public BatchRun(int totalNumberOfItems)

        {

            TotalNumberOfItems = totalNumberOfItems;

        }

 

        #endregion

 

        #region Properties

 

        public DateTime? LastUpdatedTime { get; set; }

        public DateTime? StartTime { get; set; }

        public int TotalNumberOfItems { get; set; }

        public int ItemsCompleted { get; private set; }

        public bool ShouldStop { get; set; }

 

        public bool HasNotBegun

        {

            get

            {

                return StartTime == null;

            }

        }

 

        public bool IsCompletedOrExpired

        {

            get

            {

                return PercentDone == 100 || ShouldStop || (LastUpdatedTime != null && LastUpdatedTime.Value < DateTime.Now.AddMinutes(-Settings.Default.StalledMinuteWait));

            }

        }

 

        public int PercentDone

        {

            get

            {

                if (TotalNumberOfItems == 0)

                    return 0;

                return (int)(100 * ((double)ItemsCompleted / (double)TotalNumberOfItems));

            }

        }

 

        private TimeSpan? TotalTime

        {

            get

            {

                if (StartTime == null)

                    return null;

                return DateTime.Now - StartTime.Value;

            }

        }

 

        public TimeSpan EstimatedTimeRemaining

        {

            get

            {

                if (ItemsCompleted == 0 || TotalTime == null)

                    return default(TimeSpan);

                return TimeSpan.FromSeconds(

                (int)((TotalTime.Value.TotalSeconds / ItemsCompleted) * (TotalNumberOfItems - ItemsCompleted)));

            }

        }

        #endregion

 

        #region Public Methods

        public void Start()

        {

            if (TotalNumberOfItems == 0)

            {

                throw new ArgumentException("Total Number of Items not set!", "TotalNumberOfItems");

            }

            StartTime = DateTime.Now;

            LastUpdatedTime = DateTime.Now;

        }

 

        public bool IncrementItemsCompleted()

        {

            if (ItemsCompleted < TotalNumberOfItems)

            {

                LastUpdatedTime = DateTime.Now;

                return (++ItemsCompleted == TotalNumberOfItems);

            }

            return true;

        }

        #endregion

    }

Figure 1

 

 


        protected BatchRun CurrentBatchRun

        {

            get { return (BatchRun)this.Application["CurrentBatchRun"]; }

            set { this.Application["CurrentBatchRun"] = value; }

        }

Figure 2

 

The BatchRun class keeps all of the information that you need to derive what percent of the job is complete as well as how much time is left.  As you can see, I've included some intelligent properties within the BatchRun class to make these calculations easier and uniform.

The special application state property allows you to just access and modify the information about the job without having to be thinking about where you are storing this data.

The rest of the solution is just hooking into this information to let the user know what the status of the job is.

Here's the entire code behind file for the batch run example web page (please note that not everything I've done here is optimal, but is merely to give an example of how one would accomplish such a task):

 


using System;

using System.Threading;

 

namespace RunJobWebsite

{

    public partial class _Default : System.Web.UI.Page

    {

        protected BatchRun CurrentBatchRun

        {

            get { return (BatchRun)this.Application["CurrentBatchRun"]; }

            set { this.Application["CurrentBatchRun"] = value; }

        }

 

        protected void Page_Load(object sender, EventArgs e)

        {

            if (!IsPostBack)

            {

                this.txtStartDate.Text = DateTime.Today.AddMonths(-1).ToShortDateString();

                this.txtEndDate.Text = DateTime.Today.ToShortDateString();

            }

        }

 

        protected void btnRunBatch_Click(object sender, EventArgs e)

        {

            SetBatchParameterTableVisibility(false);

 

            if (!CurrentBatchInProgress())

            {

                this.lblPercentage.Text = "0";

                this.lblTimeRemaining.Text = "Unknown";

                CurrentBatchRun = new BatchRun();

                var ts = new ThreadStart(RunBatch);

                var thread = new Thread(ts);

                thread.Start();

            }

            else

            {

                ShowAlert("Statement Batch Already In Progress");

            }

            timerBatchRun.Enabled = true;

        }

 

        private void RunBatch()

        {

            DateTime startDate = DateTime.Parse(this.txtStartDate.Text);

            DateTime endDate = DateTime.Parse(this.txtEndDate.Text);

 

            var tempDate = startDate;

            int i = 0;

            while (tempDate < endDate)

            {

                i++;

                tempDate = tempDate.AddHours(1);

            }

 

            CurrentBatchRun.TotalNumberOfItems = i;

            CurrentBatchRun.Start();

 

            tempDate = startDate;

            while (tempDate < endDate)

            {

                DoSomething(new Random().Next(1, 5));

                tempDate = tempDate.AddHours(1);

                if (CurrentBatchRun == null || CurrentBatchRun.ShouldStop)

                {

                    break;

                }

                CurrentBatchRun.IncrementItemsCompleted();

            }

        }

 

        private void DoSomething(int seconds)

        {

            Thread.Sleep(seconds * 1000);

        }

 

        private void SetBatchParameterTableVisibility(bool visible)

        {

            this.tblBatchParameters.Visible = visible;

            this.tblBatchProgress.Visible = !visible;

        }

 

        private bool CurrentBatchInProgress()

        {

            var batchRun = CurrentBatchRun;

            if (batchRun == null || batchRun.HasNotBegun)

            {

                return false;

            }

            return !batchRun.IsCompletedOrExpired;

        }

 

        protected void timerBatchRun_Tick(object sender, EventArgs e)

        {

            BatchRun currentBatch = CurrentBatchRun;

            if (currentBatch != null && currentBatch.HasNotBegun)

            {

                return;

            }

            if (currentBatch == null)

            {

                SetBatchParameterTableVisibility(true);

                return;

            }

            if (currentBatch.IsCompletedOrExpired)

            {

                if (currentBatch.ShouldStop)

                {

                    ShowAlert(String.Format("Statement Run Completed at {0}, but was cancelled.", currentBatch.LastUpdatedTime));

                }

                else

                {

                    ShowAlert(String.Format("Statement Run Completed at {0}.", currentBatch.LastUpdatedTime));

                }

                timerBatchRun.Enabled = false;

                SetBatchParameterTableVisibility(true);

            }

            else

            {

                this.lblPercentage.Text = currentBatch.PercentDone.ToString();

                this.lblTimeRemaining.Text = String.Format("{0:f2}", currentBatch.EstimatedTimeRemaining.TotalMinutes);

            }

        }

 

        private void ShowAlert(string alert)

        {

            Response.Write("<script>alert('" + alert + "')</script>");

        }

        protected void btnStop_Click(object sender, EventArgs e)

        {

            ShowAlert("Batch Run Stopped");

            if (CurrentBatchRun != null)

            {

                CurrentBatchRun.ShouldStop = true;

                this.timerBatchRun.Enabled = false;

                SetBatchParameterTableVisibility(true);

            }

        }

 

 

    }

}

 

Download Example Files:

Sample Run Job Web Application

1 comment:

Blog said...

Works perfectly, very helpful, thanks!