Jaskinie Podróże Nurki Grafika Mizar Teksty Kulinaria Lemkov Namiary Mapa RSS English
Spelunka Trybików Teksty DBExpress Sprawdzanie błędnych odwołań a nowy menedżer pamięci YAC Software
  Wróć

Spis

Charsets

Wykresy

DBExpress

Delphi

HTML

Intraweb

MSTest

PHP

Programowanie

R

Rhino Mocks

Software

Testowanie

Testowanie UI

VB.NET

VCL

WPF

Sprawdzanie błędnych odwołań a nowy menedżer pamięci
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

Komentarze
Kurczę!
Na razie brak komentarzy...

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ń

DBExpress a dostęp wielowątkowy

Formy jak ramki

Dostęp do składowych chronionych

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