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
|