Como tratar erros - AWS Flow Framework para Java

As traduções são geradas por tradução automática. Em caso de conflito entre o conteúdo da tradução e da versão original em inglês, a versão em inglês prevalecerá.

Como tratar erros

O constructo try/catch/finally no Java simplifica o tratamento de erros e é usado de forma ubíqua. Ele permite associar manipuladores de erros a um bloco de código. Internamente, isso funciona inserindo metadados adicionais sobre os manipuladores de erro na pilha de chamada. Quando uma exceção é gerada, o tempo de execução examina a pilha de chamada para localizar um manipulador de erros associado e o invocar, e se nenhum manipulador de erros apropriado for localizado, ele propagará a exceção para cima na cadeia de chamada.

Isso funciona bem para código síncrono, mas a manipulação de erros em assíncronos e em programas distribuídos impõe desafios adicionais. Como uma chamada assíncrona é retornada imediatamente, o chamador não está na pilha de chamada quando o código assíncrono é executado. Isso significa que as exceções não tratadas no código assíncrono não podem ser tratadas pelo chamador na forma habitual. Normalmente, as exceções originadas em código assíncrono são tratadas passando o estado de erro para um retorno de chamada que é passado para o método assíncrono. Como alternativa, se um Future<?> estiver sendo usado, ele relatará um erro quando você tentar acessá-lo. Isso não é o ideal porque o código que recebe a exceção (o retorno de chamada ou o código que usa o Future<?>) não tem o contexto da chamada original e não pode controlar adequadamente a exceção. Além disso, em um sistema assíncrono distribuído, com componentes que são executados simultaneamente, mais de um erro pode ocorrer simultaneamente. Esses erros podem ser de tipos e de severidades diferentes e precisam ser tratados de forma adequada.

A limpeza de um recurso após uma chamada assíncrona também é difícil. Ao contrário do código síncrono, você não pode usar try/catch/finally no código de chamada para limpar recursos, uma vez que o trabalho iniciado no bloco try ainda pode estar em andamento quando o bloco finally é executado.

A estrutura fornece um mecanismo que torna o tratamento de erro em código assíncrono distribuído semelhante (e quase tão simples quanto) ao try/catch/finally do Java.

ImageProcessingActivitiesClient activitiesClient = new ImageProcessingActivitiesClientImpl(); public void createThumbnail(final String webPageUrl) { new TryCatchFinally() { @Override protected void doTry() throws Throwable { List<String> images = getImageUrls(webPageUrl); for (String image: images) { Promise<String> localImage = activitiesClient.downloadImage(image); Promise<String> thumbnailFile = activitiesClient.createThumbnail(localImage); activitiesClient.uploadImage(thumbnailFile); } } @Override protected void doCatch(Throwable e) throws Throwable { // Handle exception and rethrow failures LoggingActivitiesClient logClient = new LoggingActivitiesClientImpl(); logClient.reportError(e); throw new RuntimeException("Failed to process images", e); } @Override protected void doFinally() throws Throwable { activitiesClient.cleanUp(); } }; }

A classe TryCatchFinally e suas variantes, TryFinally e TryCatch, funcionam de forma semelhante ao try/catch/finally do Java. Usando-a, você pode associar manipuladores de exceção a blocos de código de fluxo de trabalho que podem executar como tarefas assíncronas e remotas. O método doTry() é logicamente equivalente ao bloco try. A estrutura executa automaticamente o código em doTry(). Uma lista de objetos Promise pode ser passada para o construtor de TryCatchFinally. O método doTry será executado quando todos os objetos Promise passados para o construtor estiverem prontos. Se uma exceção for gerada pelo código que foi invocado de forma assíncrona em doTry(), qualquer trabalho pendente em doTry() será cancelado e doCatch() será chamado para tratar a exceção. Por exemplo, na lista acima, se downloadImage lançar uma exceção, createThumbnail e uploadImage serão cancelados. Finalmente, doFinally() é chamado quando todo o trabalho assíncrono for feito (concluído, com falha ou cancelado). Ele pode ser usado para limpeza de recursos. Você também pode aninhar essas classes para atender às suas necessidades.

Quando uma exceção é relatada em doCatch(), a estrutura fornece uma pilha de chamada lógica completa que inclui chamadas assíncronas e remotas. Isso pode ser útil ao depurar, especialmente se você tiver métodos assíncronos que chamam outros métodos assíncronos. Por exemplo, uma exceção de downloadImage produzirá uma exceção como esta:

RuntimeException: error downloading image at downloadImage(Main.java:35) at ---continuation---.(repeated:1) at errorHandlingAsync$1.doTry(Main.java:24) at ---continuation---.(repeated:1) …

Semântica de TryCatchFinally

A execução de um programa AWS Flow Framework para Java pode ser visualizada como uma árvore de ramificações de execução simultânea. Uma chamada para um método assíncrono, para uma atividade e para o próprio TryCatchFinally cria uma nova ramificação nessa árvore de execução. Por exemplo, o fluxo de trabalho de processamento de imagem pode ser visualizado como na árvore mostrada na figura a seguir.

Árvore de execução assíncrona

Um erro em uma ramificação de execução causa o desenrolamento da ramificação, assim como uma exceção causa o desenrolamento da pilha de chamada em um programa Java. O desenrolamento continua movendo a movimentação da ramificação de execução para cima até que o erro seja tratado ou a raiz da árvore seja acessada, nesse caso a execução do fluxo de trabalho é encerrada.

A estrutura relata erros que acontecem durante o processamento de tarefas como exceções. Ela associa os manipuladores de exceção (métodos doCatch()) definidos em TryCatchFinally com todas as tarefas criadas pelo código no doTry() correspondente. Se uma tarefa falhar - por exemplo, devido a um tempo limite ou a uma exceção não tratada -, a exceção apropriada será levantada e o doCatch() correspondente será chamado para tratá-la. Para conseguir isso, a estrutura trabalha em conjunto com o Amazon SWF para propagar erros remotos e ressuscitá-los como exceções no contexto do chamador.

Cancelamento

Quando ocorre uma exceção em código síncrono, o controle salta diretamente para o bloco catch, ignorando qualquer código restante no bloco try. Por exemplo:

try { a(); b(); c(); } catch (Exception e) { e.printStackTrace(); }

Neste código, se b() gerar uma exceção, c() nunca será invocado. Compare isso com um fluxo de trabalho:

new TryCatch() { @Override protected void doTry() throws Throwable { activityA(); activityB(); activityC(); } @Override protected void doCatch(Throwable e) throws Throwable { e.printStackTrace(); } };

Nesse caso, todas as chamadas para activityA, activityB e activityC retornam com êxito e resultam na criação de três tarefas que serão executadas assincronamente. Digamos que posteriormente a tarefa para a activityB resulte em um erro. Esse erro é registrado no histórico pelo Amazon SWF. Para tratar esse erro, a estrutura primeiro tentará cancelar todas as outras tarefas originadas no escopo do mesmo doTry(), nesse caso, activityA e activityC. Quando todas essas tarefas forem concluídas (canceladas, com falha ou concluídas com êxito), o método doCatch() apropriado será invocado para tratar o erro.

Ao contrário do exemplo síncrono, onde c() nunca foi executado, activityC foi invocada e uma tarefa foi programada para execução. Portanto, a estrutura fará uma tentativa de cancelá-la, mas não há garantia de que ela será cancelada. O cancelamento não pode ser garantido porque a atividade pode já ter sido concluída, pode ignorar a solicitação de cancelamento ou pode falhar devido a um erro. Contudo, a estrutura fornece a garantia de que doCatch() é chamado somente depois que todas as tarefas iniciadas no doTry() correspondente foram concluídas. Também garante que doFinally() seja chamado somente depois que todas as tarefas iniciadas em doCatch() e doTry() foram concluídas. Se, por exemplo, as atividades no exemplo acima dependerem umas das outras, digamos que activityB dependa de activityA e activityC de activityB, o cancelamento de activityC será imediato porque não está programado no Amazon SWF até que activityB seja concluído:

new TryCatch() { @Override protected void doTry() throws Throwable { Promise<Void> a = activityA(); Promise<Void> b = activityB(a); activityC(b); } @Override protected void doCatch(Throwable e) throws Throwable { e.printStackTrace(); } };

Pulsação de atividade

O AWS Flow Framework para o mecanismo de cancelamento cooperativo do Java permite que as tarefas de atividade em voo sejam canceladas de forma graciosa. Quando o cancelamento é acionado, as tarefas que foram bloqueadas ou estão aguardando para serem atribuídas a um operador são canceladas automaticamente. Se, no entanto, uma tarefa já estiver atribuída a um operador, a estrutura solicitará que a atividade seja cancelada. A implementação da atividade deve tratar explicitamente essas solicitações de cancelamento. Isso é feito relatando a pulsação da atividade.

Relatar a pulsação permite que a implementação da atividade relate o progresso de uma tarefa de atividade em andamento, o que é útil para o monitoramento e permite que a atividade verifique se há solicitações de cancelamento. O método recordActivityHeartbeat gerará uma CancellationException se um cancelamento tiver sido solicitado. A implementação da atividade pode capturar essa exceção e responder à solicitação de cancelamento, ou pode ignorar a solicitação engolindo a exceção. Para honrar a solicitação de cancelamento, a atividade deve executar a limpeza desejada, se houver, e, em seguida gerar a CancellationException novamente. Quando essa exceção é gerada em uma implementação de atividade, a estrutura registra que a tarefa de atividade foi concluída em estado cancelado.

O exemplo a seguir mostra uma atividade que faz download e processa imagens. Se houver pulsação depois do processamento de cada imagem e se o cancelamento for solicitado, ele limpará e gerará a exceção novamente para reconhecer o cancelamento.

@Override public void processImages(List<String> urls) { int imageCounter = 0; for (String url: urls) { imageCounter++; Image image = download(url); process(image); try { ActivityExecutionContext context = contextProvider.getActivityExecutionContext(); context.recordActivityHeartbeat(Integer.toString(imageCounter)); } catch(CancellationException ex) { cleanDownloadFolder(); throw ex; } } }

Relatar a pulsação da atividade não é necessário, mas é recomendável se a atividade for de longa execução ou estiver executando operações caras que você deseja cancelar em condições de erros. Você deve chamar heartbeatActivityTask periodicamente na implementação da atividade.

Se o tempo limite da atividade for esgotado, ActivityTaskTimedOutException será gerada, e getDetails no objeto de exceção retornará os dados passados para a última chamada bem-sucedida para heartbeatActivityTask para a tarefa de atividade correspondente. A implementação do fluxo de trabalho pode usar essas informações para determinar quanto de progresso foi feito antes do tempo limite da tarefa de atividade ter esgotado.

nota

Não é uma boa prática fazer heartbeat com muita frequência, pois o Amazon SWF pode limitar as solicitações de heartbeat. Consulte o Guia do desenvolvedor do Amazon Simple Workflow Service para obter informações sobre os limites impostos pelo Amazon SWF.

Cancelamento explícito de uma tarefa

Além das condições de erro, há outros casos em você pode cancelar explicitamente uma tarefa. Por exemplo, uma atividade para processar pagamentos usando um cartão de crédito pode precisar ser cancelada se o usuário cancelar o pedido. A estrutura permite que você cancele explicitamente tarefas criadas no escopo de um TryCatchFinally. No exemplo a seguir, a tarefa de pagamento será cancelada se um sinal for recebido enquanto o pagamento estava sendo processado.

public class OrderProcessorImpl implements OrderProcessor { private PaymentProcessorClientFactory factory = new PaymentProcessorClientFactoryImpl(); boolean processingPayment = false; private TryCatchFinally paymentTask = null; @Override public void processOrder(int orderId, final float amount) { paymentTask = new TryCatchFinally() { @Override protected void doTry() throws Throwable { processingPayment = true; PaymentProcessorClient paymentClient = factory.getClient(); paymentClient.processPayment(amount); } @Override protected void doCatch(Throwable e) throws Throwable { if (e instanceof CancellationException) { paymentClient.log("Payment canceled."); } else { throw e; } } @Override protected void doFinally() throws Throwable { processingPayment = false; } }; } @Override public void cancelPayment() { if (processingPayment) { paymentTask.cancel(null); } } }

Recebimento de notificação de tarefas canceladas

Quando uma tarefa é concluída em estado cancelado, a estrutura informa a lógica do fluxo de trabalho gerando uma CancellationException. Quando uma atividade é concluída em estado cancelado, é feito um registro no histórico, e a estrutura chama o doCatch() apropriado com uma CancellationException. Conforme mostrado no exemplo anterior, quando a tarefa de processamento de pagamento é cancelada, o fluxo de trabalho recebe uma CancellationException

Uma CancellationException não tratada é propagada para cima na ramificação de execução como qualquer outra exceção. No entanto, o método doCatch() receberá a CancellationException apenas se não houver outra exceção no escopo. Outras exceções têm prioridade mais alta que um cancelamento.

TryCatchFinally aninhado

Você pode aninhar TryCatchFinallys para atender às suas necessidades. Como cada TryCatchFinally cria uma nova ramificação na árvore de execução, é possível criar escopos aninhados. As exceções no escopo pai provocarão tentativas de cancelamento de todas as tarefas iniciadas pelos TryCatchFinallys aninhados dentro dele. No entanto, as exceções em TryCatchFinally aninhado não são propagadas automaticamente para o pai. Para propagar uma exceção de um TryCatchFinally aninhado para o TryCatchFinally que o contém, você deve gerar novamente a exceção em doCatch(). Ou seja, apenas as exceções não tratadas são movidas para acima, assim como o try/catch do Java. Se você cancelar um TryCatchFinally aninhado chamando o método de cancelamento, o TryCatchFinally aninhado será cancelado, mas o TryCatchFinally que o contém não será cancelado automaticamente.

TryCatchFinally aninhado
new TryCatch() { @Override protected void doTry() throws Throwable { activityA(); new TryCatch() { @Override protected void doTry() throws Throwable { activityB(); } @Override protected void doCatch(Throwable e) throws Throwable { reportError(e); } }; activityC(); } @Override protected void doCatch(Throwable e) throws Throwable { reportError(e); } };