Jaskinie Podróże Nurki Grafika Mizar Teksty Kulinaria Lemkov Namiary Mapa RSS English
Spelunka Trybików Teksty DBExpress DBExpress a dostęp wielowątkowy YAC Software
  Wróć

Spis

Charsets

Wykresy

DBExpress

Delphi

HTML

Intraweb

MSTest

PHP

Programowanie

R

Rhino Mocks

Software

Testowanie

Testowanie UI

VB.NET

VCL

WPF

DBExpress a dostęp wielowątkowy
No cóż, kolejny poważny problem z korzystaniem ze sterowników DBExpress z MySQL i Delphi 2007 - podczas większego obciążenia (kurde, nie myślałem, że przez kilka połączeń będę musiał tak nazywać) dostawałem dziwne błędy Access Violation raportowane dla WideStrings.TWideStrings.GetValue.

Ki czort?

Ze sławetnej dokumentacji Delphi:

Absolute thread safety is left to applications using dbExpress. However, some thread safety issues are best handled by the dbExpress framework. dbExpress thread safe operations include loading and unloading drivers, and connection creation As mentioned earlier, a delegate driver can be created to make the entire public interface of dbExpress thread safe if needed.

Hmm... ciekaw jestem, jaka jest różnica między "absolute thread safety" a zwykłą, znaną, standardową "thread safety"...

Nieważne. Znów QualityCentral okazuje się tu przyjacielem - patrz QC#57326. Proponowane rozwiązanie zakłada wprowadzenie sekcji krytycznej przy tworzeniu połączeń SQL. Raport oznaczony jest jako "Test Case Error" - pewnie skoro DBExpress nie zapewnia poprawnego działania wielowątkowego (wg cytatu powyżej), CodeGear nie uważa, że to problem, gdy ich biblioteki się wywracają przy standardowych zastosowaniach...

Przed skorzystaniem z zaproponowanej rady najpierw chciałem stworzyć test case, który pokazywałby problem z dużym prawdopodobieństwem. Myślałem o mniej więcej 20 wątkach, z których każdy losowo wykonywałby zapytania select i insert to bazy danych:
  type
    TYIKDBTestThread = class(TThread)
    private
      FException: boolean;
      FTerminated: boolean;
    protected
      procedure Execute; override;
    end;
  
  procedure TYIKDBTestThread.Execute;
  const
    CCommandCount = 100;
  var
    k: integer;
    LConnection: TSQLConnection;
    LQuery: TSQLQuery;
  begin
    FTerminated := FALSE;
    try
      try
        for k := 0 to CCommandCount - 1 do
        begin
          LConnection := CreateSQLConnection;
          try
            if Random(2) = 0 then
              LConnection.ExecuteDirect(
                'insert into TIMING values (''1'', ''1'', ''1'', ''1'')')
            else
            begin
              LQuery := TSQLQuery.Create(NIL);
              try
                LQuery.SQLConnection := LConnection;
                LQuery.SQL.Text := 'select * from TIMING';
                LQuery.Open;
              finally
                FreeAndNIL(LQuery);
              end;
            end;
          finally
            FreeAndNIL(LConnection);
          end;
        end;
      except
        on Exception do
          FException := TRUE;
      end;
    finally
      FTerminated := TRUE;
    end;
  end;
Flagi FTerminated i FException zostały wprowadzone aby sprawdzać, co się dzieje w wątkach (i aby zfailować :) test gdy FException jest prawdziwe w którymkolwiek z wątków). CreateSQLConnection tworzy połączenie ustawiając KeepConnection na FALSE i Connected na TRUE.

Następnie, kod test case'a:
  procedure TYIKDBTests.TestSqlExprThreads;
  const
    CThreadCount = 20;
  var
    k, LCount: integer;
    LThreadList: TObjectList;
  begin
    Randomize;
    LThreadList := TObjectList.Create;
    try
      for k := 0 to CThreadCount - 1 do
        LThreadList.Add(TYIKDBTestThread.Create(FALSE));
      repeat
        LCount := 0;
        for k := 0 to LThreadList.Count - 1 do
          if not (LThreadList[k] as TYIKDBTestThread).FTerminated then
            Inc(LCount);
      until LCount = 0;
      for k := 0 to LThreadList.Count - 1 do
        Check(not (LThreadList[k] as TYIKDBTestThread).FException);
    finally
      FreeAndNIL(LThreadList);
    end;
  end;
Fajne jest to, że powyższy test case ładnie pokazuje problem prawie za każdym razem. To na pewno będzie pomocne w sprawdzaniu / szukaniu dobrego rozwiązania / obejścia.

No dobra, czas więc na rozwiązanie zaproponowane w raporcie QC - dodanie sekcji krytycznej na tworzenie połączeń SQL.

Najpierw wyglądało na to, że rzecz działa nieźle, lecz niestety, raz na jakiś czas zacząłem dostawać błąd AV przy System.TObject.Free... Co pewnie oznaczało, że sekcja krytyczna potrzebna jest także przy zamykaniu połączenia (i to ta sama sekcja). O ile to rozwiązuje sprawę błędów AV, wprowadza niestety zakleszczenie...

Przy różnych próbach dodawania sekcji krytycznych dla różnych operacji (np. czytania i pisania do bazy, tzn. wokół ExecuteDirect i TQuery.Open) otrzymywałem różne zachowanie, ale żadne z tych rozwiązań nie dawało 100% niezawodności...

Zacząłem zatem zagłębiać się w kod SqlExpr.pas - niezbyt fajna robota, ale musiałem uruchomić swój program. Jako że sekcje krytyczne wydawały się rozwiązywać problem (oprócz zakleszczenia), starałem się maksymalnie ograniczyć kod, który musiał znaleźć się w sekcji - w metodach TSQLConnection.DoConnect i .DoDisconnect. Po wielu eksperymentach udało się ograniczyć zakres do następującego kodu w .DoConnect:
  if (FDBXConnection is TDBXConnectionEx)
    and (TDBXConnectionEx(FDBXConnection).ProductName = 'BlackfishSQL') then
  begin
    FDefaultSchema := 'DEFAULT_SCHEMA';
  end;
Ponieważ nie korzystam z BlackfishSQL :-) rzeczony kod po prostu wykomentowałem (problem jest z odwołaniem do ProductName). Po tej zmianie i po wyrzuceniu wszystkich sekcji krytycznych (które, nota bene, zwalniały test case nawet 5 razy) dostałem prawie stabilne rozwiązanie. Piszę "prawie stabilne" gdyż pojawiają się, choć bardzo rzadko, inne błędy (które pojawiały się i wcześniej, więc wygląda na to, że spowodowane są jeszcze czym innym). Na ogół parametry połączenia są rozwalone, ale szczerze? Nie mam już czas na dalszą z tym zabawę. Szczególnie, że po wprowadzonej zmianie moja aplikacja działa poprawnie - nie ma już utraty danych...

Ostatnia rzecz: wygląda na to, że Delphi 2009 rozwiązuje ten problem (jak i poprzedni). Jednak błędy opisane w paragrafie wyżej (z niszczeniem parametrów połączenia) nadal się pojawiają...

Góra

Komentarze
#1
Trevor Toms napisał(a) dnia 2015-01-29 17:45:30
Your "BlackfishSQL" fix has resolved a long-running multi-user web service error for us. We suffered random SQL errors in dbExpress (D2007) usually of the form "Missing Driver Name" or segment faults. These would only occur when a second or third thread was started by IIS.

Thanks for this valuable fix!

Góra

Dodaj komentarz (pola z gwiazdką są obowiązkowe)
Imię / ksywa *
Mail (pozostanie ukryty) *
Twoja strona
Komentarz (bez tagów) *
Wpisz tekst wyświetlony poniżej *
 

Góra

Tagi

DBExpress

Delphi


Podobne strony

Interfejsy w Delphi... znowu

Weryfikacja "wiszących" procedur obsługi zdarzeń w formach Delphi

Przeciąganie plików na okno aplikacji

Intraweb a MaxConnections

Argumenty za używaniem FreeAndNIL

Intraweb jako moduł DSO Apache'a

Intraweb a "Device not supported"

Zautomatyzowane testowanie GUI

Zaokrąglanie i dokładność na FPU 8087

Intraweb a SessionTimeout

Używanie TChart w programach Intraweb

Unknown driver: MySQL

TIdMessage a CharSet

Gwarancje oprogramowania

Automatyczne testowanie formularzy okien

TChart - brakujące etykiety w osiach

Tracona pamięć i eksplozje połączeń w DBExpress

Kontrola dyrektyw kompilacji warunkowej oraz ustawień przełączników kompilacji

Wykrywanie traconej pamięci a DUnit

last_insert_id() a DBExpress

Rejestracja rozszerzeń

Formy jak ramki

Sprawdzanie błędnych odwołań a nowy menedżer pamięci

Dostęp do składowych chronionych

Obiekty, interfejsy i obsługa pamięci w Delphi - ki czort?