錯誤處理 - AWS Flow Framework 對於爪哇

本文為英文版的機器翻譯版本,如內容有任何歧義或不一致之處,概以英文版為準。

錯誤處理

Java 中的 try/catch/finally 建構可簡化錯誤處理,而且隨時可以使用。此建構可讓您建立錯誤處理器與程式碼區塊的關聯。就內部而言,其運作方式為填入呼叫堆疊上錯誤處理器的其他中繼資料。拋出例外狀況時,執行時間會查看相關聯錯誤處理器的呼叫堆疊,並呼叫之;如果找不到適當的錯誤處理器,則會將例外狀況傳播到呼叫鏈。

這適用於同步程式碼,但處理非同步和分散式程式中的錯誤會造成其他挑戰。因為立即傳回非同步呼叫,所以發起人在非同步程式碼執行時不在呼叫堆疊上。這表示發起人無法如常處理非同步程式碼中的未處理例外狀況。一般而言,會透過將錯誤狀態傳遞給已傳遞到非同步方法的回呼,來處理源自非同步程式碼的例外狀況。或者,如果使用 Future<?>,則會在您嘗試存取它時報告錯誤。這麼做比較不恰當,因為收到例外狀況的程式碼 (使用 Future<?> 的回呼或程式碼) 沒有原始呼叫的內容,而且可能無法適當地處理例外狀況。甚至,在分散式非同步系統中,如果並行執行元件,則可能會同時發生多個錯誤。這些錯誤可以是不同的類型和嚴重性,而且需要適當地加以處理。

在非同步呼叫之後清除資源也十分困難。與同步程式碼不同,您無法在呼叫程式碼中使用 try/catch/finally 來清除資源,因為在 finally 區塊執行時,try 區塊中所啟動的工作可能仍在進行中。

框架提供一種機制,讓分散式非同步程式碼中的錯誤處理類似 Java 的 try/catch/finally,而且幾乎像 Java 的 try/catch/finally 一樣簡單。

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

TryCatchFinally 類別與其變體 TryFinallyTryCatch 的運作方式與 Java 的 try/catch/finally 類似。使用它,即可建立例外狀況處理器與工作流程程式碼區塊的關聯,而工作流程程式碼區塊可能會以非同步與遠端任務執行。doTry() 方法在邏輯上等於 try 區塊。框架會自動執行 doTry() 中的程式碼。Promise 物件清單可以傳遞給 TryCatchFinally 的建構函數。所有傳入建構函數的 Promise 物件都準備就緒時,將會執行 doTry 方法。如果從 doTry() 非同步呼叫的程式碼引發例外狀況,則會取消 doTry() 中的任何待定工作,並呼叫 doCatch() 來處理例外狀況。例如,在上面的清單中,如果 downloadImage 拋出例外狀況,則會取消 createThumbnailuploadImage。最後,完成所有非同步工作 (已完成、失敗或已取消) 時,會呼叫 doFinally()。它可以用於資源清理。您也可以將這些類別巢狀處理,以符合您的需求。

doCatch() 中報告例外狀況時,框架會提供包含非同步和遠端呼叫的完整邏輯呼叫堆疊。偵錯時,這可能十分有用,特別是您有呼叫其他非同步方法的非同步方法時。例如,downloadImage 的例外狀況將會產生與下列類似的例外狀況:

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

TryCatchFinally 語意

執行AWS Flow Framework可以視覺化為並行執行中分支的樹狀目錄。非同步方法、活動和 TryCatchFinally 本身的呼叫會在此執行的樹狀目錄中建立新分支。例如,影像處理工作流程可以檢視為下圖中所顯示的樹狀目錄。

非同步執行樹狀目錄

某個執行分支中的錯誤將會導致回溯該分支,就像例外狀況導致回溯 Java 程式中的呼叫堆疊一樣。回溯會繼續移至執行分支,直到錯誤已處理或到達樹狀目錄根,在此情況下,會終止工作流程執行。

框架會報告將任務處理為例外狀況時所發生的錯誤。它會建立下列兩者的關聯:TryCatchFinally 中所定義的例外狀況處理器 (doCatch() 方法) 與對應 doTry() 中程式碼所建立的所有任務。如果任務失敗(例如,超時或未處理的例外狀況所造成),則會引發適當的例外狀況,而且會引發對應的doCatch()將被調用來處理它。為完成這項作業,框架會與 Amazon SWF 協力運作,以傳播遠端錯誤,並在發起人的內容中將遠端錯誤回復為例外狀況。

取消

同步程式碼中發生例外狀況時,控制會直接跳至 catch 區塊,並跳過 try 區塊中剩餘的程式碼。例如:

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

在此程式碼中,如果 b() 拋出例外狀況,則絕不會呼叫 c()。與工作流程比較:

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

在此情況下,activityAactivityBactivityC 呼叫都會順利傳回,並導致建立三個非同步執行的任務。假設稍後 activityB 的任務導致錯誤。Amazon SWF 會將此錯誤記錄在歷史記錄中。若要處理此情況,框架會先嘗試取消源自相同 doTry() 範圍的所有其他任務;在此情況下為 activityAactivityC。所有這類任務完成時 (取消、失敗或順利完成),都會呼叫適當的 doCatch() 方法來處理錯誤。

與永不執行 c() 的同步範例不同,會呼叫 activityC,並排定執行任務;因此,框架會嘗試予以取消,但不保證會確實取消。由於活動可能已經完成、可能忽略取消請求,或可能因錯誤而失敗,因而無法保證取消。不過,框架能夠保證只有在完成從對應 doTry() 啟動的所有任務之後,才會呼叫 doCatch()。也能保證只有在完成從 doTry()doCatch() 啟動的所有任務之後,才會呼叫 doFinally()。例如,如果上述範例中的活動彼此相依 (假設activityB取決於activityAactivityCactivityB,然後取消activityC將立即進行,因為它在 Amazon SWF 中沒有計劃直到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(); } };

活動訊號

所以此AWS Flow Framework的合作取消機制允許正常取消進行中的活動任務。觸發取消時,會自動取消封鎖或等待指派給工作者的任務。不過,如果任務已指派給工作者,則框架會請求取消活動。活動實作必須明確地處理這類取消請求。作法是報告您活動的活動訊號。

報告活動訊號允許活動實作報告進行中之活動任務 (對監控很有幫助) 的進度,且可讓活動檢查取消請求。如果已請求取消,則 recordActivityHeartbeat 方法將拋出 CancellationException。活動實作可以截獲此例外狀況,並處理取消請求,或者可以抑制例外狀況來忽略請求。若要使用取消請求,活動應該執行所需的清除 (如果有的話),然後重新拋出 CancellationException。從活動實作拋出此例外狀況時,框架會記錄已完成活動任務,但狀態為已取消。

下列範例顯示可下載和處理影像的活動。活動會在處理每個影像之後發出活動訊號,而且,如果請求取消,則會清除和重新拋出例外狀況來確認取消。

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

不需要報告活動訊號,但若您的活動是長時間執行,或可能會執行要在錯誤情況下取消的高成本操作時,則建議報告活動訊號。您應該從活動實作定期呼叫 heartbeatActivityTask

如果活動逾時,則會拋出 ActivityTaskTimedOutException,而且例外狀況物件上的 getDetails 會傳回資料,而這項資料會傳遞給對應活動任務的最後一個成功 heartbeatActivityTask 呼叫。工作流程實作可能會使用這項資訊來判斷活動任務逾時之前的進度。

注意

因為 Amazon SWF 可能會調節活動訊號請求,所以不適合過於頻繁地發出活動訊號。請參了解的Amazon Simple Workflow Service 開發者指南,瞭解 Amazon SWF 設定的限制。

明確地取消任務

除了錯誤情況外,您也可能在其他情況下明確取消任務。例如,如果使用者取消訂單,則可能需要取消使用信用卡處理付款的活動。框架可讓您明確地取消 TryCatchFinally 範圍中所建立的任務。在下列範例中,如果在處理付款時收到訊號,則會取消付款任務。

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

接收已取消之任務的通知

任務完成且處於已取消狀態時,框架會拋出 CancellationException 來通知工作流程邏輯。任務完成且處於已取消狀態時,會在歷史記錄中建立記錄,而且框架會以 CancellationException 呼叫適當的 doCatch()。如上述範例所示,在取消付款處理任務之時,工作流程會收到 CancellationException

未處理的 CancellationException 會傳播到執行分支,如同其他例外狀況。不過,只有在範圍中沒有其他例外狀況時,doCatch() 方法才會收到 CancellationException;其他例外狀況的優先順序高於取消。

巢狀 TryCatchFinally

您可以將 TryCatchFinally 巢狀處理,以符合您的需求。因為每個 TryCatchFinally 都會在執行樹狀目錄中建立新分支,所以您可以建立巢狀範圍。父範圍中的例外狀況將會導致嘗試取消透過在其內將 TryCatchFinally 巢狀處理而啟動的所有任務。不過,巢狀 TryCatchFinally 中的例外狀況不會自動傳播至父項。如果您想要將例外狀況從巢狀 TryCatchFinally 傳播至其內含的 TryCatchFinally,則應該在 doCatch() 中重新拋出例外狀況。換言之,只會發出未處理的例外狀況,就像 Java 的 try/catch 一樣。如果您呼叫取消方法來取消巢狀 TryCatchFinally,則會取消巢狀 TryCatchFinally,但不會自動取消內含的 TryCatchFinally

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