【问题标题】:Timeout for OracleDataReader.Read MethodOracleDataReader.Read 方法超时
【发布时间】:2013-05-12 21:59:46
【问题描述】:

ODP.NET OracleCommand 类有一个 CommandTimeout 属性,可用于强制执行命令超时。此属性似乎适用于 CommandText 是 SQL 语句的情况。示例代码用于说明此属性的实际作用。在代码的初始版本中,CommandTimeout 设置为零 - 告诉 ODP.NET 不要强制超时。

using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Text;
using Oracle.DataAccess.Client;

namespace ConsoleApplication3
{
    class Program
    {
        static void Main(string[] args)
        {
            using (OracleConnection con = new OracleConnection("User ID=xxxx; Password=xxxx; Data Source=xxxx;"))
            using (OracleCommand cmd = new OracleCommand())
            {
                con.Open();                
                cmd.Connection = con;

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

                try
                {
                    cmd.CommandTimeout = 0;

                    // Data set SQL:
                    cmd.CommandText = "<some long running SQL statement>";
                    cmd.CommandType = System.Data.CommandType.Text;

                    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.Message);
                }

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

上述代码的示例输出如下所示:

Executing Query...
Query complete.  Execution time: 8372 ms
First record read: 3 ms
Records 2..n read: 1222 ms
Records read: 20564
Press any key to continue...

如果我将 CommandTimeout 更改为 3...

cmd.CommandTimeout = 3;

...然后运行相同的代码会产生以下输出:

Executing Query...
Exception was thrown: ORA-01013: user requested cancel of current operation
Press any key to continue...

调用返回引用游标的存储过程是另一回事。考虑下面的测试过程(纯粹用于测试目的):

PROCEDURE PROC_A(i_sql VARCHAR2, o_cur1 OUT SYS_REFCURSOR)
is
begin

    open o_cur1
    for
    i_sql;

END PROC_A;

下面的示例代码可用于调用存储过程。请注意,它将 CommandTimeout 设置为 3。

using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Text;
using Oracle.DataAccess.Client;

namespace ConsoleApplication3
{
    class Program
    {
        static void Main(string[] args)
        {
            using (OracleConnection con = new OracleConnection("User ID=xxxx; Password=xxxx; Data Source=xxxx;"))
            using (OracleCommand cmd = new OracleCommand())
            {
                con.Open();                
                cmd.Connection = con;

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

                try
                {
                    cmd.CommandTimeout = 3;

                    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 });

                    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.Message);
                }

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

上面代码的示例输出如下所示:

Executing Query...
Query complete.  Execution time: 34 ms
First record read: 8521 ms
Records 2..n read: 1014 ms
Records read: 20564
Press any key to continue...

请注意,执行时间非常快(34 毫秒),并且没有抛出超时异常。我们在这里看到的性能是因为引用游标的 SQL 语句在第一次调用 OracleDataReader.Read 方法之前不会执行。当第一次调用 Read() 以从 refcursor 读取第一条记录时,会导致长时间运行的查询对性能造成影响。

我已经说明的行为意味着 OracleCommand.CommandTimeout 属性不能用于取消与引用游标关联的长时间运行的查询。在这种情况下,我不知道 ODP.NET 中有任何属性可用于限制引用游标 SQL 的执行时间。有人对长时间运行的引用游标 SQL 语句的执行如何在一定时间后短路有任何建议吗?

【问题讨论】:

  • 我将假设您无权访问在数据读取器上循环的代码(即,在业务类中),或者您不想污染它与性能相关的信息。
  • 我确实可以访问在数据阅读器上循环的代码。在这个特定上下文中的问题是,如果正在执行的查询由于某种原因长时间运行(即在几个大表上产生笛卡尔积的“坏”查询),我想在 N 之后短路查询秒。通过“短路”,我的意思是实际上取消了数据库中的查询。 OracleCommand.CommandTimeout 属性似乎可以做到这一点,但它在上面显示的示例中不起作用,其中“命令”正在执行一个返回 ref 游标的 proc。

标签: .net oracle odp.net


【解决方案1】:

这是我最终采用的解决方案。它只是 OracleDataReader 类的扩展方法。该方法有一个超时值和一个回调函数作为参数。回调函数通常(如果不总是)是 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();
            }
        }
    }
}

这是一个如何使用它的示例。

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();
            }
        }
    }
}

这是一个输出示例。

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

【讨论】:

    【解决方案2】:

    看来您不是第一个提出问题的人: https://forums.oracle.com/forums/thread.jspa?threadID=2125208

    您可以通过 reader.Read() 在循环内监视经过的时间并退出循环。这很好也很简单,但当然它只能在可能长时间运行的 Read 调用完成后才能退出。

    您最好的选择可能是在单独线程上的任务中执行循环,监视它,然后在原始线程上调用 cmd.Cancel:

    [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);
        }
    
      }
    

    值得注意的是,ORA-01013 异常是在工作线程上抛出的,而不是在调用 OracleCommand.Cancel 的线程上。

    【讨论】:

    • 感谢您的反馈。我曾考虑过多线程方法;该方法将允许终止调用的 .NET 端,但我认为查询将继续在 Oracle RDBMS 中执行。我也希望能够取消查询执行。
    • 可能是最后一个 reader.Read 调用,但是当您在 Sql Developer 中取消查询并且必须等待几秒钟才能完成取消时,这是否不同?
    • 我需要澄清我上次评论中的几点。当我考虑使用多线程方法时,它使用的是 Thread API 而不是 TPL。我假设中止正在执行查询的后台线程不会取消 RDBMS 中的查询。然而,在做了一些快速而肮脏的测试之后,这似乎不是真的。 RDBMS 确实会终止会话并因此取消查询。关于使用 TPL 任务和取消,问题是我需要能够终止 Read 调用本身(而不是让它完成)。查询可能需要 30 多分钟或更长时间。
    • 你会笑的 - 你的手动解决方案基本上复制了 OracleCommand.Cancel - 这似乎工作得很好。我的答案已更新以反映这一点。
    • 很抱歉迟迟没有回到这个话题,但我的优先事项已转移到其他地方。有关我最终使用的解决方案,请参阅下面的答案。它受到您发布的示例的启发,但使用扩展方法来封装任务逻辑并使其成为 OracleDataReader 类 API 的一部分。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-20
    • 2018-05-21
    • 2012-01-20
    • 1970-01-01
    • 2016-02-25
    • 2015-05-25
    相关资源
    最近更新 更多