2013-05-17 45 views
8

Klasa ODP.NET OracleCommand ma właściwość CommandTimeout, której można użyć do wymuszenia limitu czasu wykonywania polecenia. Ta właściwość wydaje się działać w sytuacjach, w których CommandText jest instrukcją SQL. Przykładowy kod służy do zilustrowania tej właściwości w akcji. W początkowej wersji kodu parametr CommandTimeout jest ustawiony na zero, co nakazuje ODP.NET nie wymuszanie limitu czasu.Limit czasu dla metody OracleDataReader.Read

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

Przykâadowa dla powyższego kodu jest pokazany poniżej:

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... 

Jeśli zmienię CommandTimeout coś jak 3 ...

cmd.CommandTimeout = 3; 

... wtedy działa tak samo kod generuje następujące wyjście:

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

Wywołanie procedury przechowywanej, która zwraca kursor ref jest już inna sprawa. Rozważ poniższą procedurę testową (wyłącznie do celów testowych):

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

    open o_cur1 
    for 
    i_sql; 

END PROC_A; 

Poniższy kod przykładowy może być użyty do wywołania zapisanego procesu. Należy zauważyć, że ustawia CommandTimeout do wartości 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(); 
      } 
     } 
    } 
} 

przykładzie wyjście z kodem powyżej pokazano poniżej:

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... 

odnotowuje, że wykonanie jest bardzo szybki (34 ms), a wyjątek przekroczenia limitu czasu nie został zgłoszony. Wydajność, którą tu widzimy, jest spowodowana tym, że instrukcja SQL dla kursora ref nie jest wykonywana przed pierwszym wywołaniem metody OracleDataReader.Read. Kiedy pierwsze wywołanie Read() zostanie wykonane w celu odczytania pierwszego rekordu z refcursor, zostanie wygenerowane uderzenie wydajności z długo działającego zapytania.

Opisane przeze mnie zachowanie oznacza, że ​​nie można użyć właściwości OracleCommand.CommandTimeout w celu anulowania długo działającego zapytania związanego z kursorem ref. Nie jestem świadomy żadnej właściwości w ODP.NET, która może być użyta do ograniczenia czasu wykonywania SQL kursora ref w tej sytuacji. Ktoś ma jakieś sugestie, w jaki sposób wykonanie długo działającego instrukcji SQL z korektorem może być zwarte po pewnym czasie?

+0

mam zamiar założyć, że albo nie masz dostępu do kodu, który jest zapętlenie nad czytnikiem danych (czyli w klasie biznes) lub że nie chce zanieczyszczać go z wydajnością powiązane informacje. –

+0

Mam dostęp do pętli kodu nad czytnikiem danych. Problem w tym konkretnym kontekście polega na tym, że jeśli wykonywane zapytanie jest z jakiegoś powodu długotrwałe (np. "Złe" zapytanie, które produkuje produkt kartezjański na kilku dużych tabelach), chciałbym sprowadzić zapytanie po N sekundy. Przez "zwarcie" mam na myśli faktycznie anulowanie zapytania w db. Właściwość OracleCommand.CommandTimeout pozornie to robi, ale nie działa w przykładzie pokazanym powyżej, gdzie "polecenie" wykonuje proces, który zwraca kursor ref. – dnickels

Odpowiedz

4

Oto rozwiązanie, z którym w końcu poszedłem. Jest to tylko metoda rozszerzenia dla klasy OracleDataReader. Ta metoda ma wartość limitu czasu i funkcję zwrotną jako parametry. Funkcja wywołania zwrotnego zazwyczaj (jeśli nie zawsze) będzie OracleCommand.Anuluj.

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

Oto przykład tego, jak można z niego korzystać.

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

A oto przykład wyjścia.

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

Wydaje się, że nie jesteś pierwszy zapytać: https://forums.oracle.com/forums/thread.jspa?threadID=2125208

Można monitorować wewnątrz pętli nad reader.Read() upływ czasu i wyjść z pętli. To fajne i proste, ale oczywiście będzie można wyjść dopiero po zakończeniu potencjalnie długiego wywołania Read.

Najprościej byłoby prawdopodobnie zrobić pętlę w ramach zadania w osobnym wątku, monitorować go, a następnie zadzwonić cmd.Cancel na oryginalnym wątku:

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

    } 

Warto zauważyć, że ORA- 01013 Wyjątek jest zgłaszany w wątku roboczym, a nie w wątku wywołującym OracleCommand.Cancel.

+0

Dzięki za opinię. Myślałem o wielowątkowym podejściu; to podejście pozwoli na zakończenie strony .NET połączenia, ale myślę, że * zapytanie będzie nadal wykonywane w Oracle RDBMS. Szukam również możliwości anulowania wykonywania kwerendy. – dnickels

+0

Może dla tego ostatniego czytnika.Czy zadzwonić, ale czy to różni się, że po anulowaniu zapytania w Sql Developer i musi poczekać kilka sekund na zakończenie anulowania? –

+0

Muszę wyjaśnić kilka punktów z mojego ostatniego komentarza. Kiedy pomyślałem o zastosowaniu podejścia wielowątkowego, korzystałem z interfejsu API wątków, a nie z licencji TPL. Zakładam, że przerwanie wątku tła, który wykonywał zapytanie, nie anuluje zapytania w RDBMS. Jednak po kilku szybkich i brudnych testach nie wydaje się to prawdą. RDBMS kończy sesję i tym samym anuluje zapytanie. Jeśli chodzi o używanie zadań TPL i anulowanie, problem polega na tym, że muszę mieć możliwość zakończenia samego wywołania Read (i nie pozwolić, aby to się skończyło). Zapytanie może trwać 30+ minut lub dłużej. – dnickels