sexta-feira, 14 de setembro de 2007

Problemas com a conexão - ou com uma conexão que cai (resetada pelo peer)

Uma conexão JPA, quando estabelecida, está sujeita a erros: falhas de rede, desconexão por timeout, etc. podem causar exceções na aplicação - isto é, erros em tempo de execução. A única saída para esse tipo de erros é criar tratadores de erro (Exception Handlers) que possam lidar com o problema.

Na aplicação que venho desenvolvendo, tenho tido erros freqüentes, tais como o seguinte:

[TopLink Warning]: 2007.09.14 04:31:21.518--UnitOfWork(1733807)--Exception [TOPLINK-4002] (Oracle TopLink Essentials - 2006.8 (Build 060830)): oracle.toplink.essentials.exceptions.DatabaseException
Internal Exception: com.mysql.jdbc.CommunicationsException: Communications link failure due to underlying exception:

** BEGIN NESTED EXCEPTION **

java.io.EOFException

STACKTRACE:

java.io.EOFException
at com.mysql.jdbc.MysqlIO.readFully(MysqlIO.java:1956)
at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:2368)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:2867)
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1616)
at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:1708)
at com.mysql.jdbc.Connection.execSQL(Connection.java:3255)
at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1293)
at com.mysql.jdbc.PreparedStatement.executeQuery(PreparedStatement.java:1428)
at oracle.toplink.essentials.internal.databaseaccess.DatabaseAccessor.executeSelect(DatabaseAccessor.java:711)
at oracle.toplink.essentials.internal.databaseaccess.DatabaseAccessor.basicExecuteCall(DatabaseAccessor.java:486)
at oracle.toplink.essentials.internal.databaseaccess.DatabaseAccessor.executeCall(DatabaseAccessor.java:437)
at oracle.toplink.essentials.threetier.ServerSession.executeCall(ServerSession.java:465)
at oracle.toplink.essentials.internal.queryframework.DatasourceCallQueryMechanism.executeCall(DatasourceCallQueryMechanism.java:213)
at oracle.toplink.essentials.internal.queryframework.DatasourceCallQueryMechanism.executeCall(DatasourceCallQueryMechanism.java:199)
at oracle.toplink.essentials.internal.queryframework.DatasourceCallQueryMechanism.executeSelectCall(DatasourceCallQueryMechanism.java:270)
at oracle.toplink.essentials.internal.queryframework.DatasourceCallQueryMechanism.selectAllRows(DatasourceCallQueryMechanism.java:600)
at oracle.toplink.essentials.queryframework.ReadAllQuery.executeObjectLevelReadQuery(ReadAllQuery.java:302)
at oracle.toplink.essentials.queryframework.ObjectLevelReadQuery.executeDatabaseQuery(ObjectLevelReadQuery.java:709)
at oracle.toplink.essentials.queryframework.DatabaseQuery.execute(DatabaseQuery.java:609)
at oracle.toplink.essentials.queryframework.ObjectLevelReadQuery.execute(ObjectLevelReadQuery.java:677)
at oracle.toplink.essentials.queryframework.ObjectLevelReadQuery.executeInUnitOfWork(ObjectLevelReadQuery.java:731)
at oracle.toplink.essentials.internal.sessions.UnitOfWorkImpl.internalExecuteQuery(UnitOfWorkImpl.java:2218)
at oracle.toplink.essentials.internal.sessions.AbstractSession.executeQuery(AbstractSession.java:937)
at oracle.toplink.essentials.internal.sessions.AbstractSession.executeQuery(AbstractSession.java:909)
at oracle.toplink.essentials.internal.ejb.cmp3.base.EJBQueryImpl.executeReadQuery(EJBQueryImpl.java:346)
at oracle.toplink.essentials.internal.ejb.cmp3.base.EJBQueryImpl.getResultList(EJBQueryImpl.java:447)
at DataSync.CooperadoSyncApp.forwardSync(CooperadoSyncApp.java:56)
at datasyncapp.Main.main(Main.java:55)


** END NESTED EXCEPTION **



Last packet sent to the server was 40 ms ago.Error Code: 0
Call:select * from cooperados where matricula = ?
bind => [20648]
Query:ReadAllQuery(DataSync.RemoteModel.CooperadosRem)


Uma abordagem seria tratar o erro no local em que ocorrem as exceções, com o código sugerido em outro post nos fóruns da OTN mas, apesar de teoricamente ser possível, se torna uma abordagem muito trabalhosa e de difícil manutenção, pois não há reutilização do código. Este post não trata exatamente desse problema, mas sim de recuperar a conexão JDBC de um EntityManager, sendo possível então utilizar o Accessor para reestabelecer a conexão. Contudo, é praticamente impossível prever quando um outro problema de conexão irá ocorrer após o primeiro tratamento, podendo ser necessário incluir diversos tratamentos dentro de um mesmo método. Na minha opinião, esta abordagem está descartada.


Procurei referências a esse problema e cheguei a um artigo que esclareceu alguns pontos. Encontrei um código interessante na página http://www.oracle.com/technology/products/ias/toplink/doc/1013/main/_html/sescfg007.htm#BCGDAGAF. A minha ação, então, foi a seguinte: ao criar a EntityManager (que gerência os objetos e sua persistência - i.e. gravação), anexo um tratador de erros, conforme segue:


[Código do Tratador de Erros]

import java.sql.SQLException;
import oracle.toplink.essentials.exceptions.*;
import oracle.toplink.essentials.queryframework.ReadQuery;

/**
*
* @author brunoa
*/
public class tratamentoExcecao implements ExceptionHandler {

/** Creates a new instance of tratamentoExcecao */
public tratamentoExcecao() {
}

public Object handleException(RuntimeException exception)
{
if (exception instanceof DatabaseException)
{
DatabaseException dbex = (DatabaseException) exception;
if (dbex.getInternalException() instanceof SQLException) {/*&&
(((SQLException) dbex.getInternalException()).getErrorCode() == MySQLTimeoutException))*/

dbex.getAccessor().reestablishConnection(dbex.getSession());
if (dbex.getQuery() instanceof ReadQuery)
{
return dbex.getSession().executeQuery(dbex.getQuery());
}
throw exception;
}

}
throw exception;
}
}




[Vinculação do tratador ao EntityManager]

((oracle.toplink.essentials.ejb.cmp3.EntityManager)rem).getActiveSession().setExceptionHandler( new tratamentoExcecao() );



Apesar de essa implementação conseguir "tratar" o erro, o funcionamento não é totalmente o esperado. O problema é que, no caso de a consulta que está sendo executada precisar de parâmetros, os mesmo não estão sendo recuperados. Isso é um problema enfrentado por outros usuários, como num post da OTN (Oracle Technology Network)



Para considerar os parâmetros, devemos utilizar uma condição que analisa se existiam parâmetros na consulta original:

if (dbex.getQueryArgumentsRecord() != null)
return dbex.getSession().executeQuery(dbex.getQuery(), (AbstractRecord) dbex.getQueryArgumentsRecord());
else
return dbex.getSession().executeQuery(dbex.getQuery());


Assim, o código final do tratador de erros ficará assim:


import java.sql.SQLException;
import java.util.Vector;
import oracle.toplink.essentials.exceptions.*;
import oracle.toplink.essentials.internal.sessions.AbstractRecord;
import oracle.toplink.essentials.queryframework.ReadQuery;

/**
*
* @author brunoa
*/
public class tratamentoExcecao implements ExceptionHandler {

/** Creates a new instance of tratamentoExcecao */
public tratamentoExcecao() {
}

public Object handleException(RuntimeException exception)
{
Vector args = new Vector();

if (exception instanceof DatabaseException)
{
DatabaseException dbex = (DatabaseException) exception;
if (dbex.getInternalException() instanceof SQLException) {/*&&
(((SQLException) dbex.getInternalException()).getErrorCode() == MySQLTimeoutException))*/

dbex.getAccessor().reestablishConnection(dbex.getSession());
if (dbex.getQuery() instanceof ReadQuery)

{
if (dbex.getQueryArgumentsRecord() != null)
return dbex.getSession().executeQuery(dbex.getQuery(), (AbstractRecord) dbex.getQueryArgumentsRecord());
else
return dbex.getSession().executeQuery(dbex.getQuery());
}
throw exception;
}

}
throw exception;
}
}

Minha Aplicação

Estou criando uma aplicação para uma simples replicação de dados entre o banco de dados local e um banco de dados remoto, hospedado por um serviço de Hosting. A descrição da arquitetura é a seguinte:

--- A CONTINUAR ---