.net OracleDataReader.Read方法超时
ODP.NET OracleCommand类具有CommandTimeout属性,可用于强制执行命令的超时。此属性似乎适用于CommandText是SQL语句的情况。示例代码用于说明此属性的作用。在代码的初始版本中,CommandTimeout设置为零,告知ODP.NET不要强制执行超时.net OracleDataReader.Read方法超时,.net,oracle,odp.net,.net,Oracle,Odp.net,ODP.NET OracleCommand类具有CommandTimeout属性,可用于强制执行命令的超时。此属性似乎适用于CommandText是SQL语句的情况。示例代码用于说明此属性的作用。在代码的初始版本中,CommandTimeout设置为零,告知ODP.NET不要强制执行超时 using System; using System.Collections.Generic; using System.Data; using System.Diagnostics; using System
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();
}
}
}
}
如果我将CommandTimeout更改为类似3的值
cmd.CommandTimeout = 3;
…然后运行相同的代码生成以下输出:
Executing Query...
Exception was thrown: ORA-01013: user requested cancel of current operation
Press any key to continue...
不过,调用返回ref游标的存储过程是另一回事。考虑下面的测试过程(纯粹用于测试目的):
下面的示例代码可用于调用存储的进程。请注意,它将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();
}
}
}
}
请注意,执行时间非常快(34毫秒),并且没有引发超时异常。我们在这里看到的性能是因为ref游标的SQL语句直到第一次调用OracleDataReader.Read方法时才执行。当执行第一个Read()调用以从refcursor读取第一条记录时,就会产生长时间运行的查询的性能影响
我所说明的行为意味着OracleCommand.CommandTimeout属性不能用于取消与ref游标关联的长时间运行的查询。在这种情况下,我不知道ODP.NET中有任何属性可以用来限制引用游标SQL的执行时间。对于长时间运行的ref cursor SQL语句如何在一定时间后被短路,有人有什么建议吗?您似乎不是第一个提出以下问题的人: 您可以在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的线程上抛出的。取消。以下是我最终采用的解决方案。它只是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();
}
}
}
}
我假设您没有权限访问在数据读取器上循环的代码(即,在业务类中),或者您不想用性能相关的信息污染它。我有权限访问在数据读取器上循环的代码。这个特定上下文中的问题是,如果执行的查询由于某种原因长时间运行(例如,在几个大表上生成笛卡尔乘积的“坏”查询),我希望在N秒后将查询短路。所谓“短路”,我的意思是实际上取消数据库中的查询。OracleCommand.CommandTimeout属性似乎可以做到这一点,但在上面显示的示例中,“command”正在执行一个返回ref游标的过程,它不起作用。感谢您的反馈。我曾经考虑过多线程方法;这种方法将允许终止调用的.NET端,但我认为查询将继续在Oracle RDBMS中执行。我也希望能够取消查询执行。这可能是最后一个reader.Read调用,但当您在Sql Developer中取消查询并需要等待几秒钟才能完成时,这是否不同?我需要澄清我上一次评论中的几点。当我考虑使用多线程方法时,它使用的是线程API,而不是TPL。我假设中止正在执行查询的后台线程不会取消RDBMS中的查询。然而,在做了一些快速而肮脏的测试之后,这似乎不是真的。RDBMS会终止会话,从而取消查询。关于使用TPL任务和取消,问题是我需要能够终止读取调用本身(而不是让它完成)。查询可能需要30分钟或更长的时间。你会笑——你的手动解决方案基本上是复制了OracleCommand.Cancel——它似乎工作得很好。我的答案已经更新,以反映这一点。很抱歉,在回到这一点上的延误,但我的优先事项已转移到其他地方。关于我最终使用的解决方案,请参见下面的答案。它受您发布的示例启发,但使用扩展方法封装任务逻辑,并使其成为OracleDataReader类API的一部分。
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...
[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);
}
}
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...