sexta-feira, 25 de abril de 2008

Sincronizando Business Objects e Business Processes no Ensemble

O cenário: temos algumas informações que devem ser replicadas em diferentes bases de dados. Esta escolha de se replicar os dados, se deve a diversos fatores, que não convém mencionar aqui.

No Ensemble da Intersystems, temos um serviço de atualização de dados em duas bases de dados distintas, inclusive com estruturas diferentes. A primeira das bases de dados, em Oracle, é acessada diretamente via ODBC, pelo Ensemble. A segunda, também em Oracle, não permite o acesso direto (pois como é usada por um software de terceiros, perderia-se a garantia de performance caso o acesso fosse liberado). Para tanto, a empresa que desenvolveu este software criou uma camada utilizando Web Services (WS) padrão SOAP, para a atualização dos dados.

Além de ter uma estrutura diferente, a segunda base de dados possui algumas restrições (constraints), o que faz com que alguns dados possam ser válidos na primeira base, mas não na segunda. Essas restrições se devem a interações que alguns dados possuem dentro do segundo banco, especialmente em se tratando da codificação das entidades dentro dele (isto é, do formato/geração das chaves). Isto ocasiona que para alguns dados, a segunda base rejeita os dados, mas a primeira base os aceita.

Pois então, com estes requisitos em mente, construímos um Business Process (BP) no Ensemble, mostrado na figura abaixo simplificadamente (exclui alguns passos do processo que são irrelevantes para o entendimento deste post):



Notem que a primeira chamada é para um Business Object (BO) que lida com a chamada para o WS. O Studio, a IDE integrada do Caché/Ensemble tem um wizard para a criação de toda a estrutura de invocação de um Web Service SOAP, bastando ter o WSDL correspondente. Além de montar as classes de dados e a classe cliente do Web Service, no Ensemble você pode gerar já a estrutura de BO e mensagens de Request/Response.

O Web Service retorna como resposta uma String. Se a atualização se deu sem erros, é retornado "OK". Caso contrário, é retornado uma breve descrição do erro. Bem, o segundo elemento do BP é justamente uma checagem para ver se aconteceu algum erro. Se aconteceu algum erro, retorna imediatamente. Caso contrário, faz a chamada para dois BOs que cuidarão da atualização na primeira base, via ODBC. Note como as estruturas são diferentes: uma única estrutura de dados da segunda base (atualizada via WS) equivale a duas estruturas na primeira base (atualizada via SQL-ODBC).

Rodando diversos testes, com usuários inclusive, surgiu um problema. De vez em quando, mais frequentemente do que eu gostaria, por alguma razão, o Adapter SQL perde a conexão e/ou sessão com o Oracle, gerando exceções. Falarei mais sobre isso em um outro post, mas fica registrado que esses problemas podem ser tão graves, que necessitam até de shutdown de todo o Caché, para o Caché/Ensemble se recuperar do erro.

Acontecendo isso, você, astuto leitor, deve ter percebido que o processo no BP tem um grave problema: ele pode atualizar a segunda base via WS, e não atualizar a primeira, causando inconsistências (lembrando que o WS é simples, não tem nada de WS-Transactions, por exemplo). A solução é colocar a chamada do Web Service dentro do contexto da transação da primeira base, e se o WS falhar, executar um rollback nos SQLs já executados. Mas... como fazer isso?

Uma solução é jogar todo o código dentro de um mesmo BO (que já iria ficar enorme, já que pra colocar tudo numa mesma transação, as duas BOs que usam ODBC-SQL se tornariam uma só). Então, pra evitar mais código-macarrão dentro de um único BO, resolvi aproveitar a estrutura do BO que faz a chamada do Web Service, e manter Business Objects separados para atualizar via WS e via ODBC.

Então, eu deveria: executar as atualizações no banco via SQL, mas não dar Commit; chamar o Web Service, e caso de sucesso na atualização do segundo banco, executar o Commit, senão dar um Rollback.

Como fazer isso, usando dois BOs separados, sendo que ao terminar a execução de uma chamada, o BO com o Adapter SQL pode não necessariamente manter a sessão (pode ser até outra instância do BO a ser invocado)? A solução é deixar tudo dentro de uma única chamada do BO. Então, surge a necessidade de comunicar/sincronizar o BO com o BP, sem terminar a execução do BO. Para isso, decidi fazer uso de globais.

Simplificadamente, a solução adotada foi:

- No BP, fazer um "fork", ou seja, abrir dois caminhos de execução paralelos.
- No primeiro caminho, colocar o BO com o Adaptador SQL.

  • Neste BO, setar controle de Commit como manual.
  • Executar os comandos SQL.
  • Se ocorrer algum erro, pode sair do BP e retornar o erro.
  • Se não ocorreu erro, sinaliza usando a global, e espera pelo sinal de retorno do WS (também sinalizado via global).
  • Se o WS retornou "OK", executa o Commit.

- No segundo caminho:
  • Coloca um código de espera, até o BO com o Adaptador SQL sinalizar.
  • Fazer a chamada do BO que invoca o Web Service.
  • Sinaliza para o BO com o SQL Adapter, o resultado da invocação do WS.


Todas as sinalizações foram feitas usando uma global. Como podemos ter mais de uma instância, usamos como controle uma chave de sessão simples, para acessar a global, e evitar conflitos.

Toda esta mega-gambiarra pode ser vista no BP abaixo:



A primeira parte do código responsável pela sincronização é mostrada abaixo. Nele, que está na BP como "Seta chave sessão para global", configuramos uma chave que identificará a "sessão", usando um timestamp ($h ou $horolog, que retorna o dia/hora atual), mais um número aleatório. Usando esta chave, acessamos uma global que será a área de dados compartilhados entre a BO e a BP.


set context.chaveGlobal = $h _ $random($piece($h,",",1))
set ^globalSync(context.chaveGlobal) = 0


Ainda na BP, colocamos na parte de código "Espera por Oracle", o código abaixo, que é basicamente um loop, que de tempos em tempos, checa se o valor na global mudou. Note que o comando HANG faz com que a execução do código seja interrompida por algum tempo, mais especificamente o tempo passado como parâmetro do comando, em segundos. No código abaixo, 200 ms, ou seja, o código checa se houve mudança na variável global de 200 em 200 milisegundos.


while (^globalSync(context.chaveGlobal) < 1) {
hang 0.2
}


Dentro da BO, depois de realizar os comandos SQL, mas antes de efetuar o COMMIT, usa-se o código abaixo. Nele, a variável global muda de valor, sinalizando para o código na BP que ela pode continuar a execução. Além disso, também temos um loop de espera, pois o COMMIT só pode ser realizado depois que o Web Service retornar. Note que enquanto no código da BP a chave global está dentro de context, na BO ela está dentro da mensagem pRequest enviada à BO.


set ^globalSync(pRequest.chaveGlobal) = 1

while (^globalSync(pRequest.chaveGlobal) = 1) {
hang .2
}


Voltando a BP, depois que a BO que invoca o Web Service retorna, verificamos se este retornou OK ou se aconteceu algum erro. Em ambos os casos, sinalizamos o resultado na variável global, como mostra o código abaixo:


if (context.statusWS.StringValue = "OK") {
set ^globalSync(context.chaveGlobal) = 2
}
else {
set ^globalSync(context.chaveGlobal) = 3
}


Por fim, nos últimos passos da BO, depois de passar pelo loop de espera, temos certeza de que o Web Service retornou (ou ocorreu um erro de timeout, por exemplo). Só resta então checar o status na variável global, e realizar o COMMIT ou ROLLBACK, como mostra (simplificadamente) o código abaixo:


if (^globalSync(pRequest.chaveGlobal) = 2) {
set st = ..Adapter.Commit()
}
else {
do ..Adapter.Rollback()
}