Timeout for OracleDataReader.Read Method

浪子不回头ぞ 提交于 2019-11-28 12:10:41

Here's the solution that I ultimately went with. It's just an extension method for the OracleDataReader class. This method has a timeout value and a callback function as parameters. The callback function would typically (if not always) be OracleCommand.Cancel.

namespace ConsoleApplication1
{
    public static class OracleDataReaderExtensions
    {
        public static bool Read(this OracleDataReader reader, int timeout, Action cancellationAction)
        {
            Task<bool> task = Task<bool>.Factory.StartNew(() => 
                {
                    try
                    {
                        return reader.Read();
                    }
                    catch (OracleException ex)
                    {
                        // When cancellationAction is called below, it will trigger 
                        // an ORA-01013 error in the Read call that is still executing.
                        // This exception can be ignored as we're handling the situation
                        // by throwing a TimeoutException.
                        if (ex.Number == 1013)
                        {
                            return false;
                        }
                        else
                        {
                            throw;
                        }
                    }
                });

            try
            {
                if (!task.Wait(timeout))
                {
                    // call the cancellation callback function (i.e. OracleCommand.Cancel())
                    cancellationAction();

                    // throw an exception to notify calling code that a timeout has occurred
                    throw new TimeoutException("The OracleDataReader.Read operation has timed-out.");
                }
                return task.Result;
            }
            catch (AggregateException ae)
            {
                throw ae.Flatten();
            }
        }
    }
}

Here's an example of how it can be used.

namespace ConsoleApplication1
{
    class Program
    {
        static string constring = "User ID=xxxx; Password=xxxx; Data Source=xxxx;";

        static void Main(string[] args)
        {
            using (OracleConnection con = new OracleConnection(constring))
            using (OracleCommand cmd = new OracleCommand())
            {
                cmd.Connection = con;
                con.Open();

                Console.WriteLine("Executing Query...");

                string sql = "<some long running sql>";
                cmd.CommandText = "PROC_A";
                cmd.CommandType = System.Data.CommandType.StoredProcedure;
                cmd.Parameters.Add(new OracleParameter("i_sql", OracleDbType.Varchar2) { Direction = ParameterDirection.Input, Value = sql });
                cmd.Parameters.Add(new OracleParameter("o_cur1", OracleDbType.RefCursor) { Direction = ParameterDirection.Output });

                try
                {
                    // execute command and get reader for ref cursor
                    OracleDataReader reader = cmd.ExecuteReader(CommandBehavior.CloseConnection);

                    // read first record; this is where the ref cursor SQL gets evaluated
                    Console.WriteLine("Reading first record...");
                    if (reader.Read(3000, cmd.Cancel)) { }

                    // read remaining records
                    Console.WriteLine("Reading records 2 to N...");
                    while (reader.Read(3000, cmd.Cancel)) { }
                }
                catch (TimeoutException ex)
                {
                    Console.WriteLine("Exception: {0}", ex.Message);
                }

                Console.WriteLine("Press any key to continue...");
                Console.Read();
            }
        }
    }
}

And here's an example of the output.

Executing Query...
Reading first record...
Exception: The OracleDataReader.Read operation has timed-out.
Press any key to continue...

It would appear that you are not the first to ask: https://forums.oracle.com/forums/thread.jspa?threadID=2125208

You could monitor within the loop over reader.Read() the elapsed time and exit the loop. That's nice and simple but of course it's only going to be able to exit after a potentially long running call to Read finishes.

Your best bet would probably be to do the loop within a Task on a separate thread, monitor it, and then call cmd.Cancel on the original thread:

[Test]
public void TimeBasicSql()
{
  using (OracleConnection con = new OracleConnection("User ID=id; Password=pass; Data Source=db;"))
  using (OracleCommand cmd = new OracleCommand())
  {
    con.Open();
    cmd.Connection = con;

    Console.WriteLine("Executing Query...");

    try
    {
      cmd.CommandTimeout = 1;
      String sql = "begin open :o_cur1 for select count(*) from all_objects, all_objects; end;";

      cmd.CommandText = sql;
      cmd.Parameters.Add(new OracleParameter("o_cur1", OracleDbType.RefCursor) { Direction = ParameterDirection.Output });

      var task = System.Threading.Tasks.Task.Factory.StartNew(() =>
        {
          try
          {
            Stopwatch watch1 = Stopwatch.StartNew();
            OracleDataReader reader = cmd.ExecuteReader();
            watch1.Stop();
            Console.WriteLine("Query complete.  Execution time: {0} ms", watch1.ElapsedMilliseconds);

            int counter = 0;
            Stopwatch watch2 = Stopwatch.StartNew();
            if (reader.Read()) counter++;
            watch2.Stop();
            Console.WriteLine("First record read: {0} ms", watch2.ElapsedMilliseconds);

            Stopwatch watch3 = Stopwatch.StartNew();
            while (reader.Read())
            {
              counter++;
            }
            watch3.Stop();
            Console.WriteLine("Records 2..n read: {0} ms", watch3.ElapsedMilliseconds);
            Console.WriteLine("Records read: {0}", counter);
          }
          catch (OracleException ex)
          {
            Console.WriteLine("Exception was thrown: {0}", ex);
          }

        });

      if (!task.Wait(cmd.CommandTimeout * 1000))
      {
        Console.WriteLine("Timeout exceeded.  Cancelling...");
        cmd.Cancel();
      }



    }
    catch (OracleException ex)
    {
      Console.WriteLine("Exception was thrown: {0}", ex);
    }

  }

It's worth noting that the ORA-01013 exception is thrown on the worker thread and not on the thread calling OracleCommand.Cancel.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!