8

I thought I was trying to do something very simple. I just want to report a running number on the screen so the user gets the idea that the SQL Stored Procedure that I'm executing is working and that they don't get impatient and start clicking buttons.

The problem is that I can't figure out how to actually call the progress reporter for the ExecutNonQueryAsync command. It gets stuck in my reporting loop and never executes the command but, if I put it after the async command, it will get executed and result will never not equal zero.

Any thoughts, comments, ideas would be appreciated. Thank you so much!

        int i = 0;
        lblProcessing.Text = "Transactions " + i.ToString();
        int result = 0;
        while (result==0)
        {
            i++;
            if (i % 500 == 0)
            {
                lblProcessing.Text = "Transactions " + i.ToString();
                lblProcessing.Refresh();
            }

        }
        //  Yes - I know - the code never gets here - that is the problem! 
        result = await cmd.ExecuteNonQueryAsync();
Blorgbeard
  • 101,031
  • 48
  • 228
  • 272
Missy
  • 1,286
  • 23
  • 52
  • Your loop never exits, because `result` is always `0`. nothing in that loop ever changes it. – Bradley Uffner Aug 30 '17 at 00:27
  • Yes - I know. The problem is that I don't know how to tap into some sort of progress reporting with the ExecuteNonQueryAsync() – Missy Aug 30 '17 at 00:29
  • As far as I know, there isn't any way to get SQL Server to report progress back to you. – Bradley Uffner Aug 30 '17 at 00:30
  • You might be able to tap in to [this](https://stackoverflow.com/questions/20028989/display-progress-of-execution-through-progress-bar) method, but I'm not sure how it will mix with async / await. – Bradley Uffner Aug 30 '17 at 00:31
  • I saw that. I had hoped there was a way with ExecuteNonQueryAsync(). Thanks anyway :) – Missy Aug 30 '17 at 00:37
  • 1
    Have you tried it? A quick looks makes me think that it would work. I don't see any async blocking issues that would stop it. – Bradley Uffner Aug 30 '17 at 00:41
  • I will look at it some more and see if I can find a way to make it work. – Missy Aug 30 '17 at 00:47
  • Just a suggestion, but you could remove the SQL tag because this question does not involve any SQL problems or answers, but just my suggestion. – Mr.J Sep 04 '17 at 01:11

5 Answers5

5

The simplest way to do this is to use a second connection to monitor the progress, and report on it. Here's a little sample to get you started:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Text;
using System.Threading.Tasks;

namespace Microsoft.Samples.SqlServer
{
    public class SessionStats
    {
        public long Reads { get; set; }
        public long Writes { get; set; }
        public long CpuTime { get; set; }
        public long RowCount { get; set; }
        public long WaitTime { get; set; }
        public string LastWaitType { get; set; }
        public string Status { get; set; }

        public override string ToString()
        {
            return $"Reads {Reads}, Writes {Writes}, CPU {CpuTime}, RowCount {RowCount}, WaitTime {WaitTime}, LastWaitType {LastWaitType}, Status {Status}";
        }
    }
    public class SqlCommandWithProgress
    {


        public static async Task ExecuteNonQuery(string ConnectionString, string Query, Action<SessionStats> OnProgress)
        {
            using (var rdr = await ExecuteReader(ConnectionString, Query, OnProgress))
            {
                rdr.Dispose();
            }
        }

        public static async Task<DataTable> ExecuteDataTable(string ConnectionString, string Query, Action<SessionStats> OnProgress)
        {
            using (var rdr = await ExecuteReader(ConnectionString, Query, OnProgress))
            {
                var dt = new DataTable();

                dt.Load(rdr);
                return dt;
            }
        }


        public static async Task<SqlDataReader> ExecuteReader(string ConnectionString, string Query, Action<SessionStats> OnProgress)
        {
            var mainCon = new SqlConnection(ConnectionString);
            using (var monitorCon = new SqlConnection(ConnectionString))
            {
                mainCon.Open();
                monitorCon.Open();



                var cmd = new SqlCommand("select @@spid session_id", mainCon);
                var spid = Convert.ToInt32(cmd.ExecuteScalar());

                cmd = new SqlCommand(Query, mainCon);

                var monitorQuery = @"
select s.reads, s.writes, r.cpu_time, s.row_count, r.wait_time, r.last_wait_type, r.status
from sys.dm_exec_requests r
join sys.dm_exec_sessions s 
  on r.session_id = s.session_id
where r.session_id = @session_id";

                var monitorCmd = new SqlCommand(monitorQuery, monitorCon);
                monitorCmd.Parameters.Add(new SqlParameter("@session_id", spid));

                var queryTask = cmd.ExecuteReaderAsync( CommandBehavior.CloseConnection );

                var cols = new { reads = 0, writes = 1, cpu_time =2,row_count = 3, wait_time = 4, last_wait_type = 5, status = 6 };
                while (!queryTask.IsCompleted)
                {
                    var firstTask = await Task.WhenAny(queryTask, Task.Delay(1000));
                    if (firstTask == queryTask)
                    {
                        break;
                    }
                    using (var rdr = await monitorCmd.ExecuteReaderAsync())
                    {
                        await rdr.ReadAsync();
                        var result = new SessionStats()
                        {
                            Reads = Convert.ToInt64(rdr[cols.reads]),
                            Writes = Convert.ToInt64(rdr[cols.writes]),
                            RowCount = Convert.ToInt64(rdr[cols.row_count]),
                            CpuTime = Convert.ToInt64(rdr[cols.cpu_time]),
                            WaitTime = Convert.ToInt64(rdr[cols.wait_time]),
                            LastWaitType = Convert.ToString(rdr[cols.last_wait_type]),
                            Status = Convert.ToString(rdr[cols.status]),
                        };
                        OnProgress(result);

                    }

                }
                return queryTask.Result;


            }
        }
    }
}

Which you would call something like this:

    class Program
    {

        static void Main(string[] args)
        {
            Run().Wait();

        }
        static async Task Run()
        {
            var constr = "server=localhost;database=tempdb;integrated security=true";
            var sql = @"
set nocount on;
select newid() d
into #foo
from sys.objects, sys.objects o2, sys.columns 
order by newid();
select count(*) from #foo;
";

            using (var rdr = await SqlCommandWithProgress.ExecuteReader(constr, sql, s => Console.WriteLine(s)))
            {
                if (!rdr.IsClosed)
                {
                    while (rdr.Read())
                    {
                        Console.WriteLine("Row read");
                    }
                }
            }
            Console.WriteLine("Hit any key to exit.");
            Console.ReadKey();


        }
    }

Which outputs:

Reads 0, Writes 0, CPU 1061, RowCount 0, WaitTime 0, LastWaitType SOS_SCHEDULER_YIELD, Status running
Reads 0, Writes 0, CPU 2096, RowCount 0, WaitTime 0, LastWaitType SOS_SCHEDULER_YIELD, Status running
Reads 0, Writes 0, CPU 4553, RowCount 11043136, WaitTime 198, LastWaitType CXPACKET, Status suspended
Row read
Hit any key to exit.
David Browne - Microsoft
  • 80,331
  • 6
  • 39
  • 67
  • This is a very interesting idea. I'm wondering if it would substantially increase the processing time. – Missy Sep 02 '17 at 22:49
  • 1
    It should not noticeably increase the processing time. – David Browne - Microsoft Sep 02 '17 at 22:53
  • Thank you so much for your long and thoughtful answer. I am trying to understand it completely. In await SqlCommandWithProgress.ExecuteReader(constr, sql, s => Console.WriteLine(s))), is s => Console.WriteLine(s) like a function to report the progress? If so, where is S originally derived? – Missy Sep 03 '17 at 19:40
  • 1
    The method takes an argument of type Action which is a function or snippet of code that you provide that takes an instance of SessionStats and does something with it. In this case just writes it to the Console. – David Browne - Microsoft Sep 03 '17 at 19:45
4

You're not going to be able to get ExecuteNonQueryAsync to do what you want here. To do what you're looking for, the result of the method would have to be either row by row or in chunks incremented during the SQL call, but that's not how submitting a query batch to SQL Server works or really how you would want it to work from an overhead perspective. You hand a SQL statement to the server and after it is finished processing the statement, it returns the total number of rows affected by the statement.

Kyle Burns
  • 1,164
  • 7
  • 16
3

Do you just want to let the user know that something is happening, and you don't actually need to display current progress?

If so, you could just display a ProgressBar with its Style set to Marquee.

If you want this to be a "self-contained" method, you could display the progress bar on a modal form, and include the form code in the method itself.

E.g.

public void ExecuteNonQueryWithProgress(SqlCommand cmd) {
    Form f = new Form() {
        Text = "Please wait...",
        Size = new Size(400, 100),
        StartPosition = FormStartPosition.CenterScreen,
        FormBorderStyle = FormBorderStyle.FixedDialog,
        MaximizeBox = false,
        ControlBox = false
    };
    f.Controls.Add(new ProgressBar() { 
        Style = ProgressBarStyle.Marquee,
        Dock = DockStyle.Fill
    });
    f.Shown += async (sender, e) => {
        await cmd.ExecuteNonQueryAsync();
        f.Close();
    };
    f.ShowDialog();
}
Blorgbeard
  • 101,031
  • 48
  • 228
  • 272
  • That occurred to me but I'm hoping for a concise piece of code that I can use and reuse in a utility program. Progress bar requires the calling program to have progress bar display. – Missy Sep 01 '17 at 15:50
  • I see.. but your current code needs access to a Label, right? Not much different. – Blorgbeard Sep 01 '17 at 15:53
  • Point taken. I will consider this but I'm hoping to find more of a bundled solution. – Missy Sep 01 '17 at 15:55
  • One way to implement a "bundled" solution (assuming you're sticking to the winforms world) would be to pop up a modal dialog with the progress bar on it. The form would be simple enough to create at runtime inside your method: you wouldn't need to use the designer for it. – Blorgbeard Sep 01 '17 at 16:30
  • This is the most straightforward answer. Would you please look at my thought below and comment? I would also appreciate an uptick on the question to preserve my reputation. Then I will select this as answered. – Missy Sep 06 '17 at 21:37
3

That is an interesting question. I have had to implement similar things in the past. In our case the priority was to:

  • Keep client side responsive in case the user doesn't want to stick around and wait.
  • Update the user of action and progress.

What I would do is use threading to run the process in the background like:

HostingEnvironment.QueueBackgroundWorkItem(ct => FunctionThatCallsSQLandTakesTime(p, q, s));

Then using a way to estimate work time I would increment a progress bar from client side on a clock. For this, query your data for a variable that gives you a linear relationship to the work time needed by FunctionThatCallsSQLandTakesTime.

For example; the number of active users this month drives the time FunctionThatCallsSQLandTakesTime takes. For each 10000 user it takes 5 minutes. So you can update your progress bar accordingly.

Simon Bourdeau
  • 429
  • 5
  • 18
1

I'm wondering if this might be a reasonable approach:

    IAsyncResult result = cmd2.BeginExecuteNonQuery();
    int count = 0;
    while (!result.IsCompleted)
    {
         count++;
         if (count % 500 == 0)
         {
            lblProcessing.Text = "Transactions " + i.ToString();
            lblProcessing.Refresh();
         }
         // Wait for 1/10 second, so the counter
         // does not consume all available resources 
         // on the main thread.
         System.Threading.Thread.Sleep(100);
    }
Missy
  • 1,286
  • 23
  • 52
  • 1
    This is not a bad idea, but the implementation has problems. You should never `Sleep` on the UI thread. It would be better to use a `Timer` and do your processing in the `Tick` event handler. – Blorgbeard Sep 06 '17 at 22:23