From 5d0a6b40b99502a048a47889cf8ea4663af101f9 Mon Sep 17 00:00:00 2001 From: Erki Ehtla <8049910+erkieh@users.noreply.github.com> Date: Sun, 30 Nov 2025 19:32:57 +0200 Subject: [PATCH] handle non serializable exceptions --- .../exceptions/DbosSerializableException.java | 19 ++++++ .../java/dev/dbos/transact/json/JSONUtil.java | 30 ++++++--- .../transact/workflow/AsyncWorkflowTest.java | 63 +++++++++++++++++++ .../dbos/transact/workflow/SimpleService.java | 6 ++ .../transact/workflow/SimpleServiceImpl.java | 25 ++++++++ .../transact/workflow/SyncWorkflowTest.java | 42 +++++++++++++ 6 files changed, 175 insertions(+), 10 deletions(-) create mode 100644 transact/src/main/java/dev/dbos/transact/exceptions/DbosSerializableException.java diff --git a/transact/src/main/java/dev/dbos/transact/exceptions/DbosSerializableException.java b/transact/src/main/java/dev/dbos/transact/exceptions/DbosSerializableException.java new file mode 100644 index 00000000..917b1e28 --- /dev/null +++ b/transact/src/main/java/dev/dbos/transact/exceptions/DbosSerializableException.java @@ -0,0 +1,19 @@ +package dev.dbos.transact.exceptions; + +/** + * {@code DbosSerializableException} is used to serialize the exception when the original one is not + * serializable. + */ +public class DbosSerializableException extends RuntimeException { + private final String originalClassName; + + public DbosSerializableException(Throwable throwable) { + super(throwable.getMessage()); + originalClassName = throwable.getClass().getName(); + setStackTrace(throwable.getStackTrace()); + } + + public String getOriginalClassName() { + return originalClassName; + } +} diff --git a/transact/src/main/java/dev/dbos/transact/json/JSONUtil.java b/transact/src/main/java/dev/dbos/transact/json/JSONUtil.java index ec1f0927..8dbe6821 100644 --- a/transact/src/main/java/dev/dbos/transact/json/JSONUtil.java +++ b/transact/src/main/java/dev/dbos/transact/json/JSONUtil.java @@ -1,14 +1,9 @@ package dev.dbos.transact.json; import dev.dbos.transact.conductor.Conductor; +import dev.dbos.transact.exceptions.DbosSerializableException; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.StreamCorruptedException; -import java.io.UncheckedIOException; +import java.io.*; import java.util.Arrays; import java.util.Base64; import java.util.List; @@ -127,12 +122,27 @@ public static WireThrowable toWire(Throwable t, Map extra, Strin w.node = node; w.extra = (extra == null) ? Map.of() : extra; - byte[] javaSer = javaSerialize(t); - String b64 = Base64.getEncoder().encodeToString(javaSer); - w.base64bytes = b64; + w.base64bytes = serializeThrowable(t); return w; } + private static String serializeThrowable(Throwable t) { + try { + byte[] javaSer; + try { + javaSer = javaSerialize(t); + } catch (Exception e) { + logger.warn("Failed to serialize Throwable", e); + RuntimeException o = new DbosSerializableException(t); + javaSer = javaSerialize(o); + } + return Base64.getEncoder().encodeToString(javaSer); + } catch (Exception e) { + logger.error("Failed to serialize Throwable", e); + return null; + } + } + public static Throwable toThrowable(WireThrowable w, ClassLoader loader) throws IOException, ClassNotFoundException { if (w.base64bytes == null) throw new IllegalArgumentException("No serialized payload"); diff --git a/transact/src/test/java/dev/dbos/transact/workflow/AsyncWorkflowTest.java b/transact/src/test/java/dev/dbos/transact/workflow/AsyncWorkflowTest.java index 38d7a361..3b963b10 100644 --- a/transact/src/test/java/dev/dbos/transact/workflow/AsyncWorkflowTest.java +++ b/transact/src/test/java/dev/dbos/transact/workflow/AsyncWorkflowTest.java @@ -126,6 +126,69 @@ public void workflowWithError() throws Exception { assertEquals(WorkflowState.ERROR.name(), handle.getStatus().status()); } + @Test + public void workWithNonSerializableException() throws SQLException { + SimpleService simpleService = + DBOS.registerWorkflows(SimpleService.class, new SimpleServiceImpl()); + + DBOS.launch(); + + String wfid = "abc"; + WorkflowHandle handle = + DBOS.startWorkflow( + () -> { + simpleService.workWithNonSerializableException(); + return null; + }, + new StartWorkflowOptions(wfid)); + + var e = assertThrows(Exception.class, () -> handle.getResult()); + assertEquals("workNonSerializableException error", e.getMessage()); + + List wfs = DBOS.listWorkflows(new ListWorkflowsInput()); + assertEquals(1, wfs.size()); + assertEquals(wfs.get(0).name(), "workNonSerializableException"); + assertNotNull(wfs.get(0).workflowId()); + assertEquals(wfs.get(0).workflowId(), handle.workflowId()); + assertEquals( + "dev.dbos.transact.workflow.SimpleServiceImpl$NonSerializableException", + wfs.get(0).error().className()); + assertEquals("workNonSerializableException error", wfs.get(0).error().message()); + assertNotNull(wfs.get(0).workflowId()); + } + + @Test + public void workWithNonSerializableExceptionInStep() throws SQLException { + SimpleService simpleService = + DBOS.registerWorkflows(SimpleService.class, new SimpleServiceImpl()); + simpleService.setSimpleService(simpleService); + + DBOS.launch(); + + String wfid = "abc"; + WorkflowHandle handle = + DBOS.startWorkflow( + () -> { + simpleService.workWithNonSerializableExceptionInStep(); + return null; + }, + new StartWorkflowOptions(wfid)); + + var e = assertThrows(Exception.class, () -> handle.getResult()); + assertEquals("stepWithNonSerializableException error", e.getMessage()); + + List wfs = DBOS.listWorkflows(new ListWorkflowsInput()); + assertEquals(1, wfs.size()); + assertEquals(wfs.get(0).name(), "workWithNonSerializableExceptionInStep"); + assertNotNull(wfs.get(0).workflowId()); + assertEquals(wfs.get(0).workflowId(), handle.workflowId()); + assertEquals( + "dev.dbos.transact.workflow.SimpleServiceImpl$NonSerializableException", + wfs.get(0).error().className()); + assertEquals("stepWithNonSerializableException error", wfs.get(0).error().message()); + assertNotNull(wfs.get(0).workflowId()); + } + @Test public void childWorkflowWithoutSet() throws Exception { diff --git a/transact/src/test/java/dev/dbos/transact/workflow/SimpleService.java b/transact/src/test/java/dev/dbos/transact/workflow/SimpleService.java index 2fd9dc63..8c08697d 100644 --- a/transact/src/test/java/dev/dbos/transact/workflow/SimpleService.java +++ b/transact/src/test/java/dev/dbos/transact/workflow/SimpleService.java @@ -8,6 +8,10 @@ public interface SimpleService { public void workWithError() throws Exception; + public void workWithNonSerializableException(); + + public void workWithNonSerializableExceptionInStep(); + public String parentWorkflowWithoutSet(String input); public String workflowWithMultipleChildren(String input) throws Exception; @@ -30,6 +34,8 @@ public interface SimpleService { void stepWithSleep(long sleepSeconds); + void stepWithNonSerializableException(); + String longParent(String input, long sleepSeconds, long timeoutSeconds) throws InterruptedException; diff --git a/transact/src/test/java/dev/dbos/transact/workflow/SimpleServiceImpl.java b/transact/src/test/java/dev/dbos/transact/workflow/SimpleServiceImpl.java index eef9bf8c..394afbc1 100644 --- a/transact/src/test/java/dev/dbos/transact/workflow/SimpleServiceImpl.java +++ b/transact/src/test/java/dev/dbos/transact/workflow/SimpleServiceImpl.java @@ -29,6 +29,16 @@ public void workWithError() throws Exception { throw new Exception("DBOS Test error"); } + @Workflow(name = "workNonSerializableException") + public void workWithNonSerializableException() { + throw new NonSerializableException("workNonSerializableException error"); + } + + @Workflow(name = "workWithNonSerializableExceptionInStep") + public void workWithNonSerializableExceptionInStep() { + simpleService.stepWithNonSerializableException(); + } + @Workflow(name = "parentWorkflowWithoutSet") public String parentWorkflowWithoutSet(String input) { String result = input; @@ -141,6 +151,11 @@ public void stepWithSleep(long sleepSeconds) { } } + @Step(name = "stepWithNonSerializableException") + public void stepWithNonSerializableException() { + throw new NonSerializableException("stepWithNonSerializableException error"); + } + @Workflow(name = "childWorkflowWithSleep") public String childWorkflowWithSleep(String input, long sleepSeconds) throws InterruptedException { @@ -210,4 +225,14 @@ public void startWfInStepById(String childId) { }, "startWfInStepById"); } + + private static class NonSerializableException extends RuntimeException { + private NonSerializableClass nonSerializableValue = new NonSerializableClass(); + + public NonSerializableException(String message) { + super(message); + } + + private static class NonSerializableClass {} + } } diff --git a/transact/src/test/java/dev/dbos/transact/workflow/SyncWorkflowTest.java b/transact/src/test/java/dev/dbos/transact/workflow/SyncWorkflowTest.java index 1165b7d0..8ac2bfa5 100644 --- a/transact/src/test/java/dev/dbos/transact/workflow/SyncWorkflowTest.java +++ b/transact/src/test/java/dev/dbos/transact/workflow/SyncWorkflowTest.java @@ -77,6 +77,48 @@ public void workflowWithError() throws SQLException { assertNotNull(wfs.get(0).workflowId()); } + @Test + public void workWithNonSerializableException() throws SQLException { + SimpleService simpleService = + DBOS.registerWorkflows(SimpleService.class, new SimpleServiceImpl()); + + DBOS.launch(); + + var e = assertThrows(Exception.class, () -> simpleService.workWithNonSerializableException()); + assertEquals("workNonSerializableException error", e.getMessage()); + + List wfs = DBOS.listWorkflows(new ListWorkflowsInput()); + assertEquals(1, wfs.size()); + assertEquals(wfs.get(0).name(), "workNonSerializableException"); + assertEquals( + "dev.dbos.transact.workflow.SimpleServiceImpl$NonSerializableException", + wfs.get(0).error().className()); + assertEquals("workNonSerializableException error", wfs.get(0).error().message()); + assertNotNull(wfs.get(0).workflowId()); + } + + @Test + public void workWithNonSerializableExceptionInStep() throws SQLException { + SimpleService simpleService = + DBOS.registerWorkflows(SimpleService.class, new SimpleServiceImpl()); + simpleService.setSimpleService(simpleService); + + DBOS.launch(); + + var e = + assertThrows(Exception.class, () -> simpleService.workWithNonSerializableExceptionInStep()); + assertEquals("stepWithNonSerializableException error", e.getMessage()); + + List wfs = DBOS.listWorkflows(new ListWorkflowsInput()); + assertEquals(1, wfs.size()); + assertEquals(wfs.get(0).name(), "workWithNonSerializableExceptionInStep"); + assertEquals( + "dev.dbos.transact.workflow.SimpleServiceImpl$NonSerializableException", + wfs.get(0).error().className()); + assertEquals("stepWithNonSerializableException error", wfs.get(0).error().message()); + assertNotNull(wfs.get(0).workflowId()); + } + @Test public void setWorkflowId() throws SQLException {