From 8b4de09b83bb43d81ea776e7ef47e5e24d37f9b8 Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Sun, 28 Jun 2026 18:48:58 +0530 Subject: [PATCH 1/3] feat(java): typed exception hierarchy Add SerializationException (+ CryptoException), WorkflowException, LockException, ConfigurationException, WebhookException under errors/, all extending TaskitoException, and route the SDK's throw sites to them so callers can catch a category or the base type. --- .../org/byteveda/taskito/DefaultTaskito.java | 12 +++++++----- .../java/org/byteveda/taskito/Taskito.java | 7 ++++--- .../taskito/dashboard/DashboardServer.java | 4 ++-- .../taskito/errors/ConfigurationException.java | 17 +++++++++++++++++ .../taskito/errors/CryptoException.java | 17 +++++++++++++++++ .../byteveda/taskito/errors/LockException.java | 17 +++++++++++++++++ .../taskito/errors/SerializationException.java | 18 ++++++++++++++++++ .../taskito/errors/WebhookException.java | 16 ++++++++++++++++ .../taskito/errors/WorkflowException.java | 18 ++++++++++++++++++ .../byteveda/taskito/errors/package-info.java | 10 ++++++++++ .../org/byteveda/taskito/events/EventName.java | 4 ++-- .../java/org/byteveda/taskito/locks/Lock.java | 4 ++-- .../org/byteveda/taskito/model/JobStatus.java | 6 +++--- .../serialization/EncryptedSerializer.java | 8 ++++---- .../taskito/serialization/JsonSerializer.java | 8 ++++---- .../serialization/SignedSerializer.java | 8 ++++---- .../byteveda/taskito/webhooks/Deliverer.java | 4 ++-- .../taskito/webhooks/WebhookManager.java | 4 ++-- .../taskito/webhooks/WebhookStore.java | 6 +++--- .../org/byteveda/taskito/worker/Worker.java | 7 ++++--- .../taskito/workflows/WorkflowRun.java | 11 ++++++----- .../taskito/workflows/WorkflowTracker.java | 15 ++++++++------- 22 files changed, 170 insertions(+), 51 deletions(-) create mode 100644 sdks/java/src/main/java/org/byteveda/taskito/errors/ConfigurationException.java create mode 100644 sdks/java/src/main/java/org/byteveda/taskito/errors/CryptoException.java create mode 100644 sdks/java/src/main/java/org/byteveda/taskito/errors/LockException.java create mode 100644 sdks/java/src/main/java/org/byteveda/taskito/errors/SerializationException.java create mode 100644 sdks/java/src/main/java/org/byteveda/taskito/errors/WebhookException.java create mode 100644 sdks/java/src/main/java/org/byteveda/taskito/errors/WorkflowException.java create mode 100644 sdks/java/src/main/java/org/byteveda/taskito/errors/package-info.java diff --git a/sdks/java/src/main/java/org/byteveda/taskito/DefaultTaskito.java b/sdks/java/src/main/java/org/byteveda/taskito/DefaultTaskito.java index 16734ba3..fccc9393 100644 --- a/sdks/java/src/main/java/org/byteveda/taskito/DefaultTaskito.java +++ b/sdks/java/src/main/java/org/byteveda/taskito/DefaultTaskito.java @@ -14,6 +14,8 @@ import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import org.byteveda.taskito.core.CoreFacade; +import org.byteveda.taskito.errors.SerializationException; +import org.byteveda.taskito.errors.WorkflowException; import org.byteveda.taskito.locks.Lock; import org.byteveda.taskito.locks.LockInfo; import org.byteveda.taskito.middleware.EnqueueContext; @@ -377,7 +379,7 @@ public WorkflowRun submitWorkflow(Workflow workflow, Map supplie // A payloadless structural step (stepAfter) must get its payload at // submit; fail fast rather than enqueue a null-payload job. if (payload == null && !suppliedPayloads.containsKey(step.name)) { - throw new TaskitoException("workflow step '" + step.name + throw new WorkflowException("workflow step '" + step.name + "' has no payload; supply one via submitWorkflow(wf, payloads)"); } payloadNames.add(step.name); @@ -547,7 +549,7 @@ private static String encode(Object value) { try { return VIEWS.writeValueAsString(value); } catch (Exception e) { - throw new TaskitoException("failed to encode request", e); + throw new SerializationException("failed to encode request", e); } } @@ -555,7 +557,7 @@ private static R decode(String json, Class type) { try { return VIEWS.readValue(json, type); } catch (Exception e) { - throw new TaskitoException("failed to decode native response", e); + throw new SerializationException("failed to decode native response", e); } } @@ -564,7 +566,7 @@ private static List decodeList(String json, Class element) { try { return VIEWS.readValue(json, type); } catch (Exception e) { - throw new TaskitoException("failed to decode native response", e); + throw new SerializationException("failed to decode native response", e); } } @@ -573,7 +575,7 @@ private static Map decodeMap(String json, Class value) { try { return VIEWS.readValue(json, type); } catch (Exception e) { - throw new TaskitoException("failed to decode native response", e); + throw new SerializationException("failed to decode native response", e); } } } diff --git a/sdks/java/src/main/java/org/byteveda/taskito/Taskito.java b/sdks/java/src/main/java/org/byteveda/taskito/Taskito.java index 3aeb767d..1b474f68 100644 --- a/sdks/java/src/main/java/org/byteveda/taskito/Taskito.java +++ b/sdks/java/src/main/java/org/byteveda/taskito/Taskito.java @@ -11,6 +11,7 @@ import java.util.Locale; import java.util.Map; import java.util.Optional; +import org.byteveda.taskito.errors.ConfigurationException; import org.byteveda.taskito.internal.JniQueueBackend; import org.byteveda.taskito.locks.Lock; import org.byteveda.taskito.locks.LockInfo; @@ -288,7 +289,7 @@ public Taskito open() { String dsn = (String) options.computeIfAbsent("dsn", key -> DEFAULT_SQLITE_DB); ensureSqliteParentDir(dsn); } else if (!options.containsKey("dsn")) { - throw new TaskitoException("url (dsn) is required"); + throw new ConfigurationException("url (dsn) is required"); } return new DefaultTaskito(JniQueueBackend.open(encodeOptions()), serializer); } @@ -305,7 +306,7 @@ private static void ensureSqliteParentDir(String dsn) { try { Files.createDirectories(parent); } catch (IOException e) { - throw new TaskitoException("failed to create sqlite directory " + parent, e); + throw new ConfigurationException("failed to create sqlite directory " + parent, e); } } @@ -313,7 +314,7 @@ private String encodeOptions() { try { return JSON.writeValueAsString(options); } catch (Exception e) { - throw new TaskitoException("failed to encode open options", e); + throw new ConfigurationException("failed to encode open options", e); } } } diff --git a/sdks/java/src/main/java/org/byteveda/taskito/dashboard/DashboardServer.java b/sdks/java/src/main/java/org/byteveda/taskito/dashboard/DashboardServer.java index 1b8ec515..ecee959f 100644 --- a/sdks/java/src/main/java/org/byteveda/taskito/dashboard/DashboardServer.java +++ b/sdks/java/src/main/java/org/byteveda/taskito/dashboard/DashboardServer.java @@ -22,7 +22,7 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; import org.byteveda.taskito.Taskito; -import org.byteveda.taskito.TaskitoException; +import org.byteveda.taskito.errors.SerializationException; import org.byteveda.taskito.model.Job; import org.byteveda.taskito.model.JobFilter; import org.byteveda.taskito.model.JobStatus; @@ -317,7 +317,7 @@ private static void respond(HttpExchange exchange, int status, Object body) thro try { out = JSON.writeValueAsBytes(body); } catch (Exception e) { - throw new TaskitoException("failed to encode response", e); + throw new SerializationException("failed to encode response", e); } exchange.getResponseHeaders().set("Content-Type", "application/json"); exchange.sendResponseHeaders(status, out.length); diff --git a/sdks/java/src/main/java/org/byteveda/taskito/errors/ConfigurationException.java b/sdks/java/src/main/java/org/byteveda/taskito/errors/ConfigurationException.java new file mode 100644 index 00000000..0f6cb5b5 --- /dev/null +++ b/sdks/java/src/main/java/org/byteveda/taskito/errors/ConfigurationException.java @@ -0,0 +1,17 @@ +package org.byteveda.taskito.errors; + +import org.byteveda.taskito.TaskitoException; + +/** + * The SDK was misconfigured — e.g. a required connection URL is missing, or a + * storage directory could not be created. + */ +public class ConfigurationException extends TaskitoException { + public ConfigurationException(String message) { + super(message); + } + + public ConfigurationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/sdks/java/src/main/java/org/byteveda/taskito/errors/CryptoException.java b/sdks/java/src/main/java/org/byteveda/taskito/errors/CryptoException.java new file mode 100644 index 00000000..7356fc0c --- /dev/null +++ b/sdks/java/src/main/java/org/byteveda/taskito/errors/CryptoException.java @@ -0,0 +1,17 @@ +package org.byteveda.taskito.errors; + +/** + * A cryptographic operation in a signing or encrypting serializer failed — + * encryption/decryption, HMAC computation, a signature mismatch, or a payload + * too short to carry its tag/IV. A {@link SerializationException} because it + * occurs while (de)serializing a secured payload. + */ +public class CryptoException extends SerializationException { + public CryptoException(String message) { + super(message); + } + + public CryptoException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/sdks/java/src/main/java/org/byteveda/taskito/errors/LockException.java b/sdks/java/src/main/java/org/byteveda/taskito/errors/LockException.java new file mode 100644 index 00000000..99b753a1 --- /dev/null +++ b/sdks/java/src/main/java/org/byteveda/taskito/errors/LockException.java @@ -0,0 +1,17 @@ +package org.byteveda.taskito.errors; + +import org.byteveda.taskito.TaskitoException; + +/** + * A distributed lock operation failed or was interrupted while waiting to + * acquire the lock. + */ +public class LockException extends TaskitoException { + public LockException(String message) { + super(message); + } + + public LockException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/sdks/java/src/main/java/org/byteveda/taskito/errors/SerializationException.java b/sdks/java/src/main/java/org/byteveda/taskito/errors/SerializationException.java new file mode 100644 index 00000000..e5311ffc --- /dev/null +++ b/sdks/java/src/main/java/org/byteveda/taskito/errors/SerializationException.java @@ -0,0 +1,18 @@ +package org.byteveda.taskito.errors; + +import org.byteveda.taskito.TaskitoException; + +/** + * A payload, option blob, or native response could not be serialized or + * deserialized — e.g. an unregistered type, malformed JSON, or a result whose + * shape does not match the expected class. + */ +public class SerializationException extends TaskitoException { + public SerializationException(String message) { + super(message); + } + + public SerializationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/sdks/java/src/main/java/org/byteveda/taskito/errors/WebhookException.java b/sdks/java/src/main/java/org/byteveda/taskito/errors/WebhookException.java new file mode 100644 index 00000000..f2ea2800 --- /dev/null +++ b/sdks/java/src/main/java/org/byteveda/taskito/errors/WebhookException.java @@ -0,0 +1,16 @@ +package org.byteveda.taskito.errors; + +import org.byteveda.taskito.TaskitoException; + +/** + * A webhook could not be stored, loaded, signed, or its payload encoded. + */ +public class WebhookException extends TaskitoException { + public WebhookException(String message) { + super(message); + } + + public WebhookException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/sdks/java/src/main/java/org/byteveda/taskito/errors/WorkflowException.java b/sdks/java/src/main/java/org/byteveda/taskito/errors/WorkflowException.java new file mode 100644 index 00000000..b9cd63b7 --- /dev/null +++ b/sdks/java/src/main/java/org/byteveda/taskito/errors/WorkflowException.java @@ -0,0 +1,18 @@ +package org.byteveda.taskito.errors; + +import org.byteveda.taskito.TaskitoException; + +/** + * A workflow could not be driven to completion — e.g. a run was not found, a + * deferred node has no registered payload, a callable condition was not + * registered on the worker, or awaiting a run was interrupted. + */ +public class WorkflowException extends TaskitoException { + public WorkflowException(String message) { + super(message); + } + + public WorkflowException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/sdks/java/src/main/java/org/byteveda/taskito/errors/package-info.java b/sdks/java/src/main/java/org/byteveda/taskito/errors/package-info.java new file mode 100644 index 00000000..79b9b402 --- /dev/null +++ b/sdks/java/src/main/java/org/byteveda/taskito/errors/package-info.java @@ -0,0 +1,10 @@ +/** + * Specific unchecked exceptions for Taskito failure scenarios. + * + *

All extend {@link org.byteveda.taskito.TaskitoException}, so a caller can + * catch the base type to handle any SDK error, or a specific subtype + * ({@link org.byteveda.taskito.errors.SerializationException}, + * {@link org.byteveda.taskito.errors.WorkflowException}, etc.) to react to one + * category. Native (JNI) errors surface as the base {@code TaskitoException}. + */ +package org.byteveda.taskito.errors; diff --git a/sdks/java/src/main/java/org/byteveda/taskito/events/EventName.java b/sdks/java/src/main/java/org/byteveda/taskito/events/EventName.java index cbd90711..a38bd6b8 100644 --- a/sdks/java/src/main/java/org/byteveda/taskito/events/EventName.java +++ b/sdks/java/src/main/java/org/byteveda/taskito/events/EventName.java @@ -1,6 +1,6 @@ package org.byteveda.taskito.events; -import org.byteveda.taskito.TaskitoException; +import org.byteveda.taskito.errors.SerializationException; /** Terminal outcome of a job, delivered to worker event listeners. */ public enum EventName { @@ -21,7 +21,7 @@ public static EventName fromKind(String kind) { case "cancelled": return CANCELLED; default: - throw new TaskitoException("unknown outcome kind: " + kind); + throw new SerializationException("unknown outcome kind: " + kind); } } } diff --git a/sdks/java/src/main/java/org/byteveda/taskito/locks/Lock.java b/sdks/java/src/main/java/org/byteveda/taskito/locks/Lock.java index 1573cab8..50105739 100644 --- a/sdks/java/src/main/java/org/byteveda/taskito/locks/Lock.java +++ b/sdks/java/src/main/java/org/byteveda/taskito/locks/Lock.java @@ -2,7 +2,7 @@ import java.time.Duration; import java.util.UUID; -import org.byteveda.taskito.TaskitoException; +import org.byteveda.taskito.errors.LockException; import org.byteveda.taskito.spi.QueueBackend; /** @@ -40,7 +40,7 @@ public boolean tryAcquire(Duration timeout) { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); - throw new TaskitoException("interrupted while acquiring lock '" + name + "'", e); + throw new LockException("interrupted while acquiring lock '" + name + "'", e); } } return true; diff --git a/sdks/java/src/main/java/org/byteveda/taskito/model/JobStatus.java b/sdks/java/src/main/java/org/byteveda/taskito/model/JobStatus.java index be09d6d4..225f5a0b 100644 --- a/sdks/java/src/main/java/org/byteveda/taskito/model/JobStatus.java +++ b/sdks/java/src/main/java/org/byteveda/taskito/model/JobStatus.java @@ -3,7 +3,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; import java.util.Locale; -import org.byteveda.taskito.TaskitoException; +import org.byteveda.taskito.errors.SerializationException; /** Lifecycle state of a job. Wire form is the lowercase name, shared across SDKs. */ public enum JobStatus { @@ -22,12 +22,12 @@ public String wire() { @JsonCreator public static JobStatus fromWire(String wire) { if (wire == null) { - throw new TaskitoException("job status is null"); + throw new SerializationException("job status is null"); } try { return valueOf(wire.toUpperCase(Locale.ROOT)); } catch (IllegalArgumentException e) { - throw new TaskitoException("unknown job status: " + wire, e); + throw new SerializationException("unknown job status: " + wire, e); } } } diff --git a/sdks/java/src/main/java/org/byteveda/taskito/serialization/EncryptedSerializer.java b/sdks/java/src/main/java/org/byteveda/taskito/serialization/EncryptedSerializer.java index 0e0e1f33..f5663e3e 100644 --- a/sdks/java/src/main/java/org/byteveda/taskito/serialization/EncryptedSerializer.java +++ b/sdks/java/src/main/java/org/byteveda/taskito/serialization/EncryptedSerializer.java @@ -6,7 +6,7 @@ import javax.crypto.Cipher; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.SecretKeySpec; -import org.byteveda.taskito.TaskitoException; +import org.byteveda.taskito.errors.CryptoException; /** * Wraps a delegate serializer with AES-GCM encryption. Each payload is prefixed @@ -40,7 +40,7 @@ public byte[] serialize(Object value) { System.arraycopy(ciphertext, 0, out, IV_LENGTH, ciphertext.length); return out; } catch (Exception e) { - throw new TaskitoException("encryption failed", e); + throw new CryptoException("encryption failed", e); } } @@ -57,7 +57,7 @@ public Object deserialize(byte[] bytes, Type type) { /** Strip the IV, decrypt + authenticate (GCM), and return the plaintext. */ private byte[] decrypt(byte[] bytes) { if (bytes.length < IV_LENGTH) { - throw new TaskitoException("encrypted payload is too short"); + throw new CryptoException("encrypted payload is too short"); } try { byte[] iv = Arrays.copyOfRange(bytes, 0, IV_LENGTH); @@ -66,7 +66,7 @@ private byte[] decrypt(byte[] bytes) { cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(TAG_BITS, iv)); return cipher.doFinal(ciphertext); } catch (Exception e) { - throw new TaskitoException("decryption failed", e); + throw new CryptoException("decryption failed", e); } } } diff --git a/sdks/java/src/main/java/org/byteveda/taskito/serialization/JsonSerializer.java b/sdks/java/src/main/java/org/byteveda/taskito/serialization/JsonSerializer.java index 6ed69e34..e5670688 100644 --- a/sdks/java/src/main/java/org/byteveda/taskito/serialization/JsonSerializer.java +++ b/sdks/java/src/main/java/org/byteveda/taskito/serialization/JsonSerializer.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.lang.reflect.Type; -import org.byteveda.taskito.TaskitoException; +import org.byteveda.taskito.errors.SerializationException; /** Default {@link Serializer}: JSON via Jackson. */ public final class JsonSerializer implements Serializer { @@ -21,7 +21,7 @@ public byte[] serialize(Object value) { try { return mapper.writeValueAsBytes(value); } catch (Exception e) { - throw new TaskitoException("failed to serialize payload", e); + throw new SerializationException("failed to serialize payload", e); } } @@ -30,7 +30,7 @@ public T deserialize(byte[] bytes, Class type) { try { return mapper.readValue(bytes, type); } catch (Exception e) { - throw new TaskitoException("failed to deserialize payload", e); + throw new SerializationException("failed to deserialize payload", e); } } @@ -39,7 +39,7 @@ public Object deserialize(byte[] bytes, Type type) { try { return mapper.readValue(bytes, mapper.getTypeFactory().constructType(type)); } catch (Exception e) { - throw new TaskitoException("failed to deserialize payload", e); + throw new SerializationException("failed to deserialize payload", e); } } } diff --git a/sdks/java/src/main/java/org/byteveda/taskito/serialization/SignedSerializer.java b/sdks/java/src/main/java/org/byteveda/taskito/serialization/SignedSerializer.java index 38a6590e..7d9c4b68 100644 --- a/sdks/java/src/main/java/org/byteveda/taskito/serialization/SignedSerializer.java +++ b/sdks/java/src/main/java/org/byteveda/taskito/serialization/SignedSerializer.java @@ -5,7 +5,7 @@ import java.util.Arrays; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; -import org.byteveda.taskito.TaskitoException; +import org.byteveda.taskito.errors.CryptoException; /** * Wraps a delegate serializer, prefixing each payload with an HMAC-SHA256 tag. @@ -46,12 +46,12 @@ public Object deserialize(byte[] bytes, Type type) { /** Verify the HMAC tag (constant-time) and return the signed body. */ private byte[] verify(byte[] bytes) { if (bytes.length < MAC_LENGTH) { - throw new TaskitoException("signed payload is too short"); + throw new CryptoException("signed payload is too short"); } byte[] mac = Arrays.copyOfRange(bytes, 0, MAC_LENGTH); byte[] body = Arrays.copyOfRange(bytes, MAC_LENGTH, bytes.length); if (!MessageDigest.isEqual(mac, mac(body))) { - throw new TaskitoException("signature mismatch"); + throw new CryptoException("signature mismatch"); } return body; } @@ -62,7 +62,7 @@ private byte[] mac(byte[] body) { mac.init(new SecretKeySpec(key, ALGORITHM)); return mac.doFinal(body); } catch (Exception e) { - throw new TaskitoException("HMAC computation failed", e); + throw new CryptoException("HMAC computation failed", e); } } } diff --git a/sdks/java/src/main/java/org/byteveda/taskito/webhooks/Deliverer.java b/sdks/java/src/main/java/org/byteveda/taskito/webhooks/Deliverer.java index 17587e1b..342aa188 100644 --- a/sdks/java/src/main/java/org/byteveda/taskito/webhooks/Deliverer.java +++ b/sdks/java/src/main/java/org/byteveda/taskito/webhooks/Deliverer.java @@ -8,7 +8,7 @@ import java.time.Duration; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; -import org.byteveda.taskito.TaskitoException; +import org.byteveda.taskito.errors.WebhookException; /** Delivers webhook payloads over HTTP with an optional HMAC signature and retries. */ final class Deliverer { @@ -45,7 +45,7 @@ static String sign(String secret, byte[] body) { mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), ALGORITHM)); return hex(mac.doFinal(body)); } catch (Exception e) { - throw new TaskitoException("webhook signature failed", e); + throw new WebhookException("webhook signature failed", e); } } diff --git a/sdks/java/src/main/java/org/byteveda/taskito/webhooks/WebhookManager.java b/sdks/java/src/main/java/org/byteveda/taskito/webhooks/WebhookManager.java index 9e19a67a..6eee78b1 100644 --- a/sdks/java/src/main/java/org/byteveda/taskito/webhooks/WebhookManager.java +++ b/sdks/java/src/main/java/org/byteveda/taskito/webhooks/WebhookManager.java @@ -9,7 +9,7 @@ import java.util.Optional; import java.util.UUID; import org.byteveda.taskito.Taskito; -import org.byteveda.taskito.TaskitoException; +import org.byteveda.taskito.errors.WebhookException; import org.byteveda.taskito.events.OutcomeEvent; import org.byteveda.taskito.middleware.Middleware; @@ -118,7 +118,7 @@ private static byte[] payload(OutcomeEvent event, String wire) { try { return JSON.writeValueAsBytes(body); } catch (Exception e) { - throw new TaskitoException("webhook payload encoding failed", e); + throw new WebhookException("webhook payload encoding failed", e); } } } diff --git a/sdks/java/src/main/java/org/byteveda/taskito/webhooks/WebhookStore.java b/sdks/java/src/main/java/org/byteveda/taskito/webhooks/WebhookStore.java index 00325a50..8af43256 100644 --- a/sdks/java/src/main/java/org/byteveda/taskito/webhooks/WebhookStore.java +++ b/sdks/java/src/main/java/org/byteveda/taskito/webhooks/WebhookStore.java @@ -5,7 +5,7 @@ import java.util.ArrayList; import java.util.List; import org.byteveda.taskito.Taskito; -import org.byteveda.taskito.TaskitoException; +import org.byteveda.taskito.errors.WebhookException; /** Persists webhooks as a JSON list in the queue's settings key/value store. */ final class WebhookStore { @@ -26,7 +26,7 @@ void save(List webhooks) { try { queue.setSetting(KEY, JSON.writeValueAsString(webhooks)); } catch (Exception e) { - throw new TaskitoException("failed to persist webhooks", e); + throw new WebhookException("failed to persist webhooks", e); } } @@ -35,7 +35,7 @@ private static List parse(String json) { CollectionType type = JSON.getTypeFactory().constructCollectionType(List.class, Webhook.class); return JSON.readValue(json, type); } catch (Exception e) { - throw new TaskitoException("failed to read webhooks", e); + throw new WebhookException("failed to read webhooks", e); } } } diff --git a/sdks/java/src/main/java/org/byteveda/taskito/worker/Worker.java b/sdks/java/src/main/java/org/byteveda/taskito/worker/Worker.java index 008be28d..9f2fd59e 100644 --- a/sdks/java/src/main/java/org/byteveda/taskito/worker/Worker.java +++ b/sdks/java/src/main/java/org/byteveda/taskito/worker/Worker.java @@ -13,7 +13,8 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; -import org.byteveda.taskito.TaskitoException; +import org.byteveda.taskito.errors.SerializationException; +import org.byteveda.taskito.errors.WorkflowException; import org.byteveda.taskito.events.Emitter; import org.byteveda.taskito.events.EventName; import org.byteveda.taskito.events.OutcomeEvent; @@ -71,7 +72,7 @@ public void rejectGate(String runId, String nodeName, String reason) { private WorkflowTracker requireTracker() { if (tracker == null) { - throw new TaskitoException("worker is not tracking workflows; call trackWorkflows() on the builder"); + throw new WorkflowException("worker is not tracking workflows; call trackWorkflows() on the builder"); } return tracker; } @@ -255,7 +256,7 @@ private String encodeOptions() { try { return JSON.writeValueAsString(options); } catch (Exception e) { - throw new TaskitoException("failed to encode worker options", e); + throw new SerializationException("failed to encode worker options", e); } } diff --git a/sdks/java/src/main/java/org/byteveda/taskito/workflows/WorkflowRun.java b/sdks/java/src/main/java/org/byteveda/taskito/workflows/WorkflowRun.java index c69642e1..8c6bc9e8 100644 --- a/sdks/java/src/main/java/org/byteveda/taskito/workflows/WorkflowRun.java +++ b/sdks/java/src/main/java/org/byteveda/taskito/workflows/WorkflowRun.java @@ -3,7 +3,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.time.Duration; import java.util.Optional; -import org.byteveda.taskito.TaskitoException; +import org.byteveda.taskito.errors.SerializationException; +import org.byteveda.taskito.errors.WorkflowException; import org.byteveda.taskito.spi.QueueBackend; /** A submitted workflow run. Query {@link #status()}, block on {@link #await}, or {@link #cancel()}. */ @@ -47,18 +48,18 @@ public WorkflowStatus await(Duration timeout) { public WorkflowStatus await(Duration timeout, Duration pollInterval) { long deadline = System.nanoTime() + timeout.toNanos(); while (true) { - WorkflowStatus status = status().orElseThrow(() -> new TaskitoException("workflow run not found: " + id)); + WorkflowStatus status = status().orElseThrow(() -> new WorkflowException("workflow run not found: " + id)); if (status.isTerminal()) { return status; } if (System.nanoTime() >= deadline) { - throw new TaskitoException("workflow '" + id + "' did not finish within " + timeout); + throw new WorkflowException("workflow '" + id + "' did not finish within " + timeout); } try { Thread.sleep(pollInterval.toMillis()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); - throw new TaskitoException("interrupted while awaiting workflow " + id, e); + throw new WorkflowException("interrupted while awaiting workflow " + id, e); } } } @@ -78,7 +79,7 @@ private WorkflowStatus decode(String raw) { try { return json.readValue(raw, WorkflowStatus.class); } catch (Exception e) { - throw new TaskitoException("failed to decode workflow status", e); + throw new SerializationException("failed to decode workflow status", e); } } } diff --git a/sdks/java/src/main/java/org/byteveda/taskito/workflows/WorkflowTracker.java b/sdks/java/src/main/java/org/byteveda/taskito/workflows/WorkflowTracker.java index 97b28903..eb730a00 100644 --- a/sdks/java/src/main/java/org/byteveda/taskito/workflows/WorkflowTracker.java +++ b/sdks/java/src/main/java/org/byteveda/taskito/workflows/WorkflowTracker.java @@ -18,7 +18,8 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import org.byteveda.taskito.TaskitoException; +import org.byteveda.taskito.errors.SerializationException; +import org.byteveda.taskito.errors.WorkflowException; import org.byteveda.taskito.events.OutcomeEvent; import org.byteveda.taskito.serialization.Serializer; import org.byteveda.taskito.spi.QueueBackend; @@ -308,7 +309,7 @@ private boolean shouldRun(String runId, PlanNode node, Map if ("callable".equals(node.condition)) { Condition predicate = callableCondition(runId, node.name); if (predicate == null) { - throw new TaskitoException("callable condition for workflow node '" + node.name + throw new WorkflowException("callable condition for workflow node '" + node.name + "' is not registered; track the workflow on the worker via trackWorkflows(workflow)"); } return predicate.test(buildContext(runId, statuses)); @@ -401,7 +402,7 @@ public void resolveGate(String runId, String nodeName, boolean approved, String private void createDeferredJobFor(String runId, PlanNode node) { byte[] payload = deferredPayload(runId, node.name); if (payload == null) { - throw new TaskitoException("no payload for deferred workflow node '" + node.name + throw new WorkflowException("no payload for deferred workflow node '" + node.name + "'; register the workflow on the worker via trackWorkflows(workflow)"); } backend.createDeferredJob( @@ -509,10 +510,10 @@ private byte[] fetchResult(String jobId) { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); - throw new TaskitoException("interrupted awaiting result for workflow job " + jobId, e); + throw new WorkflowException("interrupted awaiting result for workflow job " + jobId, e); } } - throw new TaskitoException("no result for workflow job " + jobId + " after 5s"); + throw new WorkflowException("no result for workflow job " + jobId + " after 5s"); } private Map statusMap(String runId) { @@ -587,7 +588,7 @@ private T decode(String raw, Class type) { try { return json.readValue(raw, type); } catch (Exception e) { - throw new TaskitoException("failed to decode workflow data", e); + throw new SerializationException("failed to decode workflow data", e); } } @@ -596,7 +597,7 @@ private List decodeList(String raw) { try { return json.readValue(raw, type); } catch (Exception e) { - throw new TaskitoException("failed to decode workflow plan", e); + throw new SerializationException("failed to decode workflow plan", e); } } From 6edc84e45e43d4647f1e3acd30190ba5d73ecfe1 Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Sun, 28 Jun 2026 18:49:05 +0530 Subject: [PATCH 2/3] test(java): cover the exception hierarchy Subtype relationships, and that serializers throw Serialization/Crypto. --- .../taskito/ExceptionHierarchyTest.java | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 sdks/java/src/test/java/org/byteveda/taskito/ExceptionHierarchyTest.java diff --git a/sdks/java/src/test/java/org/byteveda/taskito/ExceptionHierarchyTest.java b/sdks/java/src/test/java/org/byteveda/taskito/ExceptionHierarchyTest.java new file mode 100644 index 00000000..d8c1ed79 --- /dev/null +++ b/sdks/java/src/test/java/org/byteveda/taskito/ExceptionHierarchyTest.java @@ -0,0 +1,48 @@ +package org.byteveda.taskito; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.byteveda.taskito.errors.ConfigurationException; +import org.byteveda.taskito.errors.CryptoException; +import org.byteveda.taskito.errors.LockException; +import org.byteveda.taskito.errors.SerializationException; +import org.byteveda.taskito.errors.WebhookException; +import org.byteveda.taskito.errors.WorkflowException; +import org.byteveda.taskito.serialization.JsonSerializer; +import org.byteveda.taskito.serialization.Serializer; +import org.byteveda.taskito.serialization.SignedSerializer; +import org.junit.jupiter.api.Test; + +class ExceptionHierarchyTest { + + @Test + void everySpecificExceptionExtendsTheBase() { + assertTrue(TaskitoException.class.isAssignableFrom(SerializationException.class)); + assertTrue(TaskitoException.class.isAssignableFrom(WorkflowException.class)); + assertTrue(TaskitoException.class.isAssignableFrom(LockException.class)); + assertTrue(TaskitoException.class.isAssignableFrom(ConfigurationException.class)); + assertTrue(TaskitoException.class.isAssignableFrom(WebhookException.class)); + // CryptoException is a kind of SerializationException. + assertTrue(SerializationException.class.isAssignableFrom(CryptoException.class)); + } + + @Test + void malformedPayloadThrowsSerializationException() { + Serializer json = new JsonSerializer(); + assertThrows(SerializationException.class, () -> json.deserialize("not json".getBytes(), Integer.class)); + } + + @Test + void tamperedSignedPayloadThrowsCryptoException() { + Serializer signed = new SignedSerializer(new JsonSerializer(), "secret".getBytes()); + byte[] bytes = signed.serialize(42); + bytes[0] ^= 0x01; // corrupt the HMAC tag + + CryptoException error = assertThrows(CryptoException.class, () -> signed.deserialize(bytes, Integer.class)); + // A caller may catch it as the category or the base type. + assertInstanceOf(SerializationException.class, error); + assertInstanceOf(TaskitoException.class, error); + } +} From ba1a3d79d2a2343d1d04a9c32f3c2d396902be0e Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Tue, 30 Jun 2026 09:10:15 +0530 Subject: [PATCH 3/3] fix(java): throw WorkflowException from child step validation --- .../src/main/java/org/byteveda/taskito/DefaultTaskito.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdks/java/src/main/java/org/byteveda/taskito/DefaultTaskito.java b/sdks/java/src/main/java/org/byteveda/taskito/DefaultTaskito.java index fccc9393..40da2b1e 100644 --- a/sdks/java/src/main/java/org/byteveda/taskito/DefaultTaskito.java +++ b/sdks/java/src/main/java/org/byteveda/taskito/DefaultTaskito.java @@ -493,13 +493,13 @@ private String encodeChild(Workflow child) { // A child run has no submit-time payload map and no callable registry, so // any step that depends on either would lose state — reject it at build time. if ("callable".equals(step.condition)) { - throw new TaskitoException("child workflow step '" + step.name + throw new WorkflowException("child workflow step '" + step.name + "' uses condition(Condition); a sub-workflow cannot carry callable predicates"); } boolean controlNode = step.fanOut != null || step.fanIn != null || step.gate != null || step.subWorkflow != null; if (!controlNode && step.payload == null) { - throw new TaskitoException("child workflow step '" + step.name + throw new WorkflowException("child workflow step '" + step.name + "' has no payload; a sub-workflow cannot supply child step payloads at submit"); } specs.add(stepSpec(step));