Gestione errori - AWS Flow Framework per Java

Le traduzioni sono generate tramite traduzione automatica. In caso di conflitto tra il contenuto di una traduzione e la versione originale in Inglese, quest'ultima prevarrà.

Gestione errori

Il costrutto try/catch/finally in Java semplifica la gestione degli errori ed è quindi utilizzato diffusamente. Consente di associare gestori di errori a un blocco di codice. Internamente, ciò avviene aggiungendo ulteriori metadati sui gestori di errori allo stack di chiamate. Quando viene generata un'eccezione, il runtime cerca un gestore di errori associato nello stack di chiamate e lo richiama; se non lo trova, propaga l'eccezione fino alla catena di chiamate.

Questo processo è appropriato per il codice sincrono, ma la gestione degli errori in programmi distribuiti e asincroni è più complesso. Poiché una chiamata asincrona restituisce immediatamente un valore, il chiamante non è nello stack di chiamate quando il codice asincrono viene eseguito. Ciò significa che le eccezioni non gestite nel codice asincrono non possono essere gestite dal chiamante nel modo usuale. In genere, le eccezioni generate nel codice asincrono sono gestite passando lo stato di errore a un callback che viene passato a un metodo asincrono. Se in alternativa si utilizza Future<?>, viene restituito un errore quando tenti di accedervi. Questo processo non è ideale in quanto il codice che riceve l'eccezione (il callback o il codice che utilizza Future<?>) non dispone del contesto della chiamata originale e può non essere in grado di gestire l'eccezione in modo adeguato. Inoltre, in un sistema asincrono distribuito in cui i componenti sono eseguiti simultaneamente, possono verificarsi più errori contemporaneamente. Questi errori possono essere di tipo e gravità differenti e devono essere gestiti in modo appropriato.

Anche la pulizia delle risorse dopo una chiamata asincrona risulta alquanto complessa. A differenza del codice sincrono, non puoi utilizzare try/catch/finally nel codice di chiamata per pulire le risorse in quanto l'operazione iniziata nel blocco try potrebbe essere ancora in corso quando il blocco finally viene eseguito.

Il framework fornisce un meccanismo che rende la gestione degli errori nel codice asincrono distribuito simile ai blocchi Java try/catch/finally e quasi altrettanto semplice.

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

Il funzionamento della classe TryCatchFinally e delle relative varianti, ovvero TryFinally e TryCatch, è simile a quello dei blocchi Java try/catch/finally. Tale classe consente di associare i gestori di eccezioni a blocchi di codice di flusso di lavoro che possono essere eseguiti come task asincroni e remoti. Il metodo doTry() è equivalente, a livello di logica, al blocco try. Il framework esegue automaticamente il codice in doTry(). Un elenco di oggetti Promise può essere passato al costruttore di TryCatchFinally. Il metodo doTry sarà eseguito quanto tutti gli oggetti Promise passati al costruttore diventano pronti. Se un'eccezione viene generata dal codice richiamato in modo asincrono da doTry(), tutto il lavoro in sospeso in doTry() viene annullato e doCatch() viene chiamato per gestire l'eccezione. Ad esempio, nell'elenco qui sopra, se downloadImage genera un'eccezione, createThumbnail e uploadImage verranno annullati. Infine, doFinally() viene chiamato quando tutto il lavoro asincrono risulta terminato (completato, non riuscito o annullato). Questo metodo può essere utilizzato per la pulizia delle risorse. Puoi inoltre nidificare queste classi in base alle esigenze aziendali.

Quando un'eccezione è restituita in doCatch(), il framework fornisce uno stack di chiamate logiche che include chiamate asincrone e remote. Ciò può rivelarsi utile per il debug, soprattutto se hai dei metodi asincroni che chiamano altri metodi asincroni. Ad esempio, un'eccezione da downloadImage genererà un'eccezione come quella riportata di seguito:

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

Semantica di TryCatchFinally

L'esecuzione di unAWS Flow Frameworkper il programma Java può essere visualizzato come una struttura ad albero di rami eseguiti simultaneamente. Una chiamata a un metodo asincrono, a un'attività e a TryCatchFinally crea un nuovo ramo in tale struttura. Ad esempio, il flusso di lavoro di elaborazione di immagini può essere rappresentato dalla struttura ad albero illustrata di seguito.

Struttura di esecuzione asincrona

Un errore in un ramo dell'esecuzione comporterà la rimozione di quel ramo, proprio come un'eccezione provoca la rimozione dello stack di chiamate in un programma Java. La rimozione risale lungo il ramo di esecuzione fino a che l'errore viene gestito o viene raggiunta la radice della struttura ad albero, nel qual caso l'esecuzione di flusso di lavoro viene terminata.

Il framework segnala gli errori che si verificano durante l'elaborazione di task come eccezioni. Associa i gestori di eccezioni (metodi doCatch()) definiti in TryCatchFinally a tutti i task creati dal codice nel metodo doTry() corrispondente. Se un'attività non riesce, ad esempio a causa di un timeout o di un'eccezione non gestita, verrà sollevata l'eccezione appropriata e la corrispondentedoCatch()verrà invocato per gestirlo. Per eseguire questa operazione, il framework e propagano gli errori remoti e li ripristinano come eccezioni nel contesto del chiamante.

Annullamento

Quando si verifica un'eccezione nel codice sincrono, il controllo passa direttamente al blocco catch, ignorando il codice rimanente nel blocco try. Ad esempio:

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

In questo codice, se b() genera un'eccezione, c() non viene mai richiamato. Facciamo un raffronto con un flusso di lavoro:

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

In questo caso, le chiamate a activityA, activityB e activityC hanno esito positivo e comportano la creazione di tre task che vengono eseguiti in modo asincrono. Supponiamo che successivamente il task per activityB restituisca un errore. Tale errore viene registrato nella cronologia da Amazon SWF. Per gestirlo, il framework dapprima tenterà di annullare tutti gli altri task originati nell'ambito dello stesso doTry(); in questo caso, activityA e activityC. Quanto tutti i task risultano terminati (annullati, non riusciti o completati), il metodo doCatch() appropriato verrà richiamato per gestire l'errore.

A differenza dell'esempio sincrono, dove c() non è mai stato eseguito, activityC è stato richiamato e un task è stato pianificato per l'esecuzione. Di conseguenza, il framework effettuerà un tentativo per annullarlo, ma non è garantito che tale operazione riesca. L'annullamento non è certo in quanto l'attività può essere già stata completata, può ignorare la richiesta di annullamento o può non riuscire a causa di un errore. Il framework garantisce tuttavia che la chiamata del metodo doCatch() verrà effettuata solo dopo il completamento di tutti i task avviati dal metodo doTry() corrispondente. Garantisce inoltre la chiamata di doFinally() solo dopo il completamento di tutti i task avviati da doTry() e doCatch(). Se ad esempio le attività nell'esempio precedente dipendono le une dalle altreactivityBdipende daactivityAeactivityCsulactivityB, quindi la cancellazione diactivityCsarà immediato perché non è pianificato in Amazon SWF fino aactivityBcompleta:

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

Heartbeat dell'attività

LaAWS Flow Frameworkper il meccanismo di annullamento cooperativo di Java di consente di annullare normalmente i task di attività in elaborazione. Quando si avvia l'annullamento, i task bloccati o in attesa di essere assegnati a un lavoratore vengono annullati automaticamente. Se, tuttavia, un task è già assegnato a un lavoratore, il framework richiederà all'attività di annullarlo. L'implementazione di attività deve gestire in modo esplicito queste richieste di annullamento. Ciò viene eseguito mediante la segnalazione dell'heartbeat dell'attività.

La segnalazione dell'heartbeat consente all'implementazione di attività di comunicare l'avanzamento di un task di attività in corso, il che è utile per il monitoraggio, e all'attività di verificare l'esistenza di richieste di annullamento. Il metodo recordActivityHeartbeat genera un'eccezione CancellationException se un annullamento è stato richiesto. L'implementazione di attività può rilevare questa eccezione e agire sulla richiesta di annullamento oppure può ignorare la richiesta non tenendo conto dell'eccezione. Per soddisfare la richiesta di cancellazione, l'attività deve eseguire l'eventuale pulizia desiderata e quindi generare di nuovo CancellationException. Quando questa eccezione viene generata a partire da un'implementazione di attività, il framework registra che il task di attività è stato completato con lo stato annullato.

L'esempio seguente mostra un'attività che scarica ed elabora immagini. L'attività genera l'heartbeat dopo l'elaborazione di ogni immagine e se viene richiesto l'annullamento, esegue la pulizia e genera di nuovo l'eccezione per confermare l'annullamento.

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

La segnalazione dell'heartbeat dell'attività non è necessaria, ma è consigliata se l'attività è a esecuzione prolungata o se esegue operazioni dispendiose che intendi annullare in condizioni di errore. Devi chiamare heartbeatActivityTask periodicamente a partire dall'implementazione di attività.

In caso di timeout dell'attività, verrà generata l'eccezione ActivityTaskTimedOutException e getDetails sull'oggetto eccezione restituirà i dati passati all'ultima chiamata a heartbeatActivityTask riuscita per il task di attività corrispondente. L'implementazione di flusso di lavoro può utilizzare queste informazioni per determinare l'avanzamento prima del timeout del task di attività.

Nota

Non è consigliabile generare heartbeat troppo spesso in quanto Amazon SWF può limitare le richieste di heartbeat. Consultare ilAmazon Simple Workflow Serviceper limiti inoltrati da Amazon SWF.

Annullamento esplicito di un task

Oltre alle condizioni di errore, vi sono altri casi in cui puoi annullare esplicitamente un task. Ad esempio, è possibile che un'attività per l'elaborazione di pagamenti mediante una carta di credito debba essere annullata se l'utente annulla l'ordine. Il framework ti consente di annullare esplicitamente i task creati nell'ambito di una classe TryCatchFinally. Nell'esempio seguente, il task di pagamento viene annullato se si riceve un segnale durante l'elaborazione del pagamento.

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

Ricezione di notifiche relative a task annullati

Se un task viene completato quando lo stato è annullato, il framework informa la logica di flusso di lavoro generando un'eccezione CancellationException. Se un'attività viene completata quando lo stato è annullata, un record viene creato nella cronologia e il framework chiama il metodo doCatch() appropriato con un'eccezione CancellationException. Come mostrato nell'esempio precedente, quando il task di elaborazione del pagamento viene annullato, il workflow riceve un'eccezione CancellationException.

Un'eccezione CancellationException non gestita viene propagata nel ramo di esecuzione come avviene con qualsiasi altra eccezione. Tuttavia, il metodo doCatch() riceverà l'eccezione CancellationException solo se non vi sono altre eccezioni nell'ambito, in quanto la priorità delle altre eccezioni è superiore a quella dell'annullamento.

TryCatchFinally nidificata

Puoi nidificare la classe TryCatchFinally in funzione delle tue esigenze. Poiché ogni TryCatchFinally crea un nuovo ramo nella struttura di esecuzione, puoi creare ambiti nidificati. Le eccezioni nell'ambito padre comporteranno tentativi di annullamento di tutti i task avviati dalle classi TryCatchFinally nidificate nell'ambito. Tuttavia, le eccezioni in una classe TryCatchFinally nidificata non vengono propagate automaticamente al padre. Se desideri propagare un'eccezione da una classe TryCatchFinally nidificata alla classe TryCatchFinally che la contiene, devi generare di nuovo l'eccezione in doCatch(). In altre parole, solo le eccezioni non gestite sono propagate, esattamente come i blocchi Java try/catch. Se annulli una classe TryCatchFinally nidificata chiamando il metodo Cancel, la classe TryCatchFinally nidificata verrà annullata ma non la classe TryCatchFinally che la contiene.

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