Jakiś czas temu napotkałem denerwujący problem przy łączeniu się z bazą danych MySQL (5.0.32) z Delphi 2007 (z wszystkimi uaktualnieniami) -
w trakcie testów automatycznych raz na jakiś czas zgłaszany był błąd Access Violation przy instrukcji przypisującej tekst zapytania do TSQLQuery.SQL.Text.
Czasami rzecz działała bez żadnych problemów. czasami DUnit zgłaszał błąd w jednym teście lub innym
czasami testy działały na jednym komputerze, wywracały się na innych... takie tam.
Klasyczny przykład nie zainicjalizowanej zmiennej lub odwoływania się do zwolnionej pamięci. :-)
Program, który korzystał z MySQLa działał ok, jedynie testy nie przechodziły raz na jakiś czas.
Na szczęście, raport na QualityCentral przedstawiał sugestię, jak problem można naprawić (a przynajmniej ominąć) -
vide QC#58377.
Wystarczy przypisać NIL do FMetaData na końcu metody TDBXConnection.Close w SqlExpr.pas.
Poprawiło to działanie testów, ale przez jakiś czas nie miałem okazji do zbadania rzeczy dokładniej.
Jednakże chciałem wiedzieć dokładnie dlaczego poprawka działa i dlaczego program działał tak dobrze jak działał przed tą poprawką...
W końcu znalazłem czas aby wgłębić się w to bardziej.
Po pierwsze, należało sprawdzić, czy FastMM z różnymi opcjami odpluskwiania znajdzie problem.
Niestety, ostania wersja (4.90) nie znalazła niczego -
przy standardowych opcjach problem pojawia się a FastMM nie zgłasza żadnych błędów,
a w trybie FullDebugMode problem nie pojawia się w ogóle (co ma pewnie związek z nieco innym zarządzaniem pamięcią w tym trybie).
Przeglądając kod w SqlExpr.pas widać, że FMetaData zostaje zainicjalizowana w TSQLConnection.DoConnect,
lecz NIL nigdy nie jest temu polu przypisywane. Nie jest to koniecznie błędem - FMetaData jest jedynie referencją do obiektu,
więc jeżeli dealokujemy TSQLConnection przed dealokacją obiektu, do którego jest referencja, wszystko powinno działać dobrze.
Jednakże, FDBXConnection, na które wskazuje FMetaData jest zwalniane w TSQLConnection.DoDisconnect.
Zatem, jeżeli program odwoływałby się do FMetaData po zamknięciu połączenia, będą problemy.
Ale jak można odwołać się do FMetaData po zamknięciu połączenia?
W końcu meta informacje o danych w bazie danych powinny być potrzebne tylko przy otwartym połączeniu...
Wtedy sobie przypomniałem o jednym polu TSQLConnection - KeepConnection.
Ten parametr, gdy ustawiony na FALSE, powoduje, że sterownik zamyka połączenie tuż po wykonaniu komendy SQL.
I automatycznie wznawia połączenie gdy wykonujemy kolejne zapytania.
Zatem, teoretycznie, ustawianie tego parametru na FALSE nieco zwalnia działanie,
ale z drugiej strony utrzymuje minimalną liczbę otwartych połączeń.
(Ustawianie tego parametru na TRUE ma swoje problemy - o czym wkrótce.)
Zatem, poniższy kod (z KeepConnection ustawionym na FALSE)
LConnection := CreateSQLConnection;
try
// Pierwsze zapytanie:
LQuery := TSQLQuery.Create(NIL);
try
LQuery.SQLConnection := LConnection;
LQuery.SQL.Text := 'select * from TIMING;';
LQuery.Open;
finally
FreeAndNIL(LQuery);
end;
// Drugie zapytanie:
LQuery := TSQLQuery.Create(NIL);
try
LQuery.SQLConnection := LConnection;
LQuery.SQL.Text := 'select * from TIMING;';
LQuery.Open;
finally
FreeAndNIL(LQuery);
end;
finally
FreeAndNIL(LConnection);
end;
zamyka połączenie po dealokacji pierwszego zapytania i wznawia je,
gdy drugie zapytanie zostaje otwarte. Ale, w tym właśnie problem - FMetaData jest wykorzystywane
jeszcze przed otwarcie drugiego zapytanie, właśnie przy ustawianiu pola SQL.Text!
Chwila, ale uruchamiając powyższy kod nic złego się nie dzieje -
istotnie, obiekt wskazywany przez FMetaData został zwolniony, ale zawartość pamięci została bez zmian,
więc ponowne wykorzystanie FMetaData pomiędzy połączeniami nie wrzuca wyjątku (a przynajmniej w tak prostym kodzie jak ten powyżej).
Aby wymusić pojawienie się błędu, należy nadpisać pamięć, na którą wskazuje FMetaData.
Aby tak zrobić, należy najpierw dowiedzieć się, jak pamięć jest alokowana i zwalniana.
Pamiętajmy, że zarządca pamięci zmienił się w Delphi niedawno (dzięki wspaniałej robocie ludzi z projektu FastCode) -
od D2006 jest nowy menadżer - FastMM, który zastąpił stary moduł Borlanda.
Pod starym modułem zarządzania pamięcią wystarczyłoby alokować i czyścić
losowe bloki pamięci. Po kilku takich operacjach, pamięć zwolniona przez meta dane połączenia SQL została by nadpisana,
co doprowadziłoby do wrzucenia wyjątku przy drugim zapytaniu.
To niekoniecznie zadziała z nowym menadżerem pamięci - alokacja małych bloków podzielona jest na kubełki.
Każda alokacja jest trzymana w odpowiednim kubełku ze względu na wielkość alokowanej pamięci.
Zatem, gdybyśmy po zwolnieniu bloku pamięci
alokowali i czyścili pamięć blokami innej wielkości, nic złego by się nie stało.
A więc, gdy będziemy alokować i czyścić blok pamięci, należy wykorzystać do tego bloki tej samej wielkości
z meta dane TSQLConnection. Zatem, zmieńmy powyższy kod nieco:
// Przed pierwszym zapytaniem:
LSize := LConnection.MetaData.InstanceSize;
...
// Przed drugim zapytaniem:
SetLength(LMem, CCount);
for k := 0 to CCount - 1 do
LMem[k] := NIL;
try
for k := 0 to CCount - 1 do
begin
GetMem(LMem[k], LSize);
FillChar(LMem[k]^, LSize, $00);
end;
finally
for k := 0 to CCount - 1 do
FreeMem(LMem[k]);
end;
Teraz, uruchamiając ten kod przy CCount = 3, otrzymamy błąd AV przed drugim zapytaniem.
A zmieniając kod w SqlExpr.pas tak, że FMetaData ustawiane jest na NIL, naprawia sprawę.
Aby jeszcze raz zweryfikować powyższe przemyślenia, wykonałem jeszcze jeden test - czy ten kod wrzuci wyjątek
gdy ustawi się TSQLConnection.KeepConnection na TRUE?
Na szczęście :-) i zgodnie z oczekiwaniami, przy tym ustawieniu kod działa poprawnie nawet bez zmian w SqlExpr.pas.
Góra
|