From f244631ecbb271d68fa056e8646d487c7a706e9f Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Wed, 1 Jul 2026 23:52:53 +0530 Subject: [PATCH 1/3] feat(java): worker resource injection for handler methods --- .../processor/TaskHandlerProcessor.java | 82 +++++++++++++------ .../byteveda/taskito/annotation/Resource.java | 25 ++++++ 2 files changed, 81 insertions(+), 26 deletions(-) create mode 100644 sdks/java/src/main/java/org/byteveda/taskito/annotation/Resource.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 e6eae07a..2cf185ea 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 @@ -18,6 +18,7 @@ import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; import javax.tools.Diagnostic; import javax.tools.JavaFileObject; @@ -31,6 +32,7 @@ @SupportedSourceVersion(SourceVersion.RELEASE_11) public final class TaskHandlerProcessor extends AbstractProcessor { static final String ANNOTATION = "org.byteveda.taskito.annotation.TaskHandler"; + static final String RESOURCE = "org.byteveda.taskito.annotation.Resource"; @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { @@ -56,10 +58,17 @@ public boolean process(Set annotations, RoundEnvironment } private boolean validate(ExecutableElement method) { - if (method.getParameters().size() != 1) { - error(method, "@TaskHandler method must take exactly one parameter (the payload)"); + List params = method.getParameters(); + if (params.isEmpty()) { + error(method, "@TaskHandler method must take the payload as its first parameter"); return false; } + for (int i = 1; i < params.size(); i++) { + if (resourceName(params.get(i)) == null) { + error(method, "@TaskHandler parameters after the payload must be annotated @Resource"); + return false; + } + } if (method.getModifiers().contains(Modifier.PRIVATE)) { error(method, "@TaskHandler method must not be private"); return false; @@ -76,6 +85,7 @@ private void generate(TypeElement owner, List methods) { String ownerSimple = owner.getSimpleName().toString(); String companion = ownerSimple + "Tasks"; String qualified = pkg.isEmpty() ? companion : pkg + "." + companion; + boolean anyResources = methods.stream().anyMatch(m -> m.getParameters().size() > 1); StringBuilder out = new StringBuilder(); if (!pkg.isEmpty()) { @@ -85,7 +95,9 @@ private void generate(TypeElement owner, List methods) { .append("import org.byteveda.taskito.task.Task;\n") .append("import org.byteveda.taskito.worker.Handler;\n") .append("import org.byteveda.taskito.worker.HandlerRegistry;\n") - .append("import org.byteveda.taskito.worker.Worker;\n\n") + .append("import org.byteveda.taskito.worker.Worker;\n") + .append(anyResources ? "import org.byteveda.taskito.resources.Resources;\n" : "") + .append("\n") .append("/** Generated task descriptors + worker binding for {@link ") .append(ownerSimple) .append("}. */\n") @@ -118,20 +130,11 @@ private void generate(TypeElement owner, List methods) { .append(" impl) {\n"); for (ExecutableElement method : methods) { String constant = upperSnake(method.getSimpleName().toString()); - String methodName = method.getSimpleName().toString(); - if (isVoid(method)) { - out.append(" builder.handle(") - .append(constant) - .append(", payload -> {\n impl.") - .append(methodName) - .append("(payload);\n return null;\n });\n"); - } else { - out.append(" builder.handle(") - .append(constant) - .append(", impl::") - .append(methodName) - .append(");\n"); - } + out.append(" builder.handle(") + .append(constant) + .append(", ") + .append(handlerExpr(method)) + .append(");\n"); } out.append(" return builder;\n }\n\n"); @@ -142,15 +145,11 @@ private void generate(TypeElement owner, List methods) { for (int i = 0; i < methods.size(); i++) { ExecutableElement method = methods.get(i); String constant = upperSnake(method.getSimpleName().toString()); - String methodName = method.getSimpleName().toString(); - out.append(" Handler.of(").append(constant).append(", "); - if (isVoid(method)) { - out.append("payload -> {\n impl.") - .append(methodName) - .append("(payload);\n return null;\n })"); - } else { - out.append("impl::").append(methodName).append(")"); - } + out.append(" Handler.of(") + .append(constant) + .append(", ") + .append(handlerExpr(method)) + .append(")"); out.append(i + 1 < methods.size() ? ",\n" : ");\n"); } out.append(" }\n}\n"); @@ -181,6 +180,37 @@ private String options(ExecutableElement method) { return chain.toString(); } + /** + * The {@code TaskFunction} expression for a handler: a method reference when it + * takes only the payload, otherwise a lambda that resolves each {@code @Resource} + * parameter from the worker resource runtime. + */ + private String handlerExpr(ExecutableElement method) { + String methodName = method.getSimpleName().toString(); + List params = method.getParameters(); + StringBuilder args = new StringBuilder("payload"); + for (int i = 1; i < params.size(); i++) { + args.append(", Resources.use(\"") + .append(escape(resourceName(params.get(i)))) + .append("\")"); + } + if (isVoid(method)) { + return "payload -> { impl." + methodName + "(" + args + "); return null; }"; + } + return params.size() > 1 ? "payload -> impl." + methodName + "(" + args + ")" : "impl::" + methodName; + } + + /** The {@code @Resource} value on a parameter, or null if it is not annotated. */ + private String resourceName(VariableElement param) { + for (AnnotationMirror annotation : param.getAnnotationMirrors()) { + if (annotation.getAnnotationType().toString().equals(RESOURCE)) { + Object value = rawValue(annotation, "value"); + return value == null ? "" : value.toString(); + } + } + return null; + } + private String taskName(ExecutableElement method) { String value = stringValue(mirror(method), "value", ""); return value.isEmpty() ? method.getSimpleName().toString() : value; diff --git a/sdks/java/src/main/java/org/byteveda/taskito/annotation/Resource.java b/sdks/java/src/main/java/org/byteveda/taskito/annotation/Resource.java new file mode 100644 index 00000000..d523f0b8 --- /dev/null +++ b/sdks/java/src/main/java/org/byteveda/taskito/annotation/Resource.java @@ -0,0 +1,25 @@ +package org.byteveda.taskito.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Injects a worker resource into a {@code @TaskHandler} method. Place it on + * parameters after the payload; the generated companion resolves each by + * name from the worker's resource runtime (equivalent to {@code Resources.use(name)}) + * and passes it positionally. Source-retention — read at compile time, never at + * runtime. + * + *
{@code
+ * @TaskHandler("send_email")
+ * void send(EmailPayload payload, @Resource("db") Database db) { ... }
+ * }
+ */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.PARAMETER) +public @interface Resource { + /** The registered resource name to resolve. */ + String value(); +} From 03b315dfa0d74b9ba5242d78b3f259307c1468f5 Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Wed, 1 Jul 2026 23:52:53 +0530 Subject: [PATCH 2/3] test(java): cover resource parameter injection --- .../taskito/AnnotationProcessorTest.java | 20 +++++++++++++++++++ .../org/byteveda/taskito/ResourceGreeter.java | 13 ++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 sdks/java/src/test/java/org/byteveda/taskito/ResourceGreeter.java diff --git a/sdks/java/src/test/java/org/byteveda/taskito/AnnotationProcessorTest.java b/sdks/java/src/test/java/org/byteveda/taskito/AnnotationProcessorTest.java index 52021e69..1946a755 100644 --- a/sdks/java/src/test/java/org/byteveda/taskito/AnnotationProcessorTest.java +++ b/sdks/java/src/test/java/org/byteveda/taskito/AnnotationProcessorTest.java @@ -37,6 +37,26 @@ void generatedCompanionEnqueuesAndHandles(@TempDir Path dir) throws Exception { } } + @Test + @Timeout(30) + void injectsResourceParameter(@TempDir Path dir) throws Exception { + try (Taskito queue = + Taskito.builder().sqlite(dir.resolve("ri.db").toString()).open()) { + // ResourceGreeter.greet takes a @Resource("salutation") param; the generated + // bind resolves it from the worker resource runtime. + queue.resource("salutation", ctx -> "hi"); + String id = queue.enqueue(ResourceGreeterTasks.GREET, "ada"); + CountDownLatch done = new CountDownLatch(1); + try (Worker worker = queue.worker() + .apply(builder -> ResourceGreeterTasks.bind(builder, new ResourceGreeter())) + .on(EventName.SUCCESS, event -> done.countDown()) + .start()) { + assertTrue(done.await(20, TimeUnit.SECONDS), "task should complete"); + assertEquals("hi ada", queue.getResult(id, String.class).orElseThrow()); + } + } + } + @Test @Timeout(30) void registerViaGeneratedHandlerRegistry(@TempDir Path dir) throws Exception { diff --git a/sdks/java/src/test/java/org/byteveda/taskito/ResourceGreeter.java b/sdks/java/src/test/java/org/byteveda/taskito/ResourceGreeter.java new file mode 100644 index 00000000..4105c39d --- /dev/null +++ b/sdks/java/src/test/java/org/byteveda/taskito/ResourceGreeter.java @@ -0,0 +1,13 @@ +package org.byteveda.taskito; + +import org.byteveda.taskito.annotation.Resource; +import org.byteveda.taskito.annotation.TaskHandler; + +/** Test fixture: a handler with a {@code @Resource}-injected parameter after the payload. */ +class ResourceGreeter { + + @TaskHandler("rg.greet") + String greet(String name, @Resource("salutation") String salutation) { + return salutation + " " + name; + } +} From 923db66e560e51bb25a2ceeb17b81c077ac9b857 Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Thu, 2 Jul 2026 00:12:54 +0530 Subject: [PATCH 3/3] fix(java): reject Resource annotation on the payload parameter --- .../org/byteveda/taskito/processor/TaskHandlerProcessor.java | 4 ++++ 1 file changed, 4 insertions(+) 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 2cf185ea..fbf34e48 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 @@ -63,6 +63,10 @@ private boolean validate(ExecutableElement method) { error(method, "@TaskHandler method must take the payload as its first parameter"); return false; } + if (resourceName(params.get(0)) != null) { + error(method, "the first @TaskHandler parameter is the payload and must not be annotated @Resource"); + return false; + } for (int i = 1; i < params.size(); i++) { if (resourceName(params.get(i)) == null) { error(method, "@TaskHandler parameters after the payload must be annotated @Resource");