Wednesday, October 13, 2010

Maldito bug do ConnectionBroker!

Há algum tempo venho experimentando uns crashes constantes do IDE do Delphi (tanto D6 quanto BDS2006) ao trabalhar com um projeto multi-camadas baseado no DataSnap.
De forma a abstratir a conexão do ClientDataSet do protocolo (DCOM, Socket ou WebConnection), eu utilizo um TConnectionBroker em cada DataModule onde tem TClientDataSet ligado ao servidor.

Pois bem, reparei que ao clicar no item de menu "Close All" do Delphi sempre ocorria um Access Violation, que com frequência obrigava-me a reiniciar o IDE. Resolvi identificar a causa do erro. Instalei um novo ExceptionHandler para o IDE, baseado no JclDebug, que me permitiria obter o stack trace.
Em seguida, quando o erro tornou a ocorrer, salvei o stack trace e pude então identificar a fonte do AV. Ele sempre ocorria no método TConnectionBroker.GetConnected.
Bastou 5 minutos investigando o código do TConnectionBroker para verificar onde está o problema: O ConnectionBroker não seta a property Connection para NIL internamente se o objeto Connection for destruído, caso os dois tenham Owners diferentes (estejam em DataModules diferentes).
Em design time, dentro do IDE do Delphi, frequentemente o objeto Connection é destruído ANTES do ConnectionBroker. Como a property ConnectionBroker.Connection continua diferente de NIL, qualquer referência ao Connection irá gerar um AV.

Considero isto um bug. O correto seria utilizar o mecanismo de notificação que existe no TComponent, utilizando FreeNotification e RemoveFreeNotification, de forma que o TDispatchConnection notifique o TConnectionBroker sempre que for destruído. Assim o ConnectionBroker poderá setar internamente o Connection para nil. Detalhe: Este problema só ocorre em DesignTime, dentro do IDE. Em testes não consegui fazer o problema se repetir dentro de aplicações.

Para implementar este comportamento em um descendente customizado do TConnectionBroker seria necessário sobrecarregar o método SetConnection, que é privado. Logo, tive que criar uma "cópia" do TConnectionBroker em outra unit, e dar outro nome para ele, TConectionBrokerEx.
O método SetConnection do meu TConnectionBrokerEx ficou assim (o resto do código é cópia exata do código do TConnectionBroker):
procedure TConnectionBrokerEx.SetConnection(const Value: TCustomRemoteServer);
resourcestring
SNoConnectToBroker = 'Connection not allowed to TConnectionBroker';
begin
if FConnection <> Value then
begin
if Value = Self then
raise Exception.Create(SNoCircularConnection)
else
if Assigned(Value) and (Value is TConnectionBroker) then
raise Exception.Create(SNoConnectToBroker);
// As linhas abaixo marcadas com {*} foram adicionadas para promover a
// notificação entre Connection e ConnectionBroker quando os dois possuem
// Owners diferentes (estão em DataModules/Forms diferentes)
if Assigned(FConnection) then {*}
FConnection.RemoveFreeNotification(Self); {*}
FConnection := Value;
if Assigned(FConnection) then {*}
FConnection.FreeNotification(Self); {*}
end;
end;
Substituí todos os TConnectionBroker do meu projeto por TConnectionBrokerEx e pus um fim definitivo nos AV's que me forçavam reiniciar o IDE dezenas de vezes por dia! :-)