Zdravím všechny, kterým po aktualizace Delphi na verzi 10.3 Rio přestaly fungovat notifikace událostí objektem TFDEventAlerter. Příčinou je chybné volání metody TFDThread.Queue namísto dřívější metody TFDThread.Synchornize, aniž by vývojáři ošetřili, že ihned po zavolání metody uvolní objekt, se kterým má metoda pracovat. Chyba se projeví v případě, že je nastavený příznak TFDEventAlerter.Options.Synchronize = True (výchozí stav) porušením přístupu do paměti.
Jelikož řešení není docela triviální a volání Queue namísto Synchronize je pro tento účel v podstatě žádoucí, uvádím trošku rozsáhlejší řešení problému. Bude to ovšem vyžadovat změnu tří systémových knihoven FireDAC a zahrnutí složky $(BDS)\source\data\firedac do SearchPath projektu.
Takže, co se vlastně děje, když nastane událost databáze?
1) Nová událost (Event) je zařazena do fronty TFDThread.FMessages.
2) V metodě TFDThread.Execute je událost načtena do proměnné oMsg a pak je volána metoda oMsg.Perform. Poté je proměnná oMsg okamžitě uvolněna voláním FDFree(oMsg)
Nyní co se děje v metodě TFDPhysEventMessage.Perform:
Chování záleží na hodnotě Synchronize komponenty TFDEventAlerter:
1) Pro False je rovnou volána metoda BasePerform.
2) Pro True je volána metoda TFDThread.Queue(nil, BasePerform) a vykonání BasePerform je zařazeno do fronty volání v hlavním vlákně.
Nyní si představme, co se stane ve Win32, pokud je Synchronize = True (tj. není aktivní počítání referencí a automatický garbage collection):
1) oMsg je odebrána z fronty zpráv
2) Volání metody BasePerform je zařazeno do volání v hlavním vlákně
3) oMsg je uvolněno z paměti
4) Během několika milisekund začne metoda BasePerform pracovat s objektem oMsg, který již byl uvolněn
Řešení tohoto problému není úplně snadné, obzvlášť pro rozsáhlejší projekty, které spoléhají na synchronizované zpracování událostí. Samozřejmě můžete vypnout přínaz Synchronize pro všechny instance komponenty TFDEventAlerter a zpracovat si události sami.
Moje řešení je určeno především pro konkrétní ovladač (v mém případě MSSQL), ale pro ostatní databáze je to podobné. Implementace kroků 1-4 je univerzální a vyřeší problém pro všechny databáze, kroky 5 a 6 umožní využít výhody řazení událostí do fronty namísto přerušení threadu pro zpracování zpráv.
1) Přidejte novou metodu do třídy TFDThreadMsgBase ve FireDAC.Stan.Util (virtual, protected):
function TFDThreadMsgBase.GetFreesItself: Boolean;
begin
Result := False;
end;
2) Přidejte novou vlastnost do třídy TFDThreadMsgBase ve FireDAC.Stan.Util:
public
property FreesItself: Boolean read GetFreesItself;
[code]
3) Změňte vnitřní cyklus metody TFDThread.Execute ve FireDAC.Stan.Util:
[code]
oMsg := TFDThreadMsgBase(oReceivedMsgs[i]);
try
fMsgFreesItself := oMsg.FreesItself; // pro uchování příznaku použijte lokální proměnou typu Boolean
if not oMsg.Perform(Self) then
Break
else if fMsgFreesItself then // zde zabráníme duplicitnímu uvolnění objektu
oMsg := nil;
finally
FDFree(oMsg);
end;
4) Aktualizujte metodu TFDPhysEventMessage.Perform ve FireDAC.Phys:
- nahraďte volání "TFDThread.Queue(nil, BasePerform)" následujícím kódem:
if FreesItself then
TFDThread.Queue(nil, BasePerform)
else
TFDThread.Synchronize(nil, BasePerform)
5) Přidejte novou chráněnou metodu do třídy TFDPhysMSSQLEventMessage ve FireDAC.Phys.MSSQL (overridden, protected):
function TFDPhysMSSQLEventMessage.GetFreesItself: Boolean;
begin
Result := True;
end;
6) Aktualizujte metodu TFDPhysMSSQLEventAlerter.InternalHandle ve FireDAC.Phys.MSSQL:
begin
oMsg := TFDPhysMSSQLEventMessage(AEventMessage);
try
{ current implementation from the second line }
finally
if oMsg.FreesItself then
FDFree(oMsg);
end;
end;
Uvedené změny by měly obnovit funkčnost existujícího kódu a zlepšit funkčnost notifikací událostí pro MS SQL díky volání TFDThread.Queue namísto TFDThread.Synchronize.