From 55f6543f848746e00c087243b8163e6cf6e74518 Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:19:17 +0530 Subject: [PATCH 1/8] feat(java): idempotent auto-derive on enqueue --- .../processor/TaskHandlerProcessor.java | 8 ++ .../org/byteveda/taskito/DefaultTaskito.java | 48 ++++++++++-- .../taskito/annotation/TaskHandler.java | 3 + .../taskito/internal/IdempotencyKeys.java | 43 ++++++++++ .../byteveda/taskito/task/EnqueueOptions.java | 47 +++++++++++ .../java/org/byteveda/taskito/task/Task.java | 36 +++++++-- .../org/byteveda/taskito/IdempotencyTest.java | 78 +++++++++++++++++++ 7 files changed, 250 insertions(+), 13 deletions(-) create mode 100644 sdks/java/src/main/java/org/byteveda/taskito/internal/IdempotencyKeys.java create mode 100644 sdks/java/src/test/java/org/byteveda/taskito/IdempotencyTest.java diff --git a/sdks/java/processor/src/main/java/org/byteveda/taskito/processor/TaskHandlerProcessor.java b/sdks/java/processor/src/main/java/org/byteveda/taskito/processor/TaskHandlerProcessor.java index 95bd28af..9589c2fa 100644 --- a/sdks/java/processor/src/main/java/org/byteveda/taskito/processor/TaskHandlerProcessor.java +++ b/sdks/java/processor/src/main/java/org/byteveda/taskito/processor/TaskHandlerProcessor.java @@ -190,6 +190,9 @@ private String options(ExecutableElement method) { if (priority != 0) { chain.append(".priority(").append(priority).append(")"); } + if (boolValue(mirror, "idempotent", false)) { + chain.append(".idempotent(true)"); + } return chain.toString(); } @@ -285,6 +288,11 @@ private long longValue(AnnotationMirror mirror, String key, long fallback) { return value instanceof Number ? ((Number) value).longValue() : fallback; } + private boolean boolValue(AnnotationMirror mirror, String key, boolean fallback) { + Object value = rawValue(mirror, key); + return value instanceof Boolean ? (Boolean) value : fallback; + } + private Object rawValue(AnnotationMirror mirror, String key) { if (mirror == null) { return null; 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 b34ed942..f6cf4cf2 100644 --- a/sdks/java/src/main/java/org/byteveda/taskito/DefaultTaskito.java +++ b/sdks/java/src/main/java/org/byteveda/taskito/DefaultTaskito.java @@ -24,6 +24,7 @@ import org.byteveda.taskito.errors.WorkflowException; import org.byteveda.taskito.interception.Interception; import org.byteveda.taskito.interception.Interceptor; +import org.byteveda.taskito.internal.IdempotencyKeys; import org.byteveda.taskito.locks.Lock; import org.byteveda.taskito.locks.LockInfo; import org.byteveda.taskito.middleware.EnqueueContext; @@ -167,12 +168,13 @@ public String enqueue(Task task, T payload) { @Override public String enqueue(Task task, T payload, EnqueueOptions options) { - return required(dispatchEnqueue(task.name(), payload, options, task.codecNames()), task.name()); + return required( + dispatchEnqueue(task.name(), payload, options, task.codecNames(), task.idempotent()), task.name()); } @Override public String enqueue(String taskName, Object payload) { - return required(dispatchEnqueue(taskName, payload, EnqueueOptions.none(), List.of()), taskName); + return required(dispatchEnqueue(taskName, payload, EnqueueOptions.none(), List.of(), false), taskName); } @Override @@ -182,12 +184,12 @@ public Optional tryEnqueue(Task task, T payload) { @Override public Optional tryEnqueue(Task task, T payload, EnqueueOptions options) { - return dispatchEnqueue(task.name(), payload, options, task.codecNames()); + return dispatchEnqueue(task.name(), payload, options, task.codecNames(), task.idempotent()); } @Override public Optional tryEnqueue(String taskName, Object payload) { - return dispatchEnqueue(taskName, payload, EnqueueOptions.none(), List.of()); + return dispatchEnqueue(taskName, payload, EnqueueOptions.none(), List.of(), false); } /** Unwrap a dispatch result, turning a gate {@code Skip} into an {@link EnqueueSkippedException}. */ @@ -202,7 +204,11 @@ private static String required(Optional jobId, String taskName) { * the enqueue; throws when a gate rejects it. */ private Optional dispatchEnqueue( - String taskName, Object payload, EnqueueOptions options, List codecNames) { + String taskName, + Object payload, + EnqueueOptions options, + List codecNames, + boolean taskIdempotentDefault) { String originalTaskName = taskName; for (Interceptor interceptor : interceptors) { Interception outcome = interceptor.intercept(taskName, payload); @@ -249,10 +255,40 @@ private Optional dispatchEnqueue( .metadata(encode(context.metadata())) .build(); } - byte[] data = encodeCodecs(serializer.serialize(context.payload()), codecNames); + // Serialize before codec-encoding so the idempotency key hashes the deterministic + // pre-codec payload — a non-deterministic codec (e.g. an AES-GCM nonce) must not + // change the dedup key. + byte[] payloadBytes = serializer.serialize(context.payload()); + String uniqueKey = resolveUniqueKey(taskName, payloadBytes, finalOptions, taskIdempotentDefault); + if (uniqueKey != null && !uniqueKey.equals(finalOptions.uniqueKey())) { + finalOptions = finalOptions.toBuilder().uniqueKey(uniqueKey).build(); + } + byte[] data = encodeCodecs(payloadBytes, codecNames); return Optional.of(backend.enqueue(taskName, data, encode(finalOptions))); } + /** + * Resolve the effective dedup key: an explicit {@code uniqueKey} wins, then an explicit + * {@code idempotencyKey}; a per-enqueue {@code idempotent(false)} opts out; otherwise a + * per-enqueue {@code idempotent(true)} or the task-level default auto-derives one from the + * payload. Returns {@code null} when the enqueue should not be deduped. + */ + private static String resolveUniqueKey( + String taskName, byte[] preCodecPayload, EnqueueOptions options, boolean taskIdempotentDefault) { + if (options.uniqueKey() != null) { + return options.uniqueKey(); + } + if (options.idempotencyKey() != null) { + return options.idempotencyKey(); + } + Boolean idempotent = options.idempotent(); + if (Boolean.FALSE.equals(idempotent)) { + return null; + } + boolean enabled = Boolean.TRUE.equals(idempotent) || taskIdempotentDefault; + return enabled ? IdempotencyKeys.autoKey(taskName, preCodecPayload) : null; + } + /** * Apply a defer delay to the options, overriding any delay already set — * {@code Defer(Duration.ZERO)} (e.g. {@code deferUntil} of a past instant) diff --git a/sdks/java/src/main/java/org/byteveda/taskito/annotation/TaskHandler.java b/sdks/java/src/main/java/org/byteveda/taskito/annotation/TaskHandler.java index a253777a..b1dc2252 100644 --- a/sdks/java/src/main/java/org/byteveda/taskito/annotation/TaskHandler.java +++ b/sdks/java/src/main/java/org/byteveda/taskito/annotation/TaskHandler.java @@ -32,4 +32,7 @@ /** Priority; 0 (default) is left unset. */ int priority() default 0; + + /** Auto-derive an idempotency {@code uniqueKey} from the payload on every enqueue. */ + boolean idempotent() default false; } diff --git a/sdks/java/src/main/java/org/byteveda/taskito/internal/IdempotencyKeys.java b/sdks/java/src/main/java/org/byteveda/taskito/internal/IdempotencyKeys.java new file mode 100644 index 00000000..a5b351af --- /dev/null +++ b/sdks/java/src/main/java/org/byteveda/taskito/internal/IdempotencyKeys.java @@ -0,0 +1,43 @@ +package org.byteveda.taskito.internal; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; + +/** + * Derives the automatic idempotency {@code uniqueKey} for a task from its name and + * serialized payload. The recipe is a cross-SDK wire contract — peer SDKs compute the + * identical key for the same {@code (name, payload)} so idempotent enqueues dedupe across + * languages: + * + *
{@code
+ * uniqueKey = "auto:" + sha256( utf8(name) ++ 0x00 ++ payload ).hex().substring(0, 32)
+ * }
+ * + *

The separator is a single {@code NUL} byte (not a printable delimiter) so a task name + * cannot be confused with the start of a payload. The hash is taken over the pre-codec + * payload bytes, so a non-deterministic codec (e.g. an AES-GCM nonce) cannot break dedup. + */ +public final class IdempotencyKeys { + private static final String PREFIX = "auto:"; + private static final int KEY_HEX_CHARS = 32; + + private IdempotencyKeys() {} + + /** The {@code auto:}-prefixed idempotency key for {@code taskName} over {@code payload}. */ + public static String autoKey(String taskName, byte[] payload) { + MessageDigest digest; + try { + digest = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + // SHA-256 is mandated on every conformant JVM; its absence is unrecoverable. + throw new IllegalStateException("SHA-256 is not available", e); + } + digest.update(taskName.getBytes(StandardCharsets.UTF_8)); + digest.update((byte) 0x00); + digest.update(payload); + String hex = HexFormat.of().formatHex(digest.digest()); + return PREFIX + hex.substring(0, KEY_HEX_CHARS); + } +} diff --git a/sdks/java/src/main/java/org/byteveda/taskito/task/EnqueueOptions.java b/sdks/java/src/main/java/org/byteveda/taskito/task/EnqueueOptions.java index 853b053c..9d440a93 100644 --- a/sdks/java/src/main/java/org/byteveda/taskito/task/EnqueueOptions.java +++ b/sdks/java/src/main/java/org/byteveda/taskito/task/EnqueueOptions.java @@ -31,6 +31,12 @@ public final class EnqueueOptions { @JsonProperty("namespace") private final String namespace; + // Idempotency inputs resolve to uniqueKey locally (see DefaultTaskito) and never cross + // the wire, so they carry no @JsonProperty and are not serialized into the options JSON. + private final Boolean idempotent; + + private final String idempotencyKey; + private EnqueueOptions(Builder b) { this.queue = b.queue; this.priority = b.priority; @@ -40,6 +46,8 @@ private EnqueueOptions(Builder b) { this.uniqueKey = b.uniqueKey; this.metadata = b.metadata; this.namespace = b.namespace; + this.idempotent = b.idempotent; + this.idempotencyKey = b.idempotencyKey; } public static EnqueueOptions none() { @@ -61,9 +69,29 @@ public Builder toBuilder() { b.uniqueKey = uniqueKey; b.metadata = metadata; b.namespace = namespace; + b.idempotent = idempotent; + b.idempotencyKey = idempotencyKey; return b; } + /** The explicit dedup key, or {@code null} when none was set. */ + public String uniqueKey() { + return uniqueKey; + } + + /** + * Tri-state idempotency toggle: {@code TRUE} forces auto-derivation of a {@code uniqueKey}, + * {@code FALSE} opts this enqueue out of a task-level default, {@code null} defers to the task. + */ + public Boolean idempotent() { + return idempotent; + } + + /** An explicit idempotency key (used as the {@code uniqueKey} when set), or {@code null}. */ + public String idempotencyKey() { + return idempotencyKey; + } + public static final class Builder { private String queue; private Integer priority; @@ -73,6 +101,8 @@ public static final class Builder { private String uniqueKey; private String metadata; private String namespace; + private Boolean idempotent; + private String idempotencyKey; public Builder queue(String queue) { this.queue = queue; @@ -141,6 +171,23 @@ public Builder namespace(String namespace) { return this; } + /** + * Dedupe this enqueue by auto-deriving a {@code uniqueKey} from the task name and + * payload. A duplicate enqueue is a no-op while the first job is pending or running. + * An explicit {@link #uniqueKey}/{@link #idempotencyKey} takes precedence; passing + * {@code false} opts out of a task-level default. + */ + public Builder idempotent(boolean idempotent) { + this.idempotent = idempotent; + return this; + } + + /** Dedupe this enqueue under an explicit key (equivalent to a caller-supplied {@code uniqueKey}). */ + public Builder idempotencyKey(String idempotencyKey) { + this.idempotencyKey = idempotencyKey; + return this; + } + public EnqueueOptions build() { return new EnqueueOptions(this); } diff --git a/sdks/java/src/main/java/org/byteveda/taskito/task/Task.java b/sdks/java/src/main/java/org/byteveda/taskito/task/Task.java index ce40ef58..b71a8d8f 100644 --- a/sdks/java/src/main/java/org/byteveda/taskito/task/Task.java +++ b/sdks/java/src/main/java/org/byteveda/taskito/task/Task.java @@ -20,8 +20,15 @@ public final class Task { private final EnqueueOptions options; private final RetryPolicy retryPolicy; private final List codecs; - - private Task(String name, Type payloadType, EnqueueOptions options, RetryPolicy retryPolicy, List codecs) { + private final boolean idempotent; + + private Task( + String name, + Type payloadType, + EnqueueOptions options, + RetryPolicy retryPolicy, + List codecs, + boolean idempotent) { this.name = Objects.requireNonNull(name, "task name must not be null"); if (name.trim().isEmpty()) { throw new IllegalArgumentException("task name must not be blank"); @@ -30,21 +37,22 @@ private Task(String name, Type payloadType, EnqueueOptions options, RetryPolicy this.options = Objects.requireNonNull(options, "options must not be null"); this.retryPolicy = retryPolicy; this.codecs = List.copyOf(codecs); + this.idempotent = idempotent; } /** A task whose payload deserializes to {@code payloadType}. */ public static Task of(String name, Class payloadType) { - return new Task<>(name, payloadType, EnqueueOptions.none(), null, List.of()); + return new Task<>(name, payloadType, EnqueueOptions.none(), null, List.of(), false); } /** A task whose payload deserializes to a generic type, e.g. {@code new TypeReference>(){}}. */ public static Task of(String name, TypeReference payloadType) { - return new Task<>(name, payloadType.getType(), EnqueueOptions.none(), null, List.of()); + return new Task<>(name, payloadType.getType(), EnqueueOptions.none(), null, List.of(), false); } /** A copy of this task with the given default options. */ public Task withOptions(EnqueueOptions options) { - return new Task<>(name, payloadType, options, retryPolicy, codecs); + return new Task<>(name, payloadType, options, retryPolicy, codecs, idempotent); } /** @@ -53,7 +61,7 @@ public Task withOptions(EnqueueOptions options) { * from {@link #maxRetries}. */ public Task retryPolicy(RetryPolicy retryPolicy) { - return new Task<>(name, payloadType, options, retryPolicy, codecs); + return new Task<>(name, payloadType, options, retryPolicy, codecs, idempotent); } /** @@ -63,7 +71,16 @@ public Task retryPolicy(RetryPolicy retryPolicy) { * {@code Taskito.builder().codec(name, codec)} on producers and workers. */ public Task codecs(String... codecs) { - return new Task<>(name, payloadType, options, retryPolicy, Arrays.asList(codecs)); + return new Task<>(name, payloadType, options, retryPolicy, Arrays.asList(codecs), idempotent); + } + + /** + * A copy of this task that auto-derives a {@code uniqueKey} from the payload on every + * enqueue, so a duplicate enqueue is a no-op while the first job is pending or running. + * A per-enqueue {@link EnqueueOptions#idempotent(boolean)} overrides this default. + */ + public Task idempotent(boolean idempotent) { + return new Task<>(name, payloadType, options, retryPolicy, codecs, idempotent); } public Task queue(String queue) { @@ -121,4 +138,9 @@ public RetryPolicy retryPolicy() { public List codecNames() { return codecs; } + + /** Whether this task auto-derives an idempotency {@code uniqueKey} by default. */ + public boolean idempotent() { + return idempotent; + } } diff --git a/sdks/java/src/test/java/org/byteveda/taskito/IdempotencyTest.java b/sdks/java/src/test/java/org/byteveda/taskito/IdempotencyTest.java new file mode 100644 index 00000000..bea479e4 --- /dev/null +++ b/sdks/java/src/test/java/org/byteveda/taskito/IdempotencyTest.java @@ -0,0 +1,78 @@ +package org.byteveda.taskito; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import org.byteveda.taskito.internal.IdempotencyKeys; +import org.byteveda.taskito.task.EnqueueOptions; +import org.byteveda.taskito.task.Task; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.io.TempDir; + +class IdempotencyTest { + + // Golden vectors — the auto-key recipe is a cross-SDK wire contract, so these must match + // the values a peer SDK produces from sha256(utf8(name) ++ 0x00 ++ payload)[:32]. + @Test + void autoKeyMatchesCrossSdkGoldenVectors() { + assertEquals( + "auto:ca3f9aac3860dab1f1edb773c39ca21e", + IdempotencyKeys.autoKey("send_email", "{\"to\":\"a@b.com\"}".getBytes(StandardCharsets.UTF_8))); + assertEquals("auto:ff688985bc4fd8bdde332c05f8cb0d86", IdempotencyKeys.autoKey("task", new byte[0])); + assertEquals( + "auto:6b70352f019e763745b932f8330fe74e", + IdempotencyKeys.autoKey("send_email", "hello".getBytes(StandardCharsets.UTF_8))); + } + + @Test + void autoKeyHasStableShape() { + String key = IdempotencyKeys.autoKey("t", "p".getBytes(StandardCharsets.UTF_8)); + assertEquals("auto:".length() + 32, key.length()); + } + + @Test + @Timeout(30) + void idempotentTaskDedupesWhilePending(@TempDir Path dir) throws Exception { + try (Taskito queue = + Taskito.builder().url(dir.resolve("idem.db").toString()).open()) { + Task task = Task.of("idem.dedup", String.class).idempotent(true); + // No worker: both jobs stay pending, so the second enqueue dedups to the first. + String first = queue.enqueue(task, "hello"); + String second = queue.enqueue(task, "hello"); + assertEquals(first, second, "same payload should dedup to the same job"); + + String other = queue.enqueue(task, "world"); + assertNotEquals(first, other, "a different payload derives a different key"); + } + } + + @Test + @Timeout(30) + void explicitIdempotencyKeyWinsOverPayload(@TempDir Path dir) throws Exception { + try (Taskito queue = + Taskito.builder().url(dir.resolve("idemkey.db").toString()).open()) { + Task task = Task.of("idem.key", String.class); + EnqueueOptions opts = EnqueueOptions.builder().idempotencyKey("k1").build(); + // Different payloads but the same explicit key dedup together. + String first = queue.enqueue(task, "a", opts); + String second = queue.enqueue(task, "b", opts); + assertEquals(first, second); + } + } + + @Test + @Timeout(30) + void perCallOptOutDisablesTaskDefault(@TempDir Path dir) throws Exception { + try (Taskito queue = + Taskito.builder().url(dir.resolve("optout.db").toString()).open()) { + Task task = Task.of("idem.optout", String.class).idempotent(true); + EnqueueOptions off = EnqueueOptions.builder().idempotent(false).build(); + String first = queue.enqueue(task, "hello", off); + String second = queue.enqueue(task, "hello", off); + assertNotEquals(first, second, "idempotent(false) opts out of the task default"); + } + } +} From f3ca3f39a862e2c63f1aa8235550c760f05b4143 Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:22:33 +0530 Subject: [PATCH 2/8] feat(java): dependsOn on enqueue --- crates/taskito-java/src/convert.rs | 3 +- .../byteveda/taskito/task/EnqueueOptions.java | 28 +++++++ .../org/byteveda/taskito/DependsOnTest.java | 81 +++++++++++++++++++ 3 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 sdks/java/src/test/java/org/byteveda/taskito/DependsOnTest.java diff --git a/crates/taskito-java/src/convert.rs b/crates/taskito-java/src/convert.rs index 8019808a..d12f18dd 100644 --- a/crates/taskito-java/src/convert.rs +++ b/crates/taskito-java/src/convert.rs @@ -43,6 +43,7 @@ pub struct EnqueueOptions { pub unique_key: Option, pub metadata: Option, pub namespace: Option, + pub depends_on: Option>, } /// Parse a JSON argument, attributing any failure to the named field. @@ -71,7 +72,7 @@ pub fn build_new_job( unique_key: options.unique_key, metadata: options.metadata, notes: None, - depends_on: Vec::new(), + depends_on: options.depends_on.unwrap_or_default(), expires_at: None, result_ttl_ms: None, namespace: options diff --git a/sdks/java/src/main/java/org/byteveda/taskito/task/EnqueueOptions.java b/sdks/java/src/main/java/org/byteveda/taskito/task/EnqueueOptions.java index 9d440a93..ddd06bc7 100644 --- a/sdks/java/src/main/java/org/byteveda/taskito/task/EnqueueOptions.java +++ b/sdks/java/src/main/java/org/byteveda/taskito/task/EnqueueOptions.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import java.time.Duration; +import java.util.List; /** Immutable per-enqueue options. Unset fields take core defaults. */ @JsonInclude(JsonInclude.Include.NON_NULL) @@ -31,6 +32,9 @@ public final class EnqueueOptions { @JsonProperty("namespace") private final String namespace; + @JsonProperty("dependsOn") + private final List dependsOn; + // Idempotency inputs resolve to uniqueKey locally (see DefaultTaskito) and never cross // the wire, so they carry no @JsonProperty and are not serialized into the options JSON. private final Boolean idempotent; @@ -46,6 +50,7 @@ private EnqueueOptions(Builder b) { this.uniqueKey = b.uniqueKey; this.metadata = b.metadata; this.namespace = b.namespace; + this.dependsOn = b.dependsOn; this.idempotent = b.idempotent; this.idempotencyKey = b.idempotencyKey; } @@ -69,11 +74,17 @@ public Builder toBuilder() { b.uniqueKey = uniqueKey; b.metadata = metadata; b.namespace = namespace; + b.dependsOn = dependsOn; b.idempotent = idempotent; b.idempotencyKey = idempotencyKey; return b; } + /** Job ids this enqueue waits on before it can be dequeued, or {@code null} when none. */ + public List dependsOn() { + return dependsOn; + } + /** The explicit dedup key, or {@code null} when none was set. */ public String uniqueKey() { return uniqueKey; @@ -101,6 +112,7 @@ public static final class Builder { private String uniqueKey; private String metadata; private String namespace; + private List dependsOn; private Boolean idempotent; private String idempotencyKey; @@ -171,6 +183,22 @@ public Builder namespace(String namespace) { return this; } + /** + * Gate this job on the completion of the given job ids: it stays pending (not dequeued) + * until every dependency completes, and is cancelled if any dependency fails. Each id + * must reference a job that is still live or already complete. + */ + public Builder dependsOn(String... jobIds) { + this.dependsOn = List.of(jobIds); + return this; + } + + /** List form of {@link #dependsOn(String...)}. */ + public Builder dependsOn(List jobIds) { + this.dependsOn = List.copyOf(jobIds); + return this; + } + /** * Dedupe this enqueue by auto-deriving a {@code uniqueKey} from the task name and * payload. A duplicate enqueue is a no-op while the first job is pending or running. diff --git a/sdks/java/src/test/java/org/byteveda/taskito/DependsOnTest.java b/sdks/java/src/test/java/org/byteveda/taskito/DependsOnTest.java new file mode 100644 index 00000000..e20dafe4 --- /dev/null +++ b/sdks/java/src/test/java/org/byteveda/taskito/DependsOnTest.java @@ -0,0 +1,81 @@ +package org.byteveda.taskito; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Path; +import java.time.Duration; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import org.byteveda.taskito.model.Job; +import org.byteveda.taskito.model.JobStatus; +import org.byteveda.taskito.task.EnqueueOptions; +import org.byteveda.taskito.task.Task; +import org.byteveda.taskito.worker.Worker; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.io.TempDir; + +class DependsOnTest { + + @Test + @Timeout(30) + void dependentWaitsForDependency(@TempDir Path dir) throws Exception { + try (Taskito queue = + Taskito.builder().url(dir.resolve("dep.db").toString()).open()) { + Task a = Task.of("dep.a", String.class); + Task b = Task.of("dep.b", String.class); + AtomicBoolean aDone = new AtomicBoolean(false); + AtomicBoolean bSawADone = new AtomicBoolean(false); + CountDownLatch bRan = new CountDownLatch(1); + + String idA = queue.enqueue(a, "a"); + queue.enqueue(b, "b", EnqueueOptions.builder().dependsOn(idA).build()); + + try (Worker worker = queue.worker() + .handle(a, p -> { + aDone.set(true); + return p; + }) + .handle(b, p -> { + bSawADone.set(aDone.get()); + bRan.countDown(); + return p; + }) + .start()) { + assertTrue(bRan.await(20, TimeUnit.SECONDS), "dependent should run once its dependency completes"); + assertTrue(bSawADone.get(), "the dependency must complete before the dependent runs"); + } + } + } + + @Test + @Timeout(30) + void cascadeCancelsDependentWhenDependencyFails(@TempDir Path dir) throws Exception { + try (Taskito queue = + Taskito.builder().url(dir.resolve("dep_cascade.db").toString()).open()) { + Task a = Task.of("dep.cascade.a", String.class).maxRetries(0); + Task b = Task.of("dep.cascade.b", String.class); + AtomicBoolean bRan = new AtomicBoolean(false); + + String idA = queue.enqueue(a, "boom"); + String idB = queue.enqueue(b, "b", EnqueueOptions.builder().dependsOn(idA).build()); + + try (Worker worker = queue.worker() + .handle(a, p -> { + throw new RuntimeException("boom"); + }) + .handle(b, p -> { + bRan.set(true); + return p; + }) + .start()) { + Job jobB = queue.awaitJob(idB, Duration.ofSeconds(20)).orElseThrow(); + assertEquals(JobStatus.CANCELLED, jobB.status, "a failed dependency cascade-cancels the dependent"); + assertFalse(bRan.get(), "the dependent must never run"); + } + } + } +} From 0487fde6fe4ff2226339aa8c686faa163f9fe149 Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:27:40 +0530 Subject: [PATCH 3/8] feat(java): structured notes on enqueue --- crates/taskito-java/src/convert.rs | 7 +- .../byteveda/taskito/dashboard/Contract.java | 1 + .../errors/NotesValidationException.java | 18 +++ .../java/org/byteveda/taskito/model/Job.java | 26 +++- .../byteveda/taskito/serialization/Notes.java | 131 ++++++++++++++++++ .../byteveda/taskito/task/EnqueueOptions.java | 22 +++ .../java/org/byteveda/taskito/NotesTest.java | 100 +++++++++++++ 7 files changed, 303 insertions(+), 2 deletions(-) create mode 100644 sdks/java/src/main/java/org/byteveda/taskito/errors/NotesValidationException.java create mode 100644 sdks/java/src/main/java/org/byteveda/taskito/serialization/Notes.java create mode 100644 sdks/java/src/test/java/org/byteveda/taskito/NotesTest.java diff --git a/crates/taskito-java/src/convert.rs b/crates/taskito-java/src/convert.rs index d12f18dd..e9c897cf 100644 --- a/crates/taskito-java/src/convert.rs +++ b/crates/taskito-java/src/convert.rs @@ -44,6 +44,8 @@ pub struct EnqueueOptions { pub metadata: Option, pub namespace: Option, pub depends_on: Option>, + /// Pre-encoded canonical JSON produced by the SDK; stored verbatim. + pub notes: Option, } /// Parse a JSON argument, attributing any failure to the named field. @@ -71,7 +73,7 @@ pub fn build_new_job( timeout_ms: options.timeout_ms.unwrap_or(0), unique_key: options.unique_key, metadata: options.metadata, - notes: None, + notes: options.notes, depends_on: options.depends_on.unwrap_or_default(), expires_at: None, result_ttl_ms: None, @@ -130,6 +132,8 @@ pub struct JobView<'a> { pub namespace: Option<&'a str>, /// Opaque metadata blob (JSON the SDK sets, e.g. middleware-injected trace ids). pub metadata: Option<&'a str>, + /// Structured notes as canonical JSON; the SDK parses it back into a map. + pub notes: Option<&'a str>, } impl<'a> From<&'a Job> for JobView<'a> { @@ -152,6 +156,7 @@ impl<'a> From<&'a Job> for JobView<'a> { unique_key: j.unique_key.as_deref(), namespace: j.namespace.as_deref(), metadata: j.metadata.as_deref(), + notes: j.notes.as_deref(), } } } diff --git a/sdks/java/src/main/java/org/byteveda/taskito/dashboard/Contract.java b/sdks/java/src/main/java/org/byteveda/taskito/dashboard/Contract.java index fbfb9f80..c589e83b 100644 --- a/sdks/java/src/main/java/org/byteveda/taskito/dashboard/Contract.java +++ b/sdks/java/src/main/java/org/byteveda/taskito/dashboard/Contract.java @@ -44,6 +44,7 @@ static Map job(Job j) { m.put("error", j.error); m.put("unique_key", j.uniqueKey); m.put("namespace", j.namespace); + m.put("notes", j.notes); return m; } diff --git a/sdks/java/src/main/java/org/byteveda/taskito/errors/NotesValidationException.java b/sdks/java/src/main/java/org/byteveda/taskito/errors/NotesValidationException.java new file mode 100644 index 00000000..921de2a2 --- /dev/null +++ b/sdks/java/src/main/java/org/byteveda/taskito/errors/NotesValidationException.java @@ -0,0 +1,18 @@ +package org.byteveda.taskito.errors; + +import org.byteveda.taskito.TaskitoException; + +/** + * A job's structured {@code notes} violated the bounded-annotation contract — too many + * fields, a key or value that is too long, excessive nesting, an unsupported value type, + * or an encoded size over the limit. The message names the offending key or constraint. + */ +public class NotesValidationException extends TaskitoException { + public NotesValidationException(String message) { + super(message); + } + + public NotesValidationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/sdks/java/src/main/java/org/byteveda/taskito/model/Job.java b/sdks/java/src/main/java/org/byteveda/taskito/model/Job.java index dba8166b..82a6b256 100644 --- a/sdks/java/src/main/java/org/byteveda/taskito/model/Job.java +++ b/sdks/java/src/main/java/org/byteveda/taskito/model/Job.java @@ -3,10 +3,17 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Map; +import java.util.Optional; +import org.byteveda.taskito.errors.SerializationException; /** Immutable view of a job. Timestamps are Unix milliseconds. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class Job { + private static final ObjectMapper MAPPER = new ObjectMapper(); + public final String id; public final String queue; public final String taskName; @@ -25,6 +32,9 @@ public final class Job { public final String namespace; public final String metadata; + /** Structured notes as canonical JSON, or {@code null}. Use {@link #notesMap()} for a parsed view. */ + public final String notes; + @JsonCreator public Job( @JsonProperty("id") String id, @@ -43,7 +53,8 @@ public Job( @JsonProperty("error") String error, @JsonProperty("uniqueKey") String uniqueKey, @JsonProperty("namespace") String namespace, - @JsonProperty("metadata") String metadata) { + @JsonProperty("metadata") String metadata, + @JsonProperty("notes") String notes) { this.id = id; this.queue = queue; this.taskName = taskName; @@ -61,5 +72,18 @@ public Job( this.uniqueKey = uniqueKey; this.namespace = namespace; this.metadata = metadata; + this.notes = notes; + } + + /** The structured notes parsed into a map, or empty when the job carries none. */ + public Optional> notesMap() { + if (notes == null) { + return Optional.empty(); + } + try { + return Optional.of(MAPPER.readValue(notes, new TypeReference>() {})); + } catch (Exception e) { + throw new SerializationException("failed to parse job notes", e); + } } } diff --git a/sdks/java/src/main/java/org/byteveda/taskito/serialization/Notes.java b/sdks/java/src/main/java/org/byteveda/taskito/serialization/Notes.java new file mode 100644 index 00000000..39eedce6 --- /dev/null +++ b/sdks/java/src/main/java/org/byteveda/taskito/serialization/Notes.java @@ -0,0 +1,131 @@ +package org.byteveda.taskito.serialization; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.SerializationFeature; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import org.byteveda.taskito.errors.NotesValidationException; + +/** + * Validation and canonical encoding for the structured {@code notes} field on a job. + * + *

Notes are a small, bounded, user-readable annotation map attached at enqueue time and + * stored as a JSON string in the {@code jobs.notes} column. Validation lives in the SDK (the + * single boundary every enqueue funnels through) so the native layer stays schema-agnostic and + * stores the encoded string verbatim. The bounds keep a note set renderable as a fixed-size + * key/value table without truncation. + * + *

Contract: at most {@link #MAX_FIELDS} top-level keys; each key a non-empty string no longer + * than {@link #MAX_KEY_LENGTH} characters; values are JSON primitives, lists, or maps nested at + * most {@link #MAX_DEPTH} levels; string leaves at most {@link #MAX_VALUE_LENGTH} characters; and + * the encoded document at most {@link #MAX_BYTES} UTF-8 bytes. + */ +public final class Notes { + public static final int MAX_FIELDS = 15; + public static final int MAX_KEY_LENGTH = 64; + public static final int MAX_VALUE_LENGTH = 500; + public static final int MAX_DEPTH = 3; + public static final int MAX_BYTES = 4096; + + // Canonical form: keys sorted, compact separators (Jackson's default), non-ASCII emitted raw. + private static final ObjectWriter CANONICAL = + new ObjectMapper().configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true).writer(); + + private Notes() {} + + /** + * Validate {@code notes} and return its canonical JSON encoding. A {@code null} map returns + * {@code null} (the absence of notes, distinct from an empty map which encodes as {@code "{}"}). + * + * @throws NotesValidationException if any bound in the class contract is violated + */ + public static String encode(Map notes) { + if (notes == null) { + return null; + } + if (notes.size() > MAX_FIELDS) { + throw new NotesValidationException( + "notes may not have more than " + MAX_FIELDS + " fields, got " + notes.size()); + } + for (Map.Entry entry : notes.entrySet()) { + validateKey(entry.getKey()); + validateValue(entry.getKey(), entry.getValue(), 1); + } + String encoded; + try { + encoded = CANONICAL.writeValueAsString(notes); + } catch (JsonProcessingException e) { + throw new NotesValidationException("notes are not serializable: " + e.getMessage(), e); + } + int bytes = encoded.getBytes(StandardCharsets.UTF_8).length; + if (bytes > MAX_BYTES) { + throw new NotesValidationException( + "encoded notes are " + bytes + " bytes, exceeds limit of " + MAX_BYTES + " bytes"); + } + return encoded; + } + + private static void validateKey(Object key) { + if (!(key instanceof String)) { + throw new NotesValidationException("note keys must be strings, got " + typeName(key)); + } + String k = (String) key; + if (k.isEmpty()) { + throw new NotesValidationException("note keys may not be empty strings"); + } + if (k.length() > MAX_KEY_LENGTH) { + throw new NotesValidationException( + "note key '" + k + "' is " + k.length() + " characters, exceeds limit of " + MAX_KEY_LENGTH); + } + } + + private static void validateValue(String path, Object value, int depth) { + if (depth > MAX_DEPTH) { + throw new NotesValidationException( + "note value at '" + path + "' exceeds max nesting depth of " + MAX_DEPTH); + } + if (value == null || value instanceof Boolean) { + return; + } + if (value instanceof String s) { + if (s.length() > MAX_VALUE_LENGTH) { + throw new NotesValidationException("note value at '" + path + "' is " + s.length() + + " characters, exceeds limit of " + MAX_VALUE_LENGTH); + } + return; + } + if (value instanceof Double || value instanceof Float) { + if (!Double.isFinite(((Number) value).doubleValue())) { + // NaN/Infinity serialize to bare tokens that strict JSON readers reject. + throw new NotesValidationException( + "note value at '" + path + "' must be a finite number, got " + value); + } + return; + } + if (value instanceof Number) { + return; + } + if (value instanceof List list) { + for (int i = 0; i < list.size(); i++) { + validateValue(path + "[" + i + "]", list.get(i), depth + 1); + } + return; + } + if (value instanceof Map map) { + for (Map.Entry entry : map.entrySet()) { + validateKey(entry.getKey()); + validateValue(path + "." + entry.getKey(), entry.getValue(), depth + 1); + } + return; + } + throw new NotesValidationException("note value at '" + path + "' has unsupported type " + typeName(value) + + "; must be a string, number, boolean, null, list, or map"); + } + + private static String typeName(Object value) { + return value == null ? "null" : value.getClass().getSimpleName(); + } +} diff --git a/sdks/java/src/main/java/org/byteveda/taskito/task/EnqueueOptions.java b/sdks/java/src/main/java/org/byteveda/taskito/task/EnqueueOptions.java index ddd06bc7..c19a6c0f 100644 --- a/sdks/java/src/main/java/org/byteveda/taskito/task/EnqueueOptions.java +++ b/sdks/java/src/main/java/org/byteveda/taskito/task/EnqueueOptions.java @@ -4,6 +4,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.time.Duration; import java.util.List; +import java.util.Map; +import org.byteveda.taskito.serialization.Notes; /** Immutable per-enqueue options. Unset fields take core defaults. */ @JsonInclude(JsonInclude.Include.NON_NULL) @@ -35,6 +37,10 @@ public final class EnqueueOptions { @JsonProperty("dependsOn") private final List dependsOn; + // Canonical JSON encoding of the structured notes (validated at build time), or null. + @JsonProperty("notes") + private final String notes; + // Idempotency inputs resolve to uniqueKey locally (see DefaultTaskito) and never cross // the wire, so they carry no @JsonProperty and are not serialized into the options JSON. private final Boolean idempotent; @@ -51,6 +57,7 @@ private EnqueueOptions(Builder b) { this.metadata = b.metadata; this.namespace = b.namespace; this.dependsOn = b.dependsOn; + this.notes = b.notes; this.idempotent = b.idempotent; this.idempotencyKey = b.idempotencyKey; } @@ -75,6 +82,7 @@ public Builder toBuilder() { b.metadata = metadata; b.namespace = namespace; b.dependsOn = dependsOn; + b.notes = notes; b.idempotent = idempotent; b.idempotencyKey = idempotencyKey; return b; @@ -113,6 +121,7 @@ public static final class Builder { private String metadata; private String namespace; private List dependsOn; + private String notes; private Boolean idempotent; private String idempotencyKey; @@ -199,6 +208,19 @@ public Builder dependsOn(List jobIds) { return this; } + /** + * Attach a bounded, user-readable annotation map to the job (validated and canonically + * encoded now, so a contract violation fails fast). Distinct from the opaque + * {@link #metadata} blob. Passing {@code null} clears any previously set notes. + * + * @throws org.byteveda.taskito.errors.NotesValidationException if the map breaks the + * {@link Notes} contract (field/key/value/depth/size limits) + */ + public Builder notes(Map notes) { + this.notes = Notes.encode(notes); + return this; + } + /** * Dedupe this enqueue by auto-deriving a {@code uniqueKey} from the task name and * payload. A duplicate enqueue is a no-op while the first job is pending or running. diff --git a/sdks/java/src/test/java/org/byteveda/taskito/NotesTest.java b/sdks/java/src/test/java/org/byteveda/taskito/NotesTest.java new file mode 100644 index 00000000..9844094f --- /dev/null +++ b/sdks/java/src/test/java/org/byteveda/taskito/NotesTest.java @@ -0,0 +1,100 @@ +package org.byteveda.taskito; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.Map; +import org.byteveda.taskito.errors.NotesValidationException; +import org.byteveda.taskito.model.Job; +import org.byteveda.taskito.serialization.Notes; +import org.byteveda.taskito.task.EnqueueOptions; +import org.byteveda.taskito.task.Task; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.io.TempDir; + +class NotesTest { + + @Test + void nullEncodesToNullAndEmptyToObject() { + assertNull(Notes.encode(null)); + assertEquals("{}", Notes.encode(Map.of())); + } + + @Test + void encodesWithSortedKeys() { + Map notes = new LinkedHashMap<>(); + notes.put("b", 1); + notes.put("a", "x"); + assertEquals("{\"a\":\"x\",\"b\":1}", Notes.encode(notes)); + } + + @Test + void rejectsTooManyFields() { + Map notes = new LinkedHashMap<>(); + for (int i = 0; i <= Notes.MAX_FIELDS; i++) { + notes.put("k" + i, i); + } + assertThrows(NotesValidationException.class, () -> Notes.encode(notes)); + } + + @Test + void rejectsOverlongKey() { + String key = "k".repeat(Notes.MAX_KEY_LENGTH + 1); + assertThrows(NotesValidationException.class, () -> Notes.encode(Map.of(key, 1))); + } + + @Test + void rejectsOverlongStringLeaf() { + String value = "v".repeat(Notes.MAX_VALUE_LENGTH + 1); + assertThrows(NotesValidationException.class, () -> Notes.encode(Map.of("k", value))); + } + + @Test + void rejectsExcessiveNesting() { + // depth 3 leaf is allowed; depth 4 is not. + Notes.encode(Map.of("a", Map.of("b", Map.of("c", 1)))); + assertThrows( + NotesValidationException.class, () -> Notes.encode(Map.of("a", Map.of("b", Map.of("c", Map.of("d", 1)))))); + } + + @Test + void rejectsUnsupportedLeafType() { + assertThrows(NotesValidationException.class, () -> Notes.encode(Map.of("k", new Object()))); + } + + @Test + void rejectsNonFiniteNumber() { + assertThrows(NotesValidationException.class, () -> Notes.encode(Map.of("k", Double.NaN))); + } + + @Test + void rejectsOversizedEncoding() { + // 15 fields (the max) each holding a 500-char value (the max) blows past the byte cap. + Map notes = new LinkedHashMap<>(); + String big = "x".repeat(Notes.MAX_VALUE_LENGTH); + for (int i = 0; i < Notes.MAX_FIELDS; i++) { + notes.put("k" + i, big); + } + assertThrows(NotesValidationException.class, () -> Notes.encode(notes)); + } + + @Test + @Timeout(30) + void notesRoundTripThroughStorage(@TempDir Path dir) throws Exception { + try (Taskito queue = + Taskito.builder().url(dir.resolve("notes.db").toString()).open()) { + Task task = Task.of("notes.echo", String.class); + Map notes = new LinkedHashMap<>(); + notes.put("env", "prod"); + notes.put("attempt", 3); + String id = queue.enqueue(task, "hi", EnqueueOptions.builder().notes(notes).build()); + + Job job = queue.getJob(id).orElseThrow(); + assertEquals(notes, job.notesMap().orElseThrow()); + } + } +} From 385b2b3e1a45a12753b7dd60016368a46fb5d280 Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:40:34 +0530 Subject: [PATCH 4/8] feat(java): per-task circuit breakers --- crates/taskito-java/src/convert.rs | 45 ++++++- crates/taskito-java/src/queue/inspect.rs | 21 ++- crates/taskito-java/src/worker.rs | 17 ++- .../processor/TaskHandlerProcessor.java | 27 ++++ .../org/byteveda/taskito/DefaultTaskito.java | 6 + .../java/org/byteveda/taskito/Taskito.java | 4 + .../taskito/annotation/TaskHandler.java | 15 +++ .../taskito/internal/JniQueueBackend.java | 5 + .../taskito/internal/NativeQueue.java | 2 + .../taskito/model/CircuitBreakerState.java | 53 ++++++++ .../byteveda/taskito/spi/QueueBackend.java | 5 + .../taskito/task/CircuitBreakerConfig.java | 124 ++++++++++++++++++ .../java/org/byteveda/taskito/task/Task.java | 31 ++++- .../org/byteveda/taskito/worker/Worker.java | 54 +++++--- .../byteveda/taskito/AnnotatedPolicies.java | 23 ++++ .../byteveda/taskito/AnnotatedPolicyTest.java | 36 +++++ .../byteveda/taskito/CircuitBreakerTest.java | 93 +++++++++++++ 17 files changed, 536 insertions(+), 25 deletions(-) create mode 100644 sdks/java/src/main/java/org/byteveda/taskito/model/CircuitBreakerState.java create mode 100644 sdks/java/src/main/java/org/byteveda/taskito/task/CircuitBreakerConfig.java create mode 100644 sdks/java/src/test/java/org/byteveda/taskito/AnnotatedPolicies.java create mode 100644 sdks/java/src/test/java/org/byteveda/taskito/AnnotatedPolicyTest.java create mode 100644 sdks/java/src/test/java/org/byteveda/taskito/CircuitBreakerTest.java diff --git a/crates/taskito-java/src/convert.rs b/crates/taskito-java/src/convert.rs index e9c897cf..4de435d9 100644 --- a/crates/taskito-java/src/convert.rs +++ b/crates/taskito-java/src/convert.rs @@ -5,8 +5,10 @@ use serde::{Deserialize, Serialize}; use taskito_core::job::{now_millis, Job, NewJob}; +use taskito_core::resilience::circuit_breaker::CircuitState; use taskito_core::storage::models::{ - JobErrorRow, LockInfoRow, PeriodicTaskRow, TaskLogRow, TaskMetricRow, WorkerRow, + CircuitBreakerRow, JobErrorRow, LockInfoRow, PeriodicTaskRow, TaskLogRow, TaskMetricRow, + WorkerRow, }; use taskito_core::storage::{DeadJob, QueueStats}; @@ -161,6 +163,40 @@ impl<'a> From<&'a Job> for JobView<'a> { } } +/// Java-facing view of a task's circuit-breaker state. `state` is the lowercase wire +/// string (`closed`/`open`/`half_open`); timestamps are Unix milliseconds. +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CircuitBreakerView<'a> { + pub task_name: &'a str, + pub state: &'static str, + pub failure_count: i32, + pub threshold: i32, + pub window_ms: i64, + pub cooldown_ms: i64, + pub opened_at: Option, + pub last_failure_at: Option, + pub half_open_max_probes: i32, + pub half_open_success_rate: f64, +} + +impl<'a> From<&'a CircuitBreakerRow> for CircuitBreakerView<'a> { + fn from(r: &'a CircuitBreakerRow) -> Self { + Self { + task_name: &r.task_name, + state: CircuitState::from_i32(r.state).as_str(), + failure_count: r.failure_count, + threshold: r.threshold, + window_ms: r.window_ms, + cooldown_ms: r.cooldown_ms, + opened_at: r.opened_at, + last_failure_at: r.last_failure_at, + half_open_max_probes: r.half_open_max_probes, + half_open_success_rate: r.half_open_success_rate, + } + } +} + /// Options accepted by `NativeQueue.runWorker`. #[derive(Deserialize, Default)] #[serde(rename_all = "camelCase", default)] @@ -188,6 +224,13 @@ pub struct TaskRetryConfig { pub base_delay_ms: Option, pub max_delay_ms: Option, pub custom_delays_ms: Option>, + /// Circuit-breaker knobs. `threshold` present ⇒ the breaker is enabled; the SDK + /// already converts window/cooldown to milliseconds, so these are stored as-is. + pub circuit_breaker_threshold: Option, + pub circuit_breaker_window_ms: Option, + pub circuit_breaker_cooldown_ms: Option, + pub circuit_breaker_half_open_probes: Option, + pub circuit_breaker_half_open_success_rate: Option, } /// Filter accepted by `NativeQueue.listJobs`. diff --git a/crates/taskito-java/src/queue/inspect.rs b/crates/taskito-java/src/queue/inspect.rs index 26523c67..8505bbd7 100644 --- a/crates/taskito-java/src/queue/inspect.rs +++ b/crates/taskito-java/src/queue/inspect.rs @@ -9,7 +9,8 @@ use taskito_core::Storage; use super::borrow_queue; use crate::convert::{ - status_code, to_json, JobErrorView, JobFilter, JobView, MetricView, StatsView, WorkerView, + status_code, to_json, CircuitBreakerView, JobErrorView, JobFilter, JobView, MetricView, + StatsView, WorkerView, }; use crate::error::BindingError; use crate::ffi::{guard, new_string, read_optional_string, read_string}; @@ -146,3 +147,21 @@ pub extern "system" fn Java_org_byteveda_taskito_internal_NativeQueue_listWorker new_string(env, to_json(&views)?) }) } + +/// `String listCircuitBreakers(long handle)` — every configured task's breaker state. +#[no_mangle] +pub extern "system" fn Java_org_byteveda_taskito_internal_NativeQueue_listCircuitBreakers< + 'local, +>( + mut env: JNIEnv<'local>, + _class: JClass<'local>, + handle: jlong, +) -> jstring { + guard(&mut env, std::ptr::null_mut(), |env| { + let queue = unsafe { borrow_queue(handle) }; + let breakers = queue.storage.list_circuit_breakers()?; + let views: Vec = + breakers.iter().map(CircuitBreakerView::from).collect(); + new_string(env, to_json(&views)?) + }) +} diff --git a/crates/taskito-java/src/worker.rs b/crates/taskito-java/src/worker.rs index 51bfa6f2..c785d87d 100644 --- a/crates/taskito-java/src/worker.rs +++ b/crates/taskito-java/src/worker.rs @@ -12,6 +12,7 @@ use std::time::Duration; use jni::objects::{GlobalRef, JByteArray, JClass, JObject, JString, JValue}; use jni::sys::jlong; use jni::JNIEnv; +use taskito_core::resilience::circuit_breaker::CircuitBreakerConfig; use taskito_core::resilience::retry::RetryPolicy; use taskito_core::scheduler::{ResultOutcome, TaskConfig}; use taskito_core::worker::WorkerDispatcher; @@ -181,12 +182,26 @@ fn register_task_policies(scheduler: &mut Scheduler, configs: Option listWorkers() { return decodeList(backend.listWorkersJson(), WorkerInfo.class); } + @Override + public List listCircuitBreakers() { + return decodeList(backend.listCircuitBreakersJson(), CircuitBreakerState.class); + } + // ── Admin ─────────────────────────────────────────────────────── @Override 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 b54583c2..2642146c 100644 --- a/sdks/java/src/main/java/org/byteveda/taskito/Taskito.java +++ b/sdks/java/src/main/java/org/byteveda/taskito/Taskito.java @@ -21,6 +21,7 @@ import org.byteveda.taskito.locks.Lock; import org.byteveda.taskito.locks.LockInfo; import org.byteveda.taskito.middleware.Middleware; +import org.byteveda.taskito.model.CircuitBreakerState; import org.byteveda.taskito.model.DeadJob; import org.byteveda.taskito.model.Job; import org.byteveda.taskito.model.JobError; @@ -178,6 +179,9 @@ static Builder builder() { List listWorkers(); + /** Every configured task's circuit-breaker state. */ + List listCircuitBreakers(); + // ── Admin ─────────────────────────────────────────────────────── List listDead(long limit, long offset); diff --git a/sdks/java/src/main/java/org/byteveda/taskito/annotation/TaskHandler.java b/sdks/java/src/main/java/org/byteveda/taskito/annotation/TaskHandler.java index b1dc2252..0dafa40d 100644 --- a/sdks/java/src/main/java/org/byteveda/taskito/annotation/TaskHandler.java +++ b/sdks/java/src/main/java/org/byteveda/taskito/annotation/TaskHandler.java @@ -35,4 +35,19 @@ /** Auto-derive an idempotency {@code uniqueKey} from the payload on every enqueue. */ boolean idempotent() default false; + + /** Circuit-breaker failure threshold; 0 (default) leaves the breaker off. */ + int circuitBreakerThreshold() default 0; + + /** Rolling window, in seconds, over which failures count toward the threshold. */ + long circuitBreakerWindowSeconds() default 60; + + /** How long, in seconds, the breaker stays open before admitting half-open probes. */ + long circuitBreakerCooldownSeconds() default 300; + + /** Probe runs admitted while half-open. */ + int circuitBreakerHalfOpenProbes() default 5; + + /** Probe success rate (0.0–1.0) required to re-close the breaker. */ + double circuitBreakerHalfOpenSuccessRate() default 0.8; } diff --git a/sdks/java/src/main/java/org/byteveda/taskito/internal/JniQueueBackend.java b/sdks/java/src/main/java/org/byteveda/taskito/internal/JniQueueBackend.java index 47647f5a..73a982b4 100644 --- a/sdks/java/src/main/java/org/byteveda/taskito/internal/JniQueueBackend.java +++ b/sdks/java/src/main/java/org/byteveda/taskito/internal/JniQueueBackend.java @@ -122,6 +122,11 @@ public String listWorkersJson() { return withOpenHandle(() -> NativeQueue.listWorkers(handle)); } + @Override + public String listCircuitBreakersJson() { + return withOpenHandle(() -> NativeQueue.listCircuitBreakers(handle)); + } + @Override public String listDeadJson(long limit, long offset) { return withOpenHandle(() -> NativeQueue.listDead(handle, limit, offset)); diff --git a/sdks/java/src/main/java/org/byteveda/taskito/internal/NativeQueue.java b/sdks/java/src/main/java/org/byteveda/taskito/internal/NativeQueue.java index b43c7424..715026bd 100644 --- a/sdks/java/src/main/java/org/byteveda/taskito/internal/NativeQueue.java +++ b/sdks/java/src/main/java/org/byteveda/taskito/internal/NativeQueue.java @@ -53,6 +53,8 @@ private NativeQueue() {} public static native String listWorkers(long handle); + public static native String listCircuitBreakers(long handle); + // ── Admin ─────────────────────────────────────────────────────── public static native String listDead(long handle, long limit, long offset); diff --git a/sdks/java/src/main/java/org/byteveda/taskito/model/CircuitBreakerState.java b/sdks/java/src/main/java/org/byteveda/taskito/model/CircuitBreakerState.java new file mode 100644 index 00000000..64b515a3 --- /dev/null +++ b/sdks/java/src/main/java/org/byteveda/taskito/model/CircuitBreakerState.java @@ -0,0 +1,53 @@ +package org.byteveda.taskito.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * A task's circuit-breaker state as observed by the core. {@code state} is the lowercase wire + * value ({@code "closed"}, {@code "open"}, or {@code "half_open"}). Timestamps are Unix + * milliseconds, or {@code null} when the breaker has not reached that transition. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class CircuitBreakerState { + public final String taskName; + public final String state; + public final int failureCount; + public final int threshold; + public final long windowMs; + public final long cooldownMs; + public final Long openedAt; + public final Long lastFailureAt; + public final int halfOpenMaxProbes; + public final double halfOpenSuccessRate; + + @JsonCreator + public CircuitBreakerState( + @JsonProperty("taskName") String taskName, + @JsonProperty("state") String state, + @JsonProperty("failureCount") int failureCount, + @JsonProperty("threshold") int threshold, + @JsonProperty("windowMs") long windowMs, + @JsonProperty("cooldownMs") long cooldownMs, + @JsonProperty("openedAt") Long openedAt, + @JsonProperty("lastFailureAt") Long lastFailureAt, + @JsonProperty("halfOpenMaxProbes") int halfOpenMaxProbes, + @JsonProperty("halfOpenSuccessRate") double halfOpenSuccessRate) { + this.taskName = taskName; + this.state = state; + this.failureCount = failureCount; + this.threshold = threshold; + this.windowMs = windowMs; + this.cooldownMs = cooldownMs; + this.openedAt = openedAt; + this.lastFailureAt = lastFailureAt; + this.halfOpenMaxProbes = halfOpenMaxProbes; + this.halfOpenSuccessRate = halfOpenSuccessRate; + } + + /** Whether the breaker is currently open (fast-failing this task). */ + public boolean isOpen() { + return "open".equals(state); + } +} diff --git a/sdks/java/src/main/java/org/byteveda/taskito/spi/QueueBackend.java b/sdks/java/src/main/java/org/byteveda/taskito/spi/QueueBackend.java index 1313827e..69f054ef 100644 --- a/sdks/java/src/main/java/org/byteveda/taskito/spi/QueueBackend.java +++ b/sdks/java/src/main/java/org/byteveda/taskito/spi/QueueBackend.java @@ -52,6 +52,11 @@ public interface QueueBackend extends AutoCloseable { String listWorkersJson(); + /** Circuit-breaker states as a JSON array; defaults to none for backends without breakers. */ + default String listCircuitBreakersJson() { + return "[]"; + } + // ── Admin ─────────────────────────────────────────────────────── String listDeadJson(long limit, long offset); diff --git a/sdks/java/src/main/java/org/byteveda/taskito/task/CircuitBreakerConfig.java b/sdks/java/src/main/java/org/byteveda/taskito/task/CircuitBreakerConfig.java new file mode 100644 index 00000000..7d388983 --- /dev/null +++ b/sdks/java/src/main/java/org/byteveda/taskito/task/CircuitBreakerConfig.java @@ -0,0 +1,124 @@ +package org.byteveda.taskito.task; + +import java.time.Duration; +import java.util.Objects; + +/** + * Per-task circuit-breaker configuration. The breaker opens once {@link #threshold} failures + * occur within {@link #window}, stays open for {@link #cooldown}, then admits up to + * {@link #halfOpenProbes} probe runs and re-closes only if their success rate reaches + * {@link #halfOpenSuccessRate}. Enforcement lives in the core scheduler; this only supplies the + * configuration, registered when the worker starts. + */ +public final class CircuitBreakerConfig { + private final int threshold; + private final Duration window; + private final Duration cooldown; + private final int halfOpenProbes; + private final double halfOpenSuccessRate; + + private CircuitBreakerConfig(Builder b) { + this.threshold = b.threshold; + this.window = b.window; + this.cooldown = b.cooldown; + this.halfOpenProbes = b.halfOpenProbes; + this.halfOpenSuccessRate = b.halfOpenSuccessRate; + } + + /** A builder for a breaker that opens after {@code threshold} failures in the window. */ + public static Builder builder(int threshold) { + return new Builder(threshold); + } + + /** A breaker with default window/cooldown/probe settings, opening after {@code threshold} failures. */ + public static CircuitBreakerConfig of(int threshold) { + return builder(threshold).build(); + } + + public int threshold() { + return threshold; + } + + public Duration window() { + return window; + } + + public Duration cooldown() { + return cooldown; + } + + public int halfOpenProbes() { + return halfOpenProbes; + } + + public double halfOpenSuccessRate() { + return halfOpenSuccessRate; + } + + public static final class Builder { + private final int threshold; + private Duration window = Duration.ofSeconds(60); + private Duration cooldown = Duration.ofSeconds(300); + private int halfOpenProbes = 5; + private double halfOpenSuccessRate = 0.8; + + private Builder(int threshold) { + if (threshold <= 0) { + throw new IllegalArgumentException("circuit-breaker threshold must be > 0"); + } + this.threshold = threshold; + } + + /** Rolling window in which failures are counted toward the threshold. */ + public Builder window(Duration window) { + this.window = requirePositive(window, "window"); + return this; + } + + /** Convenience for {@code window(Duration.ofSeconds(seconds))}. */ + public Builder windowSeconds(long seconds) { + return window(Duration.ofSeconds(seconds)); + } + + /** How long the breaker stays open before admitting half-open probes. */ + public Builder cooldown(Duration cooldown) { + this.cooldown = requirePositive(cooldown, "cooldown"); + return this; + } + + /** Convenience for {@code cooldown(Duration.ofSeconds(seconds))}. */ + public Builder cooldownSeconds(long seconds) { + return cooldown(Duration.ofSeconds(seconds)); + } + + /** Number of probe runs admitted while half-open. */ + public Builder halfOpenProbes(int halfOpenProbes) { + if (halfOpenProbes <= 0) { + throw new IllegalArgumentException("halfOpenProbes must be > 0"); + } + this.halfOpenProbes = halfOpenProbes; + return this; + } + + /** Success rate (0.0–1.0) among probes required to re-close the breaker. */ + public Builder halfOpenSuccessRate(double halfOpenSuccessRate) { + if (!(halfOpenSuccessRate >= 0.0 && halfOpenSuccessRate <= 1.0)) { + throw new IllegalArgumentException("halfOpenSuccessRate must be within [0.0, 1.0]"); + } + this.halfOpenSuccessRate = halfOpenSuccessRate; + return this; + } + + public CircuitBreakerConfig build() { + return new CircuitBreakerConfig(this); + } + + private static Duration requirePositive(Duration value, String name) { + Objects.requireNonNull(value, name + " must not be null"); + if (value.isZero() || value.isNegative()) { + throw new IllegalArgumentException(name + " must be positive"); + } + return value; + } + } +} diff --git a/sdks/java/src/main/java/org/byteveda/taskito/task/Task.java b/sdks/java/src/main/java/org/byteveda/taskito/task/Task.java index b71a8d8f..1feed424 100644 --- a/sdks/java/src/main/java/org/byteveda/taskito/task/Task.java +++ b/sdks/java/src/main/java/org/byteveda/taskito/task/Task.java @@ -21,6 +21,7 @@ public final class Task { private final RetryPolicy retryPolicy; private final List codecs; private final boolean idempotent; + private final CircuitBreakerConfig circuitBreaker; private Task( String name, @@ -28,7 +29,8 @@ private Task( EnqueueOptions options, RetryPolicy retryPolicy, List codecs, - boolean idempotent) { + boolean idempotent, + CircuitBreakerConfig circuitBreaker) { this.name = Objects.requireNonNull(name, "task name must not be null"); if (name.trim().isEmpty()) { throw new IllegalArgumentException("task name must not be blank"); @@ -38,21 +40,22 @@ private Task( this.retryPolicy = retryPolicy; this.codecs = List.copyOf(codecs); this.idempotent = idempotent; + this.circuitBreaker = circuitBreaker; } /** A task whose payload deserializes to {@code payloadType}. */ public static Task of(String name, Class payloadType) { - return new Task<>(name, payloadType, EnqueueOptions.none(), null, List.of(), false); + return new Task<>(name, payloadType, EnqueueOptions.none(), null, List.of(), false, null); } /** A task whose payload deserializes to a generic type, e.g. {@code new TypeReference>(){}}. */ public static Task of(String name, TypeReference payloadType) { - return new Task<>(name, payloadType.getType(), EnqueueOptions.none(), null, List.of(), false); + return new Task<>(name, payloadType.getType(), EnqueueOptions.none(), null, List.of(), false, null); } /** A copy of this task with the given default options. */ public Task withOptions(EnqueueOptions options) { - return new Task<>(name, payloadType, options, retryPolicy, codecs, idempotent); + return new Task<>(name, payloadType, options, retryPolicy, codecs, idempotent, circuitBreaker); } /** @@ -61,7 +64,7 @@ public Task withOptions(EnqueueOptions options) { * from {@link #maxRetries}. */ public Task retryPolicy(RetryPolicy retryPolicy) { - return new Task<>(name, payloadType, options, retryPolicy, codecs, idempotent); + return new Task<>(name, payloadType, options, retryPolicy, codecs, idempotent, circuitBreaker); } /** @@ -71,7 +74,7 @@ public Task retryPolicy(RetryPolicy retryPolicy) { * {@code Taskito.builder().codec(name, codec)} on producers and workers. */ public Task codecs(String... codecs) { - return new Task<>(name, payloadType, options, retryPolicy, Arrays.asList(codecs), idempotent); + return new Task<>(name, payloadType, options, retryPolicy, Arrays.asList(codecs), idempotent, circuitBreaker); } /** @@ -80,7 +83,16 @@ public Task codecs(String... codecs) { * A per-enqueue {@link EnqueueOptions#idempotent(boolean)} overrides this default. */ public Task idempotent(boolean idempotent) { - return new Task<>(name, payloadType, options, retryPolicy, codecs, idempotent); + return new Task<>(name, payloadType, options, retryPolicy, codecs, idempotent, circuitBreaker); + } + + /** + * A copy of this task guarded by {@code circuitBreaker}. The worker registers it on + * {@code start()}; once the breaker opens, the scheduler stops dispatching this task until + * it recovers. + */ + public Task circuitBreaker(CircuitBreakerConfig circuitBreaker) { + return new Task<>(name, payloadType, options, retryPolicy, codecs, idempotent, circuitBreaker); } public Task queue(String queue) { @@ -143,4 +155,9 @@ public List codecNames() { public boolean idempotent() { return idempotent; } + + /** This task's circuit-breaker configuration, or {@code null} when none is set. */ + public CircuitBreakerConfig circuitBreaker() { + return circuitBreaker; + } } 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 4333c97e..6509011a 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 @@ -8,9 +8,11 @@ import java.util.EnumMap; import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -32,6 +34,7 @@ import org.byteveda.taskito.serialization.Serializer; import org.byteveda.taskito.spi.QueueBackend; import org.byteveda.taskito.spi.WorkerControl; +import org.byteveda.taskito.task.CircuitBreakerConfig; import org.byteveda.taskito.task.RetryPolicy; import org.byteveda.taskito.task.Task; import org.byteveda.taskito.task.TaskFunction; @@ -179,6 +182,7 @@ public static final class Builder { private final Map codecs; private final Map handlers = new HashMap<>(); private final Map taskPolicies = new HashMap<>(); + private final Map taskCircuitBreakers = new HashMap<>(); private final Map>> listeners = new EnumMap<>(EventName.class); private List queues; private int concurrency; @@ -230,12 +234,16 @@ public Builder register(Handler handler) { return this; } - /** Remember a task's retry-backoff curve so {@code start()} registers it. */ + /** Remember a task's retry-backoff curve and circuit breaker so {@code start()} registers them. */ private void capturePolicy(Task task) { RetryPolicy policy = task.retryPolicy(); if (policy != null) { taskPolicies.put(task.name(), policy); } + CircuitBreakerConfig breaker = task.circuitBreaker(); + if (breaker != null) { + taskCircuitBreakers.put(task.name(), breaker); + } } /** Register every handler in a {@link HandlerRegistry} (e.g. a generated {@code XxxTasks.handlers}). */ @@ -404,7 +412,7 @@ private String encodeOptions() { if (batchSize != null) { options.put("batchSize", batchSize); } - if (!taskPolicies.isEmpty()) { + if (!taskPolicies.isEmpty() || !taskCircuitBreakers.isEmpty()) { options.put("taskConfigs", encodeTaskConfigs()); } if (mesh != null) { @@ -417,25 +425,41 @@ private String encodeOptions() { } } - /** Serialize each captured retry policy into the wire shape the binding reads. */ + /** + * Serialize each task's per-task config (retry curve and/or circuit breaker) into the + * wire shape the binding reads — one entry per task, merging both sources by name. + */ private List> encodeTaskConfigs() { - List> configs = new ArrayList<>(taskPolicies.size()); - taskPolicies.forEach((name, policy) -> { + Set names = new LinkedHashSet<>(taskPolicies.keySet()); + names.addAll(taskCircuitBreakers.keySet()); + List> configs = new ArrayList<>(names.size()); + for (String name : names) { Map config = new LinkedHashMap<>(); config.put("name", name); - if (policy.baseDelay() != null) { - config.put("baseDelayMs", policy.baseDelay().toMillis()); + RetryPolicy policy = taskPolicies.get(name); + if (policy != null) { + if (policy.baseDelay() != null) { + config.put("baseDelayMs", policy.baseDelay().toMillis()); + } + if (policy.maxDelay() != null) { + config.put("maxDelayMs", policy.maxDelay().toMillis()); + } + if (!policy.customDelays().isEmpty()) { + List delaysMs = new ArrayList<>(policy.customDelays().size()); + policy.customDelays().forEach(delay -> delaysMs.add(delay.toMillis())); + config.put("customDelaysMs", delaysMs); + } } - if (policy.maxDelay() != null) { - config.put("maxDelayMs", policy.maxDelay().toMillis()); - } - if (!policy.customDelays().isEmpty()) { - List delaysMs = new ArrayList<>(policy.customDelays().size()); - policy.customDelays().forEach(delay -> delaysMs.add(delay.toMillis())); - config.put("customDelaysMs", delaysMs); + CircuitBreakerConfig breaker = taskCircuitBreakers.get(name); + if (breaker != null) { + config.put("circuitBreakerThreshold", breaker.threshold()); + config.put("circuitBreakerWindowMs", breaker.window().toMillis()); + config.put("circuitBreakerCooldownMs", breaker.cooldown().toMillis()); + config.put("circuitBreakerHalfOpenProbes", breaker.halfOpenProbes()); + config.put("circuitBreakerHalfOpenSuccessRate", breaker.halfOpenSuccessRate()); } configs.add(config); - }); + } return configs; } diff --git a/sdks/java/src/test/java/org/byteveda/taskito/AnnotatedPolicies.java b/sdks/java/src/test/java/org/byteveda/taskito/AnnotatedPolicies.java new file mode 100644 index 00000000..5067d37a --- /dev/null +++ b/sdks/java/src/test/java/org/byteveda/taskito/AnnotatedPolicies.java @@ -0,0 +1,23 @@ +package org.byteveda.taskito; + +import org.byteveda.taskito.annotation.TaskHandler; + +/** Test fixture exercising the per-task policy attributes of {@code @TaskHandler}. */ +class AnnotatedPolicies { + + @TaskHandler(value = "ap.idem", idempotent = true) + String idem(String s) { + return s; + } + + @TaskHandler( + value = "ap.cb", + circuitBreakerThreshold = 3, + circuitBreakerWindowSeconds = 30, + circuitBreakerCooldownSeconds = 120, + circuitBreakerHalfOpenProbes = 2, + circuitBreakerHalfOpenSuccessRate = 0.5) + String cb(String s) { + return s; + } +} diff --git a/sdks/java/src/test/java/org/byteveda/taskito/AnnotatedPolicyTest.java b/sdks/java/src/test/java/org/byteveda/taskito/AnnotatedPolicyTest.java new file mode 100644 index 00000000..bf032c56 --- /dev/null +++ b/sdks/java/src/test/java/org/byteveda/taskito/AnnotatedPolicyTest.java @@ -0,0 +1,36 @@ +package org.byteveda.taskito; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Duration; +import org.byteveda.taskito.task.CircuitBreakerConfig; +import org.junit.jupiter.api.Test; + +/** The {@code @TaskHandler} processor threads per-task policy attributes onto the generated tasks. */ +class AnnotatedPolicyTest { + + @Test + void idempotentAttributeReachesGeneratedTask() { + assertTrue(AnnotatedPoliciesTasks.IDEM.idempotent()); + assertFalse(AnnotatedPoliciesTasks.CB.idempotent()); + } + + @Test + void circuitBreakerAttributesReachGeneratedTask() { + CircuitBreakerConfig cb = AnnotatedPoliciesTasks.CB.circuitBreaker(); + assertNotNull(cb, "the annotated threshold should produce a circuit breaker"); + assertEquals(3, cb.threshold()); + assertEquals(Duration.ofSeconds(30), cb.window()); + assertEquals(Duration.ofSeconds(120), cb.cooldown()); + assertEquals(2, cb.halfOpenProbes()); + assertEquals(0.5, cb.halfOpenSuccessRate()); + } + + @Test + void noThresholdMeansNoBreaker() { + assertEquals(null, AnnotatedPoliciesTasks.IDEM.circuitBreaker()); + } +} diff --git a/sdks/java/src/test/java/org/byteveda/taskito/CircuitBreakerTest.java b/sdks/java/src/test/java/org/byteveda/taskito/CircuitBreakerTest.java new file mode 100644 index 00000000..9bed39ee --- /dev/null +++ b/sdks/java/src/test/java/org/byteveda/taskito/CircuitBreakerTest.java @@ -0,0 +1,93 @@ +package org.byteveda.taskito; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Path; +import java.time.Duration; +import java.util.List; +import java.util.Optional; +import org.byteveda.taskito.model.CircuitBreakerState; +import org.byteveda.taskito.task.CircuitBreakerConfig; +import org.byteveda.taskito.task.Task; +import org.byteveda.taskito.worker.Worker; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.io.TempDir; + +class CircuitBreakerTest { + + @Test + void builderValidatesInputs() { + assertThrows(IllegalArgumentException.class, () -> CircuitBreakerConfig.of(0)); + assertThrows(IllegalArgumentException.class, () -> CircuitBreakerConfig.builder(1).halfOpenProbes(0)); + assertThrows( + IllegalArgumentException.class, + () -> CircuitBreakerConfig.builder(1).halfOpenSuccessRate(1.5)); + assertThrows( + IllegalArgumentException.class, + () -> CircuitBreakerConfig.builder(1).window(Duration.ZERO)); + } + + @Test + void builderAppliesDefaults() { + CircuitBreakerConfig cb = CircuitBreakerConfig.of(3); + assertEquals(3, cb.threshold()); + assertEquals(Duration.ofSeconds(60), cb.window()); + assertEquals(Duration.ofSeconds(300), cb.cooldown()); + assertEquals(5, cb.halfOpenProbes()); + assertEquals(0.8, cb.halfOpenSuccessRate()); + } + + @Test + @Timeout(60) + void breakerOpensAfterThresholdFailures(@TempDir Path dir) throws Exception { + try (Taskito queue = + Taskito.builder().url(dir.resolve("cb.db").toString()).open()) { + Task failing = Task.of("cb.fail", String.class) + .maxRetries(0) + .circuitBreaker(CircuitBreakerConfig.of(2)); + + // Enqueue more jobs than the threshold; once two fail, the breaker opens and gates the rest. + for (int i = 0; i < 5; i++) { + queue.enqueue(failing, "boom"); + } + + try (Worker worker = queue.worker() + .handle(failing, p -> { + throw new RuntimeException("boom"); + }) + .start()) { + CircuitBreakerState state = awaitOpenBreaker(queue, "cb.fail", Duration.ofSeconds(40)); + assertTrue(state.isOpen(), "breaker should be open after reaching the failure threshold"); + assertEquals(2, state.threshold); + } + } + } + + private static CircuitBreakerState awaitOpenBreaker(Taskito queue, String taskName, Duration timeout) + throws InterruptedException { + long deadline = System.nanoTime() + timeout.toNanos(); + while (System.nanoTime() < deadline) { + Optional found = queue.listCircuitBreakers().stream() + .filter(b -> taskName.equals(b.taskName)) + .findFirst(); + if (found.isPresent() && found.get().isOpen()) { + return found.get(); + } + Thread.sleep(100); + } + throw new AssertionError("circuit breaker for '" + taskName + "' did not open within " + timeout); + } + + @Test + @Timeout(30) + void listReturnsEmptyWithoutBreakers(@TempDir Path dir) throws Exception { + try (Taskito queue = + Taskito.builder().url(dir.resolve("cb_empty.db").toString()).open()) { + List breakers = queue.listCircuitBreakers(); + assertTrue(breakers.isEmpty()); + } + } +} From 66420d664650bc6fee765bdd73fcf31c317bdcaf Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:54:47 +0530 Subject: [PATCH 5/8] test(java): group tests into feature-area packages --- .../java/org/byteveda/taskito/{ => autoscale}/ScalerTest.java | 3 ++- .../org/byteveda/taskito/{ => codegen}/AnnotatedPolicies.java | 2 +- .../byteveda/taskito/{ => codegen}/AnnotatedPolicyTest.java | 2 +- .../taskito/{ => codegen}/AnnotationProcessorTest.java | 3 ++- .../test/java/org/byteveda/taskito/{ => codegen}/Greeter.java | 2 +- .../org/byteveda/taskito/{ => codegen}/ResourceGreeter.java | 2 +- .../org/byteveda/taskito/{ => contrib}/TaskitoLoggerTest.java | 2 +- .../java/org/byteveda/taskito/{ => contrib}/WebhookTest.java | 3 ++- .../org/byteveda/taskito/{ => core}/DeadLetterByTaskTest.java | 3 ++- .../java/org/byteveda/taskito/{ => core}/DependsOnTest.java | 3 ++- .../org/byteveda/taskito/{ => core}/EnqueueDecisionTest.java | 3 ++- .../java/org/byteveda/taskito/{ => core}/ErgonomicsTest.java | 3 ++- .../byteveda/taskito/{ => core}/ExceptionHierarchyTest.java | 3 ++- .../java/org/byteveda/taskito/{ => core}/IdempotencyTest.java | 3 ++- .../org/byteveda/taskito/{ => core}/InterceptionTest.java | 3 ++- .../test/java/org/byteveda/taskito/{ => core}/NotesTest.java | 3 ++- .../java/org/byteveda/taskito/{ => core}/PredicateTest.java | 3 ++- .../test/java/org/byteveda/taskito/{ => core}/QueueTest.java | 4 +++- .../java/org/byteveda/taskito/{ => core}/RecipesTest.java | 2 +- .../org/byteveda/taskito/{ => dashboard}/DashboardTest.java | 3 ++- .../org/byteveda/taskito/{ => internal}/FfmRoundTripTest.java | 3 ++- .../java/org/byteveda/taskito/{ => resources}/ProxyTest.java | 2 +- .../org/byteveda/taskito/{ => resources}/ResourceTest.java | 3 ++- .../taskito/{ => serialization}/EncryptedGreeter.java | 2 +- .../taskito/{ => serialization}/MsgpackSerializerTest.java | 2 +- .../taskito/{ => serialization}/PayloadCodecTest.java | 3 ++- .../taskito/{ => serialization}/PerTaskCodecTest.java | 3 ++- .../java/org/byteveda/taskito/{ => worker}/BatcherTest.java | 3 ++- .../org/byteveda/taskito/{ => worker}/CircuitBreakerTest.java | 3 ++- .../test/java/org/byteveda/taskito/{ => worker}/LockTest.java | 4 +++- .../org/byteveda/taskito/{ => worker}/MeshWorkerTest.java | 3 ++- .../java/org/byteveda/taskito/{ => worker}/PeriodicTest.java | 3 ++- .../org/byteveda/taskito/{ => worker}/RetryPolicyTest.java | 3 ++- .../java/org/byteveda/taskito/{ => worker}/WorkerTest.java | 3 ++- .../java/org/byteveda/taskito/{ => workflows}/CanvasTest.java | 2 +- .../byteveda/taskito/{ => workflows}/WorkflowCacheTest.java | 3 ++- .../taskito/{ => workflows}/WorkflowConditionTest.java | 3 ++- .../byteveda/taskito/{ => workflows}/WorkflowFanOutTest.java | 3 ++- .../byteveda/taskito/{ => workflows}/WorkflowGateTest.java | 3 ++- .../byteveda/taskito/{ => workflows}/WorkflowGraphTest.java | 2 +- .../byteveda/taskito/{ => workflows}/WorkflowSagaTest.java | 3 ++- .../taskito/{ => workflows}/WorkflowSubWorkflowTest.java | 4 +++- .../taskito/{ => workflows}/WorkflowSubmitMapTest.java | 3 ++- .../org/byteveda/taskito/{ => workflows}/WorkflowTest.java | 3 ++- 44 files changed, 80 insertions(+), 44 deletions(-) rename sdks/java/src/test/java/org/byteveda/taskito/{ => autoscale}/ScalerTest.java (98%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => codegen}/AnnotatedPolicies.java (94%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => codegen}/AnnotatedPolicyTest.java (97%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => codegen}/AnnotationProcessorTest.java (97%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => codegen}/Greeter.java (92%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => codegen}/ResourceGreeter.java (90%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => contrib}/TaskitoLoggerTest.java (97%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => contrib}/WebhookTest.java (95%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => core}/DeadLetterByTaskTest.java (96%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => core}/DependsOnTest.java (97%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => core}/EnqueueDecisionTest.java (98%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => core}/ErgonomicsTest.java (96%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => core}/ExceptionHierarchyTest.java (96%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => core}/IdempotencyTest.java (97%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => core}/InterceptionTest.java (98%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => core}/NotesTest.java (97%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => core}/PredicateTest.java (97%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => core}/QueueTest.java (98%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => core}/RecipesTest.java (98%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => dashboard}/DashboardTest.java (98%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => internal}/FfmRoundTripTest.java (96%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => resources}/ProxyTest.java (99%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => resources}/ResourceTest.java (99%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => serialization}/EncryptedGreeter.java (88%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => serialization}/MsgpackSerializerTest.java (95%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => serialization}/PayloadCodecTest.java (98%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => serialization}/PerTaskCodecTest.java (97%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => worker}/BatcherTest.java (97%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => worker}/CircuitBreakerTest.java (98%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => worker}/LockTest.java (93%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => worker}/MeshWorkerTest.java (96%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => worker}/PeriodicTest.java (96%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => worker}/RetryPolicyTest.java (97%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => worker}/WorkerTest.java (97%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => workflows}/CanvasTest.java (97%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => workflows}/WorkflowCacheTest.java (98%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => workflows}/WorkflowConditionTest.java (98%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => workflows}/WorkflowFanOutTest.java (97%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => workflows}/WorkflowGateTest.java (98%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => workflows}/WorkflowGraphTest.java (99%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => workflows}/WorkflowSagaTest.java (98%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => workflows}/WorkflowSubWorkflowTest.java (97%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => workflows}/WorkflowSubmitMapTest.java (96%) rename sdks/java/src/test/java/org/byteveda/taskito/{ => workflows}/WorkflowTest.java (98%) diff --git a/sdks/java/src/test/java/org/byteveda/taskito/ScalerTest.java b/sdks/java/src/test/java/org/byteveda/taskito/autoscale/ScalerTest.java similarity index 98% rename from sdks/java/src/test/java/org/byteveda/taskito/ScalerTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/autoscale/ScalerTest.java index b0046112..7076274c 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/ScalerTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/autoscale/ScalerTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.autoscale; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -10,6 +10,7 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.file.Path; +import org.byteveda.taskito.Taskito; import org.byteveda.taskito.scaler.Scaler; import org.byteveda.taskito.scaler.ScalerOptions; import org.byteveda.taskito.task.EnqueueOptions; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/AnnotatedPolicies.java b/sdks/java/src/test/java/org/byteveda/taskito/codegen/AnnotatedPolicies.java similarity index 94% rename from sdks/java/src/test/java/org/byteveda/taskito/AnnotatedPolicies.java rename to sdks/java/src/test/java/org/byteveda/taskito/codegen/AnnotatedPolicies.java index 5067d37a..ce2c4f0f 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/AnnotatedPolicies.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/codegen/AnnotatedPolicies.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.codegen; import org.byteveda.taskito.annotation.TaskHandler; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/AnnotatedPolicyTest.java b/sdks/java/src/test/java/org/byteveda/taskito/codegen/AnnotatedPolicyTest.java similarity index 97% rename from sdks/java/src/test/java/org/byteveda/taskito/AnnotatedPolicyTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/codegen/AnnotatedPolicyTest.java index bf032c56..63a53a05 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/AnnotatedPolicyTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/codegen/AnnotatedPolicyTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.codegen; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/AnnotationProcessorTest.java b/sdks/java/src/test/java/org/byteveda/taskito/codegen/AnnotationProcessorTest.java similarity index 97% rename from sdks/java/src/test/java/org/byteveda/taskito/AnnotationProcessorTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/codegen/AnnotationProcessorTest.java index 1946a755..5d94adc7 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/AnnotationProcessorTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/codegen/AnnotationProcessorTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.codegen; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -7,6 +7,7 @@ import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import org.byteveda.taskito.Taskito; import org.byteveda.taskito.events.EventName; import org.byteveda.taskito.worker.Worker; import org.junit.jupiter.api.Test; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/Greeter.java b/sdks/java/src/test/java/org/byteveda/taskito/codegen/Greeter.java similarity index 92% rename from sdks/java/src/test/java/org/byteveda/taskito/Greeter.java rename to sdks/java/src/test/java/org/byteveda/taskito/codegen/Greeter.java index 878a229f..9216d9c0 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/Greeter.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/codegen/Greeter.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.codegen; import java.util.List; import org.byteveda.taskito.annotation.TaskHandler; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/ResourceGreeter.java b/sdks/java/src/test/java/org/byteveda/taskito/codegen/ResourceGreeter.java similarity index 90% rename from sdks/java/src/test/java/org/byteveda/taskito/ResourceGreeter.java rename to sdks/java/src/test/java/org/byteveda/taskito/codegen/ResourceGreeter.java index 4105c39d..6debb0c9 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/ResourceGreeter.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/codegen/ResourceGreeter.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.codegen; import org.byteveda.taskito.annotation.Resource; import org.byteveda.taskito.annotation.TaskHandler; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/TaskitoLoggerTest.java b/sdks/java/src/test/java/org/byteveda/taskito/contrib/TaskitoLoggerTest.java similarity index 97% rename from sdks/java/src/test/java/org/byteveda/taskito/TaskitoLoggerTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/contrib/TaskitoLoggerTest.java index 49cc34b2..eff4c58b 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/TaskitoLoggerTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/contrib/TaskitoLoggerTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.contrib; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/WebhookTest.java b/sdks/java/src/test/java/org/byteveda/taskito/contrib/WebhookTest.java similarity index 95% rename from sdks/java/src/test/java/org/byteveda/taskito/WebhookTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/contrib/WebhookTest.java index 6dcc0d5b..5688f4e9 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/WebhookTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/contrib/WebhookTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.contrib; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.nio.file.Path; +import org.byteveda.taskito.Taskito; import org.byteveda.taskito.events.EventName; import org.byteveda.taskito.webhooks.Webhook; import org.byteveda.taskito.webhooks.WebhookManager; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/DeadLetterByTaskTest.java b/sdks/java/src/test/java/org/byteveda/taskito/core/DeadLetterByTaskTest.java similarity index 96% rename from sdks/java/src/test/java/org/byteveda/taskito/DeadLetterByTaskTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/core/DeadLetterByTaskTest.java index dc11318f..78a0e12b 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/DeadLetterByTaskTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/core/DeadLetterByTaskTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.core; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -6,6 +6,7 @@ import java.nio.file.Path; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import org.byteveda.taskito.Taskito; import org.byteveda.taskito.events.EventName; import org.byteveda.taskito.task.Task; import org.byteveda.taskito.worker.Worker; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/DependsOnTest.java b/sdks/java/src/test/java/org/byteveda/taskito/core/DependsOnTest.java similarity index 97% rename from sdks/java/src/test/java/org/byteveda/taskito/DependsOnTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/core/DependsOnTest.java index e20dafe4..d18d6d32 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/DependsOnTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/core/DependsOnTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.core; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -9,6 +9,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import org.byteveda.taskito.Taskito; import org.byteveda.taskito.model.Job; import org.byteveda.taskito.model.JobStatus; import org.byteveda.taskito.task.EnqueueOptions; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/EnqueueDecisionTest.java b/sdks/java/src/test/java/org/byteveda/taskito/core/EnqueueDecisionTest.java similarity index 98% rename from sdks/java/src/test/java/org/byteveda/taskito/EnqueueDecisionTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/core/EnqueueDecisionTest.java index 46a4ca99..d9e15291 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/EnqueueDecisionTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/core/EnqueueDecisionTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.core; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -8,6 +8,7 @@ import java.nio.file.Path; import java.time.Duration; import java.util.Optional; +import org.byteveda.taskito.Taskito; import org.byteveda.taskito.errors.EnqueueSkippedException; import org.byteveda.taskito.errors.PredicateRejectedException; import org.byteveda.taskito.model.Job; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/ErgonomicsTest.java b/sdks/java/src/test/java/org/byteveda/taskito/core/ErgonomicsTest.java similarity index 96% rename from sdks/java/src/test/java/org/byteveda/taskito/ErgonomicsTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/core/ErgonomicsTest.java index e3d5c97e..f9fab2c9 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/ErgonomicsTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/core/ErgonomicsTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.core; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -6,6 +6,7 @@ import java.nio.file.Path; import java.time.Duration; +import org.byteveda.taskito.Taskito; import org.byteveda.taskito.locks.Lock; import org.byteveda.taskito.model.Job; import org.byteveda.taskito.model.JobStatus; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/ExceptionHierarchyTest.java b/sdks/java/src/test/java/org/byteveda/taskito/core/ExceptionHierarchyTest.java similarity index 96% rename from sdks/java/src/test/java/org/byteveda/taskito/ExceptionHierarchyTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/core/ExceptionHierarchyTest.java index d8c1ed79..e8e2716f 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/ExceptionHierarchyTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/core/ExceptionHierarchyTest.java @@ -1,9 +1,10 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.core; 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.TaskitoException; import org.byteveda.taskito.errors.ConfigurationException; import org.byteveda.taskito.errors.CryptoException; import org.byteveda.taskito.errors.LockException; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/IdempotencyTest.java b/sdks/java/src/test/java/org/byteveda/taskito/core/IdempotencyTest.java similarity index 97% rename from sdks/java/src/test/java/org/byteveda/taskito/IdempotencyTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/core/IdempotencyTest.java index bea479e4..6ec7531e 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/IdempotencyTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/core/IdempotencyTest.java @@ -1,10 +1,11 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.core; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import java.nio.charset.StandardCharsets; import java.nio.file.Path; +import org.byteveda.taskito.Taskito; import org.byteveda.taskito.internal.IdempotencyKeys; import org.byteveda.taskito.task.EnqueueOptions; import org.byteveda.taskito.task.Task; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/InterceptionTest.java b/sdks/java/src/test/java/org/byteveda/taskito/core/InterceptionTest.java similarity index 98% rename from sdks/java/src/test/java/org/byteveda/taskito/InterceptionTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/core/InterceptionTest.java index b78999a0..47baf3b1 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/InterceptionTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/core/InterceptionTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.core; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -9,6 +9,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import org.byteveda.taskito.Taskito; import org.byteveda.taskito.errors.InterceptionException; import org.byteveda.taskito.interception.Interception; import org.byteveda.taskito.task.Task; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/NotesTest.java b/sdks/java/src/test/java/org/byteveda/taskito/core/NotesTest.java similarity index 97% rename from sdks/java/src/test/java/org/byteveda/taskito/NotesTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/core/NotesTest.java index 9844094f..b3068365 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/NotesTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/core/NotesTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.core; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; @@ -7,6 +7,7 @@ import java.nio.file.Path; import java.util.LinkedHashMap; import java.util.Map; +import org.byteveda.taskito.Taskito; import org.byteveda.taskito.errors.NotesValidationException; import org.byteveda.taskito.model.Job; import org.byteveda.taskito.serialization.Notes; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/PredicateTest.java b/sdks/java/src/test/java/org/byteveda/taskito/core/PredicateTest.java similarity index 97% rename from sdks/java/src/test/java/org/byteveda/taskito/PredicateTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/core/PredicateTest.java index 3c1b218b..6d219edb 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/PredicateTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/core/PredicateTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.core; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -7,6 +7,7 @@ import java.nio.file.Path; import java.util.List; +import org.byteveda.taskito.Taskito; import org.byteveda.taskito.errors.PredicateRejectedException; import org.byteveda.taskito.middleware.EnqueueContext; import org.byteveda.taskito.middleware.Middleware; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/QueueTest.java b/sdks/java/src/test/java/org/byteveda/taskito/core/QueueTest.java similarity index 98% rename from sdks/java/src/test/java/org/byteveda/taskito/QueueTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/core/QueueTest.java index 07242505..c2e28e2d 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/QueueTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/core/QueueTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.core; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -12,6 +12,8 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import org.byteveda.taskito.Queue; +import org.byteveda.taskito.Taskito; import org.byteveda.taskito.model.Job; import org.byteveda.taskito.model.JobFilter; import org.byteveda.taskito.model.JobStatus; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/RecipesTest.java b/sdks/java/src/test/java/org/byteveda/taskito/core/RecipesTest.java similarity index 98% rename from sdks/java/src/test/java/org/byteveda/taskito/RecipesTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/core/RecipesTest.java index 1d7040f3..b5c278f4 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/RecipesTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/core/RecipesTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.core; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/DashboardTest.java b/sdks/java/src/test/java/org/byteveda/taskito/dashboard/DashboardTest.java similarity index 98% rename from sdks/java/src/test/java/org/byteveda/taskito/DashboardTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/dashboard/DashboardTest.java index 4dd030b1..0407c816 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/DashboardTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/dashboard/DashboardTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.dashboard; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -11,6 +11,7 @@ import java.nio.file.Path; import java.util.Collections; import java.util.Map; +import org.byteveda.taskito.Taskito; import org.byteveda.taskito.dashboard.DashboardServer; import org.byteveda.taskito.task.EnqueueOptions; import org.byteveda.taskito.task.Task; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/FfmRoundTripTest.java b/sdks/java/src/test/java/org/byteveda/taskito/internal/FfmRoundTripTest.java similarity index 96% rename from sdks/java/src/test/java/org/byteveda/taskito/FfmRoundTripTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/internal/FfmRoundTripTest.java index 884c6967..74a16181 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/FfmRoundTripTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/internal/FfmRoundTripTest.java @@ -1,10 +1,11 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.internal; import static org.junit.jupiter.api.Assertions.assertEquals; import java.nio.file.Path; import java.time.Duration; import java.util.List; +import org.byteveda.taskito.Taskito; import org.byteveda.taskito.task.Task; import org.byteveda.taskito.worker.Worker; import org.junit.jupiter.api.Test; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/ProxyTest.java b/sdks/java/src/test/java/org/byteveda/taskito/resources/ProxyTest.java similarity index 99% rename from sdks/java/src/test/java/org/byteveda/taskito/ProxyTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/resources/ProxyTest.java index 44d69669..219ef3a3 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/ProxyTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/resources/ProxyTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.resources; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotSame; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/ResourceTest.java b/sdks/java/src/test/java/org/byteveda/taskito/resources/ResourceTest.java similarity index 99% rename from sdks/java/src/test/java/org/byteveda/taskito/ResourceTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/resources/ResourceTest.java index 9f025e3f..1301ebe6 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/ResourceTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/resources/ResourceTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.resources; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -16,6 +16,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; +import org.byteveda.taskito.Taskito; import org.byteveda.taskito.errors.ResourceException; import org.byteveda.taskito.resources.PoolConfig; import org.byteveda.taskito.resources.ResourcePool; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/EncryptedGreeter.java b/sdks/java/src/test/java/org/byteveda/taskito/serialization/EncryptedGreeter.java similarity index 88% rename from sdks/java/src/test/java/org/byteveda/taskito/EncryptedGreeter.java rename to sdks/java/src/test/java/org/byteveda/taskito/serialization/EncryptedGreeter.java index c97979e9..f058d471 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/EncryptedGreeter.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/serialization/EncryptedGreeter.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.serialization; import org.byteveda.taskito.annotation.Encrypted; import org.byteveda.taskito.annotation.TaskHandler; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/MsgpackSerializerTest.java b/sdks/java/src/test/java/org/byteveda/taskito/serialization/MsgpackSerializerTest.java similarity index 95% rename from sdks/java/src/test/java/org/byteveda/taskito/MsgpackSerializerTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/serialization/MsgpackSerializerTest.java index 63972a8e..e976a4a0 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/MsgpackSerializerTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/serialization/MsgpackSerializerTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.serialization; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/PayloadCodecTest.java b/sdks/java/src/test/java/org/byteveda/taskito/serialization/PayloadCodecTest.java similarity index 98% rename from sdks/java/src/test/java/org/byteveda/taskito/PayloadCodecTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/serialization/PayloadCodecTest.java index c32f861f..01eba3b2 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/PayloadCodecTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/serialization/PayloadCodecTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.serialization; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -12,6 +12,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import org.byteveda.taskito.Taskito; import org.byteveda.taskito.errors.CryptoException; import org.byteveda.taskito.errors.SerializationException; import org.byteveda.taskito.serialization.AesGcmCodec; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/PerTaskCodecTest.java b/sdks/java/src/test/java/org/byteveda/taskito/serialization/PerTaskCodecTest.java similarity index 97% rename from sdks/java/src/test/java/org/byteveda/taskito/PerTaskCodecTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/serialization/PerTaskCodecTest.java index 45fcb1ee..3249a549 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/PerTaskCodecTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/serialization/PerTaskCodecTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.serialization; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -8,6 +8,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import org.byteveda.taskito.Taskito; import org.byteveda.taskito.errors.InterceptionException; import org.byteveda.taskito.events.EventName; import org.byteveda.taskito.interception.Interception; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/BatcherTest.java b/sdks/java/src/test/java/org/byteveda/taskito/worker/BatcherTest.java similarity index 97% rename from sdks/java/src/test/java/org/byteveda/taskito/BatcherTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/worker/BatcherTest.java index 4f77aa5a..15cfcea0 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/BatcherTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/worker/BatcherTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.worker; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -7,6 +7,7 @@ import java.nio.file.Path; import java.time.Duration; import java.util.List; +import org.byteveda.taskito.Taskito; import org.byteveda.taskito.batch.Batcher; import org.byteveda.taskito.task.Task; import org.junit.jupiter.api.Test; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/CircuitBreakerTest.java b/sdks/java/src/test/java/org/byteveda/taskito/worker/CircuitBreakerTest.java similarity index 98% rename from sdks/java/src/test/java/org/byteveda/taskito/CircuitBreakerTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/worker/CircuitBreakerTest.java index 9bed39ee..d0bd3ab4 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/CircuitBreakerTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/worker/CircuitBreakerTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.worker; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -8,6 +8,7 @@ import java.time.Duration; import java.util.List; import java.util.Optional; +import org.byteveda.taskito.Taskito; import org.byteveda.taskito.model.CircuitBreakerState; import org.byteveda.taskito.task.CircuitBreakerConfig; import org.byteveda.taskito.task.Task; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/LockTest.java b/sdks/java/src/test/java/org/byteveda/taskito/worker/LockTest.java similarity index 93% rename from sdks/java/src/test/java/org/byteveda/taskito/LockTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/worker/LockTest.java index dc2d8b9e..45d48b39 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/LockTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/worker/LockTest.java @@ -1,10 +1,12 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.worker; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.nio.file.Path; +import org.byteveda.taskito.TaskitoException; +import org.byteveda.taskito.Taskito; import org.byteveda.taskito.locks.Lock; import org.byteveda.taskito.scheduling.PeriodicTask; import org.junit.jupiter.api.Test; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/MeshWorkerTest.java b/sdks/java/src/test/java/org/byteveda/taskito/worker/MeshWorkerTest.java similarity index 96% rename from sdks/java/src/test/java/org/byteveda/taskito/MeshWorkerTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/worker/MeshWorkerTest.java index 1f62f7c2..2c67fd8d 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/MeshWorkerTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/worker/MeshWorkerTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.worker; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -6,6 +6,7 @@ import java.nio.file.Path; import java.time.Duration; +import org.byteveda.taskito.Taskito; import org.byteveda.taskito.task.Task; import org.byteveda.taskito.worker.MeshClusterInfo; import org.byteveda.taskito.worker.MeshOptions; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/PeriodicTest.java b/sdks/java/src/test/java/org/byteveda/taskito/worker/PeriodicTest.java similarity index 96% rename from sdks/java/src/test/java/org/byteveda/taskito/PeriodicTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/worker/PeriodicTest.java index d3160f17..8f2503d9 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/PeriodicTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/worker/PeriodicTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.worker; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -7,6 +7,7 @@ import java.nio.file.Path; import java.util.List; import java.util.Optional; +import org.byteveda.taskito.Taskito; import org.byteveda.taskito.model.PeriodicInfo; import org.byteveda.taskito.scheduling.PeriodicTask; import org.junit.jupiter.api.Test; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/RetryPolicyTest.java b/sdks/java/src/test/java/org/byteveda/taskito/worker/RetryPolicyTest.java similarity index 97% rename from sdks/java/src/test/java/org/byteveda/taskito/RetryPolicyTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/worker/RetryPolicyTest.java index df0eaf57..1e7ea356 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/RetryPolicyTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/worker/RetryPolicyTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.worker; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -10,6 +10,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import org.byteveda.taskito.Taskito; import org.byteveda.taskito.events.EventName; import org.byteveda.taskito.task.RetryPolicy; import org.byteveda.taskito.task.Task; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/WorkerTest.java b/sdks/java/src/test/java/org/byteveda/taskito/worker/WorkerTest.java similarity index 97% rename from sdks/java/src/test/java/org/byteveda/taskito/WorkerTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/worker/WorkerTest.java index 831d734d..d313230a 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/WorkerTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/worker/WorkerTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.worker; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -10,6 +10,7 @@ import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import org.byteveda.taskito.Taskito; import org.byteveda.taskito.events.EventName; import org.byteveda.taskito.events.OutcomeEvent; import org.byteveda.taskito.middleware.Middleware; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/CanvasTest.java b/sdks/java/src/test/java/org/byteveda/taskito/workflows/CanvasTest.java similarity index 97% rename from sdks/java/src/test/java/org/byteveda/taskito/CanvasTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/workflows/CanvasTest.java index e0943932..4d4d7110 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/CanvasTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/workflows/CanvasTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.workflows; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/WorkflowCacheTest.java b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowCacheTest.java similarity index 98% rename from sdks/java/src/test/java/org/byteveda/taskito/WorkflowCacheTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowCacheTest.java index 49a42ca5..4f2afeb3 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/WorkflowCacheTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowCacheTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.workflows; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -6,6 +6,7 @@ import java.nio.file.Path; import java.time.Duration; import java.util.concurrent.atomic.AtomicInteger; +import org.byteveda.taskito.Taskito; import org.byteveda.taskito.errors.WorkflowException; import org.byteveda.taskito.task.Task; import org.byteveda.taskito.worker.Worker; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/WorkflowConditionTest.java b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowConditionTest.java similarity index 98% rename from sdks/java/src/test/java/org/byteveda/taskito/WorkflowConditionTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowConditionTest.java index c08ff11e..e94a5eda 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/WorkflowConditionTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowConditionTest.java @@ -1,10 +1,11 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.workflows; import static org.junit.jupiter.api.Assertions.assertEquals; import java.nio.file.Path; import java.time.Duration; import java.util.concurrent.atomic.AtomicBoolean; +import org.byteveda.taskito.Taskito; import org.byteveda.taskito.task.Task; import org.byteveda.taskito.worker.Worker; import org.byteveda.taskito.workflows.NodeStatus; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/WorkflowFanOutTest.java b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowFanOutTest.java similarity index 97% rename from sdks/java/src/test/java/org/byteveda/taskito/WorkflowFanOutTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowFanOutTest.java index 0667de51..88d3510f 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/WorkflowFanOutTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowFanOutTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.workflows; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -7,6 +7,7 @@ import java.time.Duration; import java.util.ArrayList; import java.util.List; +import org.byteveda.taskito.Taskito; import org.byteveda.taskito.task.Task; import org.byteveda.taskito.worker.Worker; import org.byteveda.taskito.workflows.NodeStatus; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/WorkflowGateTest.java b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowGateTest.java similarity index 98% rename from sdks/java/src/test/java/org/byteveda/taskito/WorkflowGateTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowGateTest.java index 686074a3..785a8c2b 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/WorkflowGateTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowGateTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.workflows; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; @@ -7,6 +7,7 @@ import java.time.Duration; import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; +import org.byteveda.taskito.Taskito; import org.byteveda.taskito.task.Task; import org.byteveda.taskito.worker.Worker; import org.byteveda.taskito.workflows.GateAction; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/WorkflowGraphTest.java b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowGraphTest.java similarity index 99% rename from sdks/java/src/test/java/org/byteveda/taskito/WorkflowGraphTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowGraphTest.java index 03cfa5f0..0fc67547 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/WorkflowGraphTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowGraphTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.workflows; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/WorkflowSagaTest.java b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowSagaTest.java similarity index 98% rename from sdks/java/src/test/java/org/byteveda/taskito/WorkflowSagaTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowSagaTest.java index 26d61bb8..f25e3440 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/WorkflowSagaTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowSagaTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.workflows; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -7,6 +7,7 @@ import java.util.List; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; +import org.byteveda.taskito.Taskito; import org.byteveda.taskito.task.Task; import org.byteveda.taskito.worker.Worker; import org.byteveda.taskito.workflows.NodeStatus; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/WorkflowSubWorkflowTest.java b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowSubWorkflowTest.java similarity index 97% rename from sdks/java/src/test/java/org/byteveda/taskito/WorkflowSubWorkflowTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowSubWorkflowTest.java index b54261f6..30c117cc 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/WorkflowSubWorkflowTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowSubWorkflowTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.workflows; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -6,6 +6,8 @@ import java.nio.file.Path; import java.time.Duration; import java.util.concurrent.atomic.AtomicInteger; +import org.byteveda.taskito.TaskitoException; +import org.byteveda.taskito.Taskito; import org.byteveda.taskito.task.Task; import org.byteveda.taskito.worker.Worker; import org.byteveda.taskito.workflows.NodeStatus; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/WorkflowSubmitMapTest.java b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowSubmitMapTest.java similarity index 96% rename from sdks/java/src/test/java/org/byteveda/taskito/WorkflowSubmitMapTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowSubmitMapTest.java index 6790ebcb..851083b7 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/WorkflowSubmitMapTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowSubmitMapTest.java @@ -1,10 +1,11 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.workflows; import static org.junit.jupiter.api.Assertions.assertEquals; import java.nio.file.Path; import java.time.Duration; import java.util.Map; +import org.byteveda.taskito.Taskito; import org.byteveda.taskito.task.Task; import org.byteveda.taskito.worker.Worker; import org.byteveda.taskito.workflows.Workflow; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/WorkflowTest.java b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowTest.java similarity index 98% rename from sdks/java/src/test/java/org/byteveda/taskito/WorkflowTest.java rename to sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowTest.java index 0b276a2e..b46acb1e 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/WorkflowTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowTest.java @@ -1,4 +1,4 @@ -package org.byteveda.taskito; +package org.byteveda.taskito.workflows; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -6,6 +6,7 @@ import java.nio.file.Path; import java.time.Duration; import java.util.concurrent.atomic.AtomicInteger; +import org.byteveda.taskito.Taskito; import org.byteveda.taskito.task.Task; import org.byteveda.taskito.worker.Worker; import org.byteveda.taskito.workflows.NodeStatus; From cae063dffbba6fe154a4b2df2ba4bb154e910d49 Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Tue, 7 Jul 2026 11:30:44 +0530 Subject: [PATCH 6/8] refactor(java): remove unused imports from test files --- .../org/byteveda/taskito/dashboard/DashboardTest.java | 1 - .../java/org/byteveda/taskito/resources/ResourceTest.java | 5 ----- .../taskito/serialization/MsgpackSerializerTest.java | 2 -- .../byteveda/taskito/serialization/PayloadCodecTest.java | 5 ----- .../byteveda/taskito/serialization/PerTaskCodecTest.java | 1 - .../org/byteveda/taskito/worker/CircuitBreakerTest.java | 1 - .../java/org/byteveda/taskito/worker/MeshWorkerTest.java | 3 --- .../java/org/byteveda/taskito/worker/RetryPolicyTest.java | 1 - .../test/java/org/byteveda/taskito/worker/WorkerTest.java | 1 - .../java/org/byteveda/taskito/workflows/CanvasTest.java | 3 --- .../org/byteveda/taskito/workflows/WorkflowCacheTest.java | 7 ------- .../byteveda/taskito/workflows/WorkflowConditionTest.java | 6 ------ .../byteveda/taskito/workflows/WorkflowFanOutTest.java | 5 ----- .../org/byteveda/taskito/workflows/WorkflowGateTest.java | 8 -------- .../org/byteveda/taskito/workflows/WorkflowGraphTest.java | 4 ---- .../org/byteveda/taskito/workflows/WorkflowSagaTest.java | 6 ------ .../taskito/workflows/WorkflowSubWorkflowTest.java | 6 ------ .../byteveda/taskito/workflows/WorkflowSubmitMapTest.java | 4 ---- .../java/org/byteveda/taskito/workflows/WorkflowTest.java | 6 ------ 19 files changed, 75 deletions(-) diff --git a/sdks/java/src/test/java/org/byteveda/taskito/dashboard/DashboardTest.java b/sdks/java/src/test/java/org/byteveda/taskito/dashboard/DashboardTest.java index 0407c816..bf61ff83 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/dashboard/DashboardTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/dashboard/DashboardTest.java @@ -12,7 +12,6 @@ import java.util.Collections; import java.util.Map; import org.byteveda.taskito.Taskito; -import org.byteveda.taskito.dashboard.DashboardServer; import org.byteveda.taskito.task.EnqueueOptions; import org.byteveda.taskito.task.Task; import org.junit.jupiter.api.Test; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/resources/ResourceTest.java b/sdks/java/src/test/java/org/byteveda/taskito/resources/ResourceTest.java index 1301ebe6..f4740531 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/resources/ResourceTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/resources/ResourceTest.java @@ -18,11 +18,6 @@ import java.util.function.Supplier; import org.byteveda.taskito.Taskito; import org.byteveda.taskito.errors.ResourceException; -import org.byteveda.taskito.resources.PoolConfig; -import org.byteveda.taskito.resources.ResourcePool; -import org.byteveda.taskito.resources.ResourceScope; -import org.byteveda.taskito.resources.ResourceStat; -import org.byteveda.taskito.resources.Resources; import org.byteveda.taskito.task.Task; import org.byteveda.taskito.task.TaskFunction; import org.byteveda.taskito.worker.Worker; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/serialization/MsgpackSerializerTest.java b/sdks/java/src/test/java/org/byteveda/taskito/serialization/MsgpackSerializerTest.java index e976a4a0..4387e6ae 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/serialization/MsgpackSerializerTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/serialization/MsgpackSerializerTest.java @@ -4,8 +4,6 @@ import java.util.LinkedHashMap; import java.util.Map; -import org.byteveda.taskito.serialization.MsgpackSerializer; -import org.byteveda.taskito.serialization.Serializer; import org.junit.jupiter.api.Test; class MsgpackSerializerTest { diff --git a/sdks/java/src/test/java/org/byteveda/taskito/serialization/PayloadCodecTest.java b/sdks/java/src/test/java/org/byteveda/taskito/serialization/PayloadCodecTest.java index 01eba3b2..8acf3e62 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/serialization/PayloadCodecTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/serialization/PayloadCodecTest.java @@ -15,11 +15,6 @@ import org.byteveda.taskito.Taskito; import org.byteveda.taskito.errors.CryptoException; import org.byteveda.taskito.errors.SerializationException; -import org.byteveda.taskito.serialization.AesGcmCodec; -import org.byteveda.taskito.serialization.CodecSerializer; -import org.byteveda.taskito.serialization.GzipCodec; -import org.byteveda.taskito.serialization.HmacCodec; -import org.byteveda.taskito.serialization.JsonSerializer; import org.byteveda.taskito.task.Task; import org.byteveda.taskito.worker.Worker; import org.junit.jupiter.api.Test; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/serialization/PerTaskCodecTest.java b/sdks/java/src/test/java/org/byteveda/taskito/serialization/PerTaskCodecTest.java index 3249a549..ad483671 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/serialization/PerTaskCodecTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/serialization/PerTaskCodecTest.java @@ -12,7 +12,6 @@ import org.byteveda.taskito.errors.InterceptionException; import org.byteveda.taskito.events.EventName; import org.byteveda.taskito.interception.Interception; -import org.byteveda.taskito.serialization.AesGcmCodec; import org.byteveda.taskito.task.Task; import org.byteveda.taskito.worker.Worker; import org.junit.jupiter.api.Test; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/worker/CircuitBreakerTest.java b/sdks/java/src/test/java/org/byteveda/taskito/worker/CircuitBreakerTest.java index d0bd3ab4..4b3ba50a 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/worker/CircuitBreakerTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/worker/CircuitBreakerTest.java @@ -12,7 +12,6 @@ import org.byteveda.taskito.model.CircuitBreakerState; import org.byteveda.taskito.task.CircuitBreakerConfig; import org.byteveda.taskito.task.Task; -import org.byteveda.taskito.worker.Worker; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.io.TempDir; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/worker/MeshWorkerTest.java b/sdks/java/src/test/java/org/byteveda/taskito/worker/MeshWorkerTest.java index 2c67fd8d..72ba58f7 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/worker/MeshWorkerTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/worker/MeshWorkerTest.java @@ -8,9 +8,6 @@ import java.time.Duration; import org.byteveda.taskito.Taskito; import org.byteveda.taskito.task.Task; -import org.byteveda.taskito.worker.MeshClusterInfo; -import org.byteveda.taskito.worker.MeshOptions; -import org.byteveda.taskito.worker.Worker; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.io.TempDir; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/worker/RetryPolicyTest.java b/sdks/java/src/test/java/org/byteveda/taskito/worker/RetryPolicyTest.java index 1e7ea356..6cd2d860 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/worker/RetryPolicyTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/worker/RetryPolicyTest.java @@ -14,7 +14,6 @@ import org.byteveda.taskito.events.EventName; import org.byteveda.taskito.task.RetryPolicy; import org.byteveda.taskito.task.Task; -import org.byteveda.taskito.worker.Worker; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.io.TempDir; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/worker/WorkerTest.java b/sdks/java/src/test/java/org/byteveda/taskito/worker/WorkerTest.java index d313230a..58dee866 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/worker/WorkerTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/worker/WorkerTest.java @@ -15,7 +15,6 @@ import org.byteveda.taskito.events.OutcomeEvent; import org.byteveda.taskito.middleware.Middleware; import org.byteveda.taskito.task.Task; -import org.byteveda.taskito.worker.Worker; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.io.TempDir; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/workflows/CanvasTest.java b/sdks/java/src/test/java/org/byteveda/taskito/workflows/CanvasTest.java index 4d4d7110..2aa4f8b5 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/workflows/CanvasTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/workflows/CanvasTest.java @@ -6,9 +6,6 @@ import java.util.List; import java.util.Set; import org.byteveda.taskito.task.Task; -import org.byteveda.taskito.workflows.Canvas; -import org.byteveda.taskito.workflows.Workflow; -import org.byteveda.taskito.workflows.WorkflowAnalysis; import org.junit.jupiter.api.Test; class CanvasTest { diff --git a/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowCacheTest.java b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowCacheTest.java index 4f2afeb3..514a22a0 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowCacheTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowCacheTest.java @@ -10,13 +10,6 @@ import org.byteveda.taskito.errors.WorkflowException; import org.byteveda.taskito.task.Task; import org.byteveda.taskito.worker.Worker; -import org.byteveda.taskito.workflows.FanMode; -import org.byteveda.taskito.workflows.NodeStatus; -import org.byteveda.taskito.workflows.Step; -import org.byteveda.taskito.workflows.Workflow; -import org.byteveda.taskito.workflows.WorkflowRun; -import org.byteveda.taskito.workflows.WorkflowState; -import org.byteveda.taskito.workflows.WorkflowStatus; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.io.TempDir; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowConditionTest.java b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowConditionTest.java index e94a5eda..7365085e 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowConditionTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowConditionTest.java @@ -8,12 +8,6 @@ import org.byteveda.taskito.Taskito; import org.byteveda.taskito.task.Task; import org.byteveda.taskito.worker.Worker; -import org.byteveda.taskito.workflows.NodeStatus; -import org.byteveda.taskito.workflows.Step; -import org.byteveda.taskito.workflows.Workflow; -import org.byteveda.taskito.workflows.WorkflowRun; -import org.byteveda.taskito.workflows.WorkflowState; -import org.byteveda.taskito.workflows.WorkflowStatus; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.io.TempDir; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowFanOutTest.java b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowFanOutTest.java index 88d3510f..262cb524 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowFanOutTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowFanOutTest.java @@ -10,11 +10,6 @@ import org.byteveda.taskito.Taskito; import org.byteveda.taskito.task.Task; import org.byteveda.taskito.worker.Worker; -import org.byteveda.taskito.workflows.NodeStatus; -import org.byteveda.taskito.workflows.Workflow; -import org.byteveda.taskito.workflows.WorkflowRun; -import org.byteveda.taskito.workflows.WorkflowState; -import org.byteveda.taskito.workflows.WorkflowStatus; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.io.TempDir; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowGateTest.java b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowGateTest.java index 785a8c2b..c0e1b60e 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowGateTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowGateTest.java @@ -10,14 +10,6 @@ import org.byteveda.taskito.Taskito; import org.byteveda.taskito.task.Task; import org.byteveda.taskito.worker.Worker; -import org.byteveda.taskito.workflows.GateAction; -import org.byteveda.taskito.workflows.GateConfig; -import org.byteveda.taskito.workflows.NodeSnapshot; -import org.byteveda.taskito.workflows.NodeStatus; -import org.byteveda.taskito.workflows.Workflow; -import org.byteveda.taskito.workflows.WorkflowRun; -import org.byteveda.taskito.workflows.WorkflowState; -import org.byteveda.taskito.workflows.WorkflowStatus; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.io.TempDir; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowGraphTest.java b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowGraphTest.java index 0fc67547..4ce69940 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowGraphTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowGraphTest.java @@ -8,10 +8,6 @@ import java.util.Set; import org.byteveda.taskito.errors.WorkflowException; import org.byteveda.taskito.task.Task; -import org.byteveda.taskito.workflows.GateConfig; -import org.byteveda.taskito.workflows.Workflow; -import org.byteveda.taskito.workflows.WorkflowAnalysis; -import org.byteveda.taskito.workflows.WorkflowVisualization; import org.junit.jupiter.api.Test; class WorkflowGraphTest { diff --git a/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowSagaTest.java b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowSagaTest.java index f25e3440..09450735 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowSagaTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowSagaTest.java @@ -10,12 +10,6 @@ import org.byteveda.taskito.Taskito; import org.byteveda.taskito.task.Task; import org.byteveda.taskito.worker.Worker; -import org.byteveda.taskito.workflows.NodeStatus; -import org.byteveda.taskito.workflows.Step; -import org.byteveda.taskito.workflows.Workflow; -import org.byteveda.taskito.workflows.WorkflowRun; -import org.byteveda.taskito.workflows.WorkflowState; -import org.byteveda.taskito.workflows.WorkflowStatus; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.io.TempDir; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowSubWorkflowTest.java b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowSubWorkflowTest.java index 30c117cc..f728966e 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowSubWorkflowTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowSubWorkflowTest.java @@ -10,12 +10,6 @@ import org.byteveda.taskito.Taskito; import org.byteveda.taskito.task.Task; import org.byteveda.taskito.worker.Worker; -import org.byteveda.taskito.workflows.NodeStatus; -import org.byteveda.taskito.workflows.Step; -import org.byteveda.taskito.workflows.Workflow; -import org.byteveda.taskito.workflows.WorkflowRun; -import org.byteveda.taskito.workflows.WorkflowState; -import org.byteveda.taskito.workflows.WorkflowStatus; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.io.TempDir; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowSubmitMapTest.java b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowSubmitMapTest.java index 851083b7..20c54e5e 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowSubmitMapTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowSubmitMapTest.java @@ -8,10 +8,6 @@ import org.byteveda.taskito.Taskito; import org.byteveda.taskito.task.Task; import org.byteveda.taskito.worker.Worker; -import org.byteveda.taskito.workflows.Workflow; -import org.byteveda.taskito.workflows.WorkflowRun; -import org.byteveda.taskito.workflows.WorkflowState; -import org.byteveda.taskito.workflows.WorkflowStatus; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.io.TempDir; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowTest.java b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowTest.java index b46acb1e..ef2e9839 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowTest.java @@ -9,12 +9,6 @@ import org.byteveda.taskito.Taskito; import org.byteveda.taskito.task.Task; import org.byteveda.taskito.worker.Worker; -import org.byteveda.taskito.workflows.NodeStatus; -import org.byteveda.taskito.workflows.Step; -import org.byteveda.taskito.workflows.Workflow; -import org.byteveda.taskito.workflows.WorkflowRun; -import org.byteveda.taskito.workflows.WorkflowState; -import org.byteveda.taskito.workflows.WorkflowStatus; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.io.TempDir; From 427c0f8736c9e56901badc677bfeacf5addd9b3b Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Tue, 7 Jul 2026 11:34:20 +0530 Subject: [PATCH 7/8] fix(java): honor idempotent auto-derive in enqueueMany Batch enqueue skipped resolveUniqueKey, so idempotent(true) lost dedup semantics in bulk. --- .../org/byteveda/taskito/DefaultTaskito.java | 14 ++++++++++++-- .../byteveda/taskito/core/IdempotencyTest.java | 17 +++++++++++++++++ 2 files changed, 29 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 06c91af0..05f77a1e 100644 --- a/sdks/java/src/main/java/org/byteveda/taskito/DefaultTaskito.java +++ b/sdks/java/src/main/java/org/byteveda/taskito/DefaultTaskito.java @@ -352,8 +352,18 @@ public List enqueueMany(Task task, List payloads, EnqueueOptio throw new EnqueueSkippedException("enqueue of '" + task.name() + "' was skipped or deferred by a gate; batch enqueue needs Allow"); } - bytes[i] = encodeCodecs(serializer.serialize(payload), task.codecNames()); - perJob.add(options); + // Resolve the dedup key per payload so bulk enqueue honors idempotent + // auto-derive, mirroring the single-enqueue path (explicit + // uniqueKey/idempotencyKey precedence preserved). Key hashes the + // deterministic pre-codec bytes. + byte[] payloadBytes = serializer.serialize(payload); + EnqueueOptions jobOptions = options; + String uniqueKey = resolveUniqueKey(task.name(), payloadBytes, jobOptions, task.idempotent()); + if (uniqueKey != null && !uniqueKey.equals(jobOptions.uniqueKey())) { + jobOptions = jobOptions.toBuilder().uniqueKey(uniqueKey).build(); + } + bytes[i] = encodeCodecs(payloadBytes, task.codecNames()); + perJob.add(jobOptions); } return Arrays.asList(backend.enqueueMany(task.name(), bytes, encode(perJob))); } diff --git a/sdks/java/src/test/java/org/byteveda/taskito/core/IdempotencyTest.java b/sdks/java/src/test/java/org/byteveda/taskito/core/IdempotencyTest.java index 6ec7531e..25981a9b 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/core/IdempotencyTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/core/IdempotencyTest.java @@ -5,6 +5,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Path; +import java.util.List; import org.byteveda.taskito.Taskito; import org.byteveda.taskito.internal.IdempotencyKeys; import org.byteveda.taskito.task.EnqueueOptions; @@ -64,6 +65,22 @@ void explicitIdempotencyKeyWinsOverPayload(@TempDir Path dir) throws Exception { } } + @Test + @Timeout(30) + void enqueueManyHonorsIdempotentAutoDerive(@TempDir Path dir) throws Exception { + try (Taskito queue = + Taskito.builder().url(dir.resolve("idembatch.db").toString()).open()) { + Task task = Task.of("idem.batch", String.class).idempotent(true); + // No worker: jobs stay pending. The batch-enqueued payload must carry the + // auto-derived key so a later single enqueue of the same payload dedups — + // regressing when enqueueMany skipped resolveUniqueKey. + List ids = queue.enqueueMany(task, List.of("hello", "world")); + String again = queue.enqueue(task, "hello"); + assertEquals(ids.get(0), again, "batch enqueue must auto-derive the dedup key"); + assertNotEquals(ids.get(1), again, "a different payload derives a different key"); + } + } + @Test @Timeout(30) void perCallOptOutDisablesTaskDefault(@TempDir Path dir) throws Exception { From d3895879e0601d71c24c6baea02fb4d7f2b55690 Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Tue, 7 Jul 2026 11:49:19 +0530 Subject: [PATCH 8/8] style(java): apply spotless formatting --- .../byteveda/taskito/serialization/Notes.java | 5 +++-- .../org/byteveda/taskito/worker/Worker.java | 3 ++- .../byteveda/taskito/core/DependsOnTest.java | 3 ++- .../org/byteveda/taskito/core/NotesTest.java | 6 ++++-- .../taskito/worker/CircuitBreakerTest.java | 18 ++++++++---------- .../org/byteveda/taskito/worker/LockTest.java | 2 +- .../workflows/WorkflowSubWorkflowTest.java | 2 +- 7 files changed, 21 insertions(+), 18 deletions(-) diff --git a/sdks/java/src/main/java/org/byteveda/taskito/serialization/Notes.java b/sdks/java/src/main/java/org/byteveda/taskito/serialization/Notes.java index 39eedce6..a4f6342d 100644 --- a/sdks/java/src/main/java/org/byteveda/taskito/serialization/Notes.java +++ b/sdks/java/src/main/java/org/byteveda/taskito/serialization/Notes.java @@ -31,8 +31,9 @@ public final class Notes { public static final int MAX_BYTES = 4096; // Canonical form: keys sorted, compact separators (Jackson's default), non-ASCII emitted raw. - private static final ObjectWriter CANONICAL = - new ObjectMapper().configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true).writer(); + private static final ObjectWriter CANONICAL = new ObjectMapper() + .configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true) + .writer(); private Notes() {} 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 6509011a..3c932e15 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 @@ -445,7 +445,8 @@ private List> encodeTaskConfigs() { config.put("maxDelayMs", policy.maxDelay().toMillis()); } if (!policy.customDelays().isEmpty()) { - List delaysMs = new ArrayList<>(policy.customDelays().size()); + List delaysMs = + new ArrayList<>(policy.customDelays().size()); policy.customDelays().forEach(delay -> delaysMs.add(delay.toMillis())); config.put("customDelaysMs", delaysMs); } diff --git a/sdks/java/src/test/java/org/byteveda/taskito/core/DependsOnTest.java b/sdks/java/src/test/java/org/byteveda/taskito/core/DependsOnTest.java index d18d6d32..85fd07ff 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/core/DependsOnTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/core/DependsOnTest.java @@ -62,7 +62,8 @@ void cascadeCancelsDependentWhenDependencyFails(@TempDir Path dir) throws Except AtomicBoolean bRan = new AtomicBoolean(false); String idA = queue.enqueue(a, "boom"); - String idB = queue.enqueue(b, "b", EnqueueOptions.builder().dependsOn(idA).build()); + String idB = queue.enqueue( + b, "b", EnqueueOptions.builder().dependsOn(idA).build()); try (Worker worker = queue.worker() .handle(a, p -> { diff --git a/sdks/java/src/test/java/org/byteveda/taskito/core/NotesTest.java b/sdks/java/src/test/java/org/byteveda/taskito/core/NotesTest.java index b3068365..cad0704e 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/core/NotesTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/core/NotesTest.java @@ -59,7 +59,8 @@ void rejectsExcessiveNesting() { // depth 3 leaf is allowed; depth 4 is not. Notes.encode(Map.of("a", Map.of("b", Map.of("c", 1)))); assertThrows( - NotesValidationException.class, () -> Notes.encode(Map.of("a", Map.of("b", Map.of("c", Map.of("d", 1)))))); + NotesValidationException.class, + () -> Notes.encode(Map.of("a", Map.of("b", Map.of("c", Map.of("d", 1)))))); } @Test @@ -92,7 +93,8 @@ void notesRoundTripThroughStorage(@TempDir Path dir) throws Exception { Map notes = new LinkedHashMap<>(); notes.put("env", "prod"); notes.put("attempt", 3); - String id = queue.enqueue(task, "hi", EnqueueOptions.builder().notes(notes).build()); + String id = queue.enqueue( + task, "hi", EnqueueOptions.builder().notes(notes).build()); Job job = queue.getJob(id).orElseThrow(); assertEquals(notes, job.notesMap().orElseThrow()); diff --git a/sdks/java/src/test/java/org/byteveda/taskito/worker/CircuitBreakerTest.java b/sdks/java/src/test/java/org/byteveda/taskito/worker/CircuitBreakerTest.java index 4b3ba50a..04e1968e 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/worker/CircuitBreakerTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/worker/CircuitBreakerTest.java @@ -21,13 +21,12 @@ class CircuitBreakerTest { @Test void builderValidatesInputs() { assertThrows(IllegalArgumentException.class, () -> CircuitBreakerConfig.of(0)); - assertThrows(IllegalArgumentException.class, () -> CircuitBreakerConfig.builder(1).halfOpenProbes(0)); - assertThrows( - IllegalArgumentException.class, - () -> CircuitBreakerConfig.builder(1).halfOpenSuccessRate(1.5)); - assertThrows( - IllegalArgumentException.class, - () -> CircuitBreakerConfig.builder(1).window(Duration.ZERO)); + assertThrows(IllegalArgumentException.class, () -> CircuitBreakerConfig.builder(1) + .halfOpenProbes(0)); + assertThrows(IllegalArgumentException.class, () -> CircuitBreakerConfig.builder(1) + .halfOpenSuccessRate(1.5)); + assertThrows(IllegalArgumentException.class, () -> CircuitBreakerConfig.builder(1) + .window(Duration.ZERO)); } @Test @@ -45,9 +44,8 @@ void builderAppliesDefaults() { void breakerOpensAfterThresholdFailures(@TempDir Path dir) throws Exception { try (Taskito queue = Taskito.builder().url(dir.resolve("cb.db").toString()).open()) { - Task failing = Task.of("cb.fail", String.class) - .maxRetries(0) - .circuitBreaker(CircuitBreakerConfig.of(2)); + Task failing = + Task.of("cb.fail", String.class).maxRetries(0).circuitBreaker(CircuitBreakerConfig.of(2)); // Enqueue more jobs than the threshold; once two fail, the breaker opens and gates the rest. for (int i = 0; i < 5; i++) { diff --git a/sdks/java/src/test/java/org/byteveda/taskito/worker/LockTest.java b/sdks/java/src/test/java/org/byteveda/taskito/worker/LockTest.java index 45d48b39..e1763042 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/worker/LockTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/worker/LockTest.java @@ -5,8 +5,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.nio.file.Path; -import org.byteveda.taskito.TaskitoException; import org.byteveda.taskito.Taskito; +import org.byteveda.taskito.TaskitoException; import org.byteveda.taskito.locks.Lock; import org.byteveda.taskito.scheduling.PeriodicTask; import org.junit.jupiter.api.Test; diff --git a/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowSubWorkflowTest.java b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowSubWorkflowTest.java index f728966e..ca3b5b3e 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowSubWorkflowTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/workflows/WorkflowSubWorkflowTest.java @@ -6,8 +6,8 @@ import java.nio.file.Path; import java.time.Duration; import java.util.concurrent.atomic.AtomicInteger; -import org.byteveda.taskito.TaskitoException; import org.byteveda.taskito.Taskito; +import org.byteveda.taskito.TaskitoException; import org.byteveda.taskito.task.Task; import org.byteveda.taskito.worker.Worker; import org.junit.jupiter.api.Test;