Control de errores - AWS Flow Framework para Java

Las traducciones son generadas a través de traducción automática. En caso de conflicto entre la traducción y la version original de inglés, prevalecerá la version en inglés.

Control de errores

La construcción try/catch/finally en Java facilita el control de errores y se utiliza de manera generalizada. Le permite asociar controladores de errores a un bloque de código. A nivel interno, esto se produce introduciendo metadatos adicionales sobre los controladores de errores en la pila de llamadas. Cuando se genera una excepción, el tiempo de ejecución busca en la pila de llamadas un controlador de errores asociado para invocarlo y, si no encuentra ninguno adecuado, propaga la excepción hacia arriba en la cadena de llamadas.

Esto funciona correctamente en el caso de código sincrónico, pero controlar los errores en programas asíncronos y distribuidos es más complicado. Como las llamadas asíncronas devuelven un valor inmediatamente, el intermediario no se encuentra en la pila de la llamada cuando se ejecuta el código asíncrono. Esto significa que el intermediario no puede controlar de la manera habitual las excepciones no controladas en el código asíncrono. Normalmente, las excepciones que se generan en el código asíncrono se controlan transfiriendo un estado de error a una devolución de llamada que se transfiere al método asíncrono. Por otro lado, si se utiliza Future<?>, notificará un error cuando intente obtener acceso a él. No es la solución idónea, ya que el código que recibe la excepción (la devolución de llamada o el código que utiliza Future<?>) no tiene el contexto de la llamada original y quizás no pueda controlar la excepción correctamente. Además, en un sistema asíncrono distribuido, donde hay componentes que se ejecutan simultáneamente, se puede producir más de un error a la vez. Estos errores pueden ser de varios tipos y tener distintos niveles de gravedad, por lo que es preciso controlarlos de manera adecuada.

Tampoco es tarea fácil limpiar recursos después de una llamada asíncrona. A diferencia de lo que sucede con el código sincrónico, no es posible utilizar try/catch/finally en el código de llamada para limpiar recursos, ya que es posible que el trabajo iniciado en el bloque try siga en curso cuando el bloque finally se ejecute.

Este marco proporciona un mecanismo que hace que el control de errores en el código asíncrono distribuido sea similar al bloque try/catch/finally de Java, y que resulte casi tan sencillo.

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(); } }; }

La clase TryCatchFinally y sus variantes, TryFinally y TryCatch, funcionan de un modo similar a try/catch/finally de Java. Al utilizarla puede asociar controladores de excepciones a bloques de código de flujo de trabajo que se pueden ejecutar como tareas asíncronas y remotas. El método doTry() es lógicamente equivalente al bloque try. El marco ejecuta automáticamente el código en doTry(). Se puede transferir una lista de objetos Promise al constructor de TryCatchFinally. El método doTry se ejecutará cuando estén listos todos los objetos Promise que se hayan transferido al constructor. Si genera una excepción mediante código invocado de manera asíncrona desde dentro de doTry(), las tareas pendientes que haya en doTry() se cancelan y se llama a doCatch() para controlar la excepción. Por ejemplo, en la lista anterior, si downloadImage genera una excepción, se cancelarán createThumbnail y uploadImage. Por último, se llama a doFinally() cuando se ha llevado a cabo todo el trabajo asíncrono (completado, erróneo o cancelado). Se puede utilizar para limpiar recursos. También es posible anidar estas clases en función de sus necesidades.

Cuando se notifica una excepción en doCatch(), el marco ofrece una pila de llamada lógica completa que incluye llamadas asíncronas y remotas. Esto puede resultar útil para el proceso de depuración, especialmente si hay métodos asíncronos que llaman a otros métodos asíncronos. Por ejemplo, una excepción de downloadImage generará una excepción de este tipo:

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

La ejecución de un programa del AWS Flow Framework para Java puede visualizarse como un árbol de ramas que se ejecutan simultáneamente. Si se llama a un método asíncrono, a una actividad y al propio TryCatchFinally, se crea una nueva rama en este árbol de ejecuciones. Por ejemplo, en la siguiente figura se puede ver el flujo de trabajo de procesamiento de imágenes en forma de árbol.

Árbol de ejecución asíncrona

Si se produce un error en una rama de ejecución, la rama volverá atrás, al igual que una excepción hace que la pila de la llamada vuelva hacia atrás en un programa de Java. El proceso de marcha atrás sigue avanzando por la rama de ejecución hasta que, bien se controla el error, o se llega a la raíz del árbol, en cuyo caso finalizaría la ejecución del flujo de trabajo.

El marco de trabajo notifica los errores que se producen al procesar las tareas como excepciones. Asocia los controladores de excepciones (métodos doCatch()) definidos en TryCatchFinally con todas las tareas que crea el código en el correspondiente doTry(). Si una tarea da error, por ejemplo, porque se agote el tiempo de espera o porque haya una excepción sin gestionar, se generará la excepción pertinente y se invocará al doCatch() correspondiente para gestionarla. Para ello, el marco de trabajo funciona de forma conjunta con Amazon SWF para propagar los errores remotos y recuperarlos como excepciones en el contexto de quien realiza la llamada.

Cancelación

Cuando se produce una excepción en el código sincrónico, el control pasa directamente al bloque catch, omitiendo el resto del código en el bloque try. Por ejemplo:

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

En este código, si b() genera una excepción, nunca se invocará a c(). Comparémoslo con un flujo de trabajo:

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

En este caso, las llamadas a activityA, activityB y activityC devuelven todas valores correctamente y hacen que se creen tres tareas que se ejecutarán de forma asíncrona. Supongamos que posteriormente la tarea para activityB genera un error. Amazon SWF registra este error en el historial. Para controlar el error, en primer lugar el marco intentará cancelar el resto de las tareas que se originaron en el ámbito del mismo doTry() (en este caso, activityA y activityC). Una vez finalizadas todas estas tareas (canceladas, erróneas o completadas correctamente), se invocará al método doCatch() adecuado para controlar el error.

Al contrario que en el ejemplo del código sincrónico, donde nunca se ejecutó c(), se invocó a activityC y se programó una tarea para su ejecución. El marco intentará cancelarla, pero no hay garantías de que se cancele. No se puede garantizar que se cancele, porque puede que la actividad ya se haya completado, no haya tenido en cuenta la solicitud de cancelación o haya dado error. Sin embargo, el marco garantiza que se llame al método doCatch() únicamente cuando hayan finalizado las tareas iniciadas desde el correspondiente método doTry(). También garantiza que se llame al método doFinally() solo cuando hayan finalizado todas las tareas iniciadas desde los métodos doTry() y doCatch(). Si, por ejemplo, las actividades del ejemplo anterior dependen una de otra, supongamos que activityB depende de activityA y activityC de activityB, la cancelación de activityC será inmediata porque no se programará en Amazon SWF hasta que finalice activityB:

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(); } };

Latido de actividades

El mecanismo de cancelación cooperativo del AWS Flow Framework para Java permite cancelar correctamente tareas de actividades en tránsito. Cuando se pone en marcha una cancelación, se cancelan automáticamente las tareas que estaban bloqueadas o que estaban en espera de ser asignadas a un trabajo. Sin embargo, si ya se ha asignado la tarea a un trabajo, el marco solicitará que se cancele la actividad. La implementación de la actividad debe controlar de forma explícita estas solicitudes de cancelación. Esto se realiza a través de la notificación de los latidos de su actividad.

La notificación de los latidos permite a la implementación de la actividad notificar el progreso de una tarea de actividad en curso, lo que resulta muy útil para monitorizar y permite a la actividad comprobar la existencia de solicitudes de cancelación. El método recordActivityHeartbeat generará una excepción CancellationException si se ha solicitado una cancelación. La implementación de la actividad puede detectar esta excepción y actuar según la solicitud de cancelación, o bien puede no tener en cuenta la solicitud integrando la excepción. Para respetar la solicitud de cancelación, la actividad debe realizar la limpieza deseada, si la hubiese, y volver a generar la excepción CancellationException. Cuando se genera esta excepción desde la implementación de una actividad, el marco registra que la tarea de actividad ha finalizado con el estado de cancelada.

En el ejemplo siguiente se muestra una actividad que descarga y procesa imágenes. Produce latidos después de procesar cada una de las imágenes y, si se solicita una cancelación, limpia y vuelve a generar la excepción para que se confirme la cancelación.

@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; } } }

Notificar los latidos de las actividades no es obligatorio, pero se recomienda hacerlo si su actividad se ejecuta durante mucho tiempo o si va a realizar operaciones exhaustivas que desearía cancelar en caso de error. Debería llamar a heartbeatActivityTask periódicamente desde la implementación de la actividad.

Si se agota el tiempo de espera de la actividad, se generará la excepción ActivityTaskTimedOutException, y el método getDetails en el objeto de la excepción devolverá los datos transferidos a la última llamada a heartbeatActivityTask realizada correctamente para la correspondiente tarea de actividad. La implementación del flujo de trabajo puede utilizar esta información para determinar los avances realizados antes de que se agotase el tiempo de espera de la tarea de actividad.

nota

No es conveniente aplicar latidos con excesiva frecuencia, ya que Amazon SWF podría limitar las solicitudes de latidos. Consulte la Guía para desarrolladores de Amazon Simple Workflow Service para conocer los límites establecidos en Amazon SWF.

Cancelación explícita de una tarea

Además de las condiciones de error, hay otros casos en que es preciso cancelar de forma explícita una tarea. Por ejemplo, es posible que sea necesario cancelar una actividad para procesar pagos mediante tarjeta de crédito si el usuario cancela la orden. El marco le permite cancelar explícitamente las tareas creadas en el ámbito de un método TryCatchFinally. En el siguiente ejemplo se cancela una tarea de pago cuando se recibe una señal mientras se procesa el pago.

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); } } }

Recepción de notificaciones de tareas canceladas

Cuando una tarea finaliza con estado de cancelada, el marco informa a la lógica del flujo de trabajo generando una excepción CancellationException. Cuando una actividad se completa con estado de cancelada, se crea un registro en el historial y el marco llama al método doCatch() adecuado con una excepción CancellationException. Tal como se muestra en el ejemplo anterior, cuando se cancela una tarea de procesamiento de pagos, el flujo de trabajo recibe una excepción CancellationException.

Una excepción CancellationException no controlada se propaga hasta la rama de ejecución, como cualquier otra excepción. Sin embargo, el método doCatch() recibirá la excepción CancellationException únicamente si no hay otra excepción en ese ámbito, dado que hay otras excepciones con mayor prioridad que la cancelación.

TryCatchFinally anidado

Puede anidar el método TryCatchFinally para adaptarlo a sus necesidades. Como cada método TryCatchFinally crea una nueva rama en el árbol de ejecución, puede crear ámbitos anidados. Las excepciones en el ámbito principal producirán intentos de cancelación de todas las tareas iniciadas por el método TryCatchFinally anidado en su interior. Sin embargo, las excepciones contenidas en un método TryCatchFinally anidado no se propagan automáticamente al principal. Si desea propagar una excepción desde un TryCatchFinally anidado al método TryCatchFinally que lo contiene, deberá volver a generar la excepción en doCatch(). Dicho de otro modo, solo se desarrollan las excepciones no controladas, igual que try/catch en Java. Si cancela un TryCatchFinally anidado llamando al método de cancelación, se cancelará el TryCatchFinally anidado, pero el TryCatchFinally que lo contiene no se cancelará automáticamente.

TryCatchFinally anidado
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); } };