From 416f31d0f46186f4865b771acfd57d54b9e9c652 Mon Sep 17 00:00:00 2001 From: Quinn Klassen Date: Mon, 18 Aug 2025 11:17:35 -0700 Subject: [PATCH 1/4] Add ServiceDefinition to OperationContext --- .../io/nexusrpc/handler/OperationContext.java | 34 ++++++++++++++++--- .../io/nexusrpc/handler/ServiceHandler.java | 29 +++++++++++++--- .../handler/OperationContextTest.java | 28 +++++++++++++++ 3 files changed, 81 insertions(+), 10 deletions(-) diff --git a/nexus-sdk/src/main/java/io/nexusrpc/handler/OperationContext.java b/nexus-sdk/src/main/java/io/nexusrpc/handler/OperationContext.java index 3628e07..cee2c13 100644 --- a/nexus-sdk/src/main/java/io/nexusrpc/handler/OperationContext.java +++ b/nexus-sdk/src/main/java/io/nexusrpc/handler/OperationContext.java @@ -2,6 +2,7 @@ import io.nexusrpc.Link; import java.time.Instant; +import io.nexusrpc.ServiceDefinition; import java.util.*; import java.util.stream.Collectors; import org.jspecify.annotations.Nullable; @@ -25,18 +26,21 @@ public static Builder newBuilder(OperationContext context) { private final @Nullable OperationMethodCanceller methodCanceller; private final List links = new ArrayList<>(); private final Instant deadline; + private final @Nullable ServiceDefinition serviceDefinition; private OperationContext( String service, String operation, Map headers, @Nullable OperationMethodCanceller methodCanceller, - Instant deadline) { + Instant deadline, + @Nullable ServiceDefinition serviceDefinition) { this.service = service; this.operation = operation; this.headers = headers; this.methodCanceller = methodCanceller; this.deadline = deadline; + this.serviceDefinition = serviceDefinition; } /** Service name for the call. */ @@ -75,6 +79,11 @@ public boolean isMethodCancelled() { return methodCanceller == null ? null : methodCanceller.getCancellationReason(); } + /** Get the service definition associated with this operation context, if any. */ + public @Nullable ServiceDefinition getServiceDefinition() { + return serviceDefinition; + } + /** * Get the deadline for the operation handler method. This is the time by which the method should * complete. This is not the operation's deadline. @@ -136,18 +145,19 @@ public OperationContext removeMethodCancellationListener( @Override public boolean equals(Object o) { - if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; OperationContext that = (OperationContext) o; return Objects.equals(service, that.service) && Objects.equals(operation, that.operation) && Objects.equals(headers, that.headers) - && Objects.equals(links, that.links); + && Objects.equals(methodCanceller, that.methodCanceller) + && Objects.equals(links, that.links) + && Objects.equals(serviceDefinition, that.serviceDefinition); } @Override public int hashCode() { - return Objects.hash(service, operation, headers); + return Objects.hash(service, operation, headers, methodCanceller, links, serviceDefinition); } @Override @@ -161,8 +171,12 @@ public String toString() { + '\'' + ", headers=" + headers + + ", methodCanceller=" + + methodCanceller + ", links=" + links + + ", serviceDefinition=" + + serviceDefinition + '}'; } @@ -173,6 +187,7 @@ public static class Builder { private final SortedMap headers; private @Nullable OperationMethodCanceller methodCanceller; private @Nullable Instant deadline; + private @Nullable ServiceDefinition serviceDefinition; private Builder() { headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); @@ -182,6 +197,9 @@ private Builder(OperationContext context) { service = context.service; operation = context.operation; headers = new TreeMap<>(context.headers); + methodCanceller = context.methodCanceller; + deadline = context.deadline; + serviceDefinition = context.serviceDefinition; } /** Set service. Required. */ @@ -219,6 +237,11 @@ public Builder setDeadline(Instant deadline) { return this; } + public Builder setServiceDefinition(ServiceDefinition serviceDefinition) { + this.serviceDefinition = serviceDefinition; + return this; + } + /** Build the context. */ public OperationContext build() { Objects.requireNonNull(service, "Service required"); @@ -236,7 +259,8 @@ public OperationContext build() { operation, Collections.unmodifiableMap(new TreeMap<>(normalizedHeaders)), methodCanceller, - deadline); + deadline, + serviceDefinition); } } } diff --git a/nexus-sdk/src/main/java/io/nexusrpc/handler/ServiceHandler.java b/nexus-sdk/src/main/java/io/nexusrpc/handler/ServiceHandler.java index f27a9ad..9f055ab 100644 --- a/nexus-sdk/src/main/java/io/nexusrpc/handler/ServiceHandler.java +++ b/nexus-sdk/src/main/java/io/nexusrpc/handler/ServiceHandler.java @@ -67,8 +67,12 @@ public OperationStartResult startOperation( if (handler == null) { throw newUnrecognizedOperationException(context.getService(), context.getOperation()); } + // Populate the service definition in the context so that the handler can use it + OperationContext contextWithServiceDef = + OperationContext.newBuilder(context).setServiceDefinition(instance.getDefinition()).build(); + OperationHandler interceptedHandler = - interceptOperationHandler(context, handler); + interceptOperationHandler(contextWithServiceDef, handler); OperationDefinition definition = instance.getDefinition().getOperations().get(context.getOperation()); @@ -84,7 +88,8 @@ public OperationStartResult startOperation( } // Invoke handler - OperationStartResult result = interceptedHandler.start(context, details, inputObject); + OperationStartResult result = + interceptedHandler.start(contextWithServiceDef, details, inputObject); // If the result is an async result we can just return, but if it's a sync result we need to // serialize back out to bytes @@ -108,7 +113,13 @@ public HandlerResultContent fetchOperationResult( if (handler == null) { throw newUnrecognizedOperationException(context.getService(), context.getOperation()); } - Object result = interceptOperationHandler(context, handler).fetchResult(context, details); + // Populate the service definition in the context so that the handler can use it + OperationContext contextWithServiceDef = + OperationContext.newBuilder(context).setServiceDefinition(instance.getDefinition()).build(); + + Object result = + interceptOperationHandler(contextWithServiceDef, handler) + .fetchResult(contextWithServiceDef, details); return resultToContent(result); } @@ -136,7 +147,11 @@ public OperationInfo fetchOperationInfo( if (handler == null) { throw newUnrecognizedOperationException(context.getService(), context.getOperation()); } - return interceptOperationHandler(context, handler).fetchInfo(context, details); + // Populate the service definition in the context so that the handler can use it + OperationContext contextWithServiceDef = + OperationContext.newBuilder(context).setServiceDefinition(instance.getDefinition()).build(); + return interceptOperationHandler(contextWithServiceDef, handler) + .fetchInfo(contextWithServiceDef, details); } @Override @@ -150,7 +165,11 @@ public void cancelOperation(OperationContext context, OperationCancelDetails det if (handler == null) { throw newUnrecognizedOperationException(context.getService(), context.getOperation()); } - interceptOperationHandler(context, handler).cancel(context, details); + // Populate the service definition in the context so that the handler can use it + OperationContext contextWithServiceDef = + OperationContext.newBuilder(context).setServiceDefinition(instance.getDefinition()).build(); + interceptOperationHandler(contextWithServiceDef, handler) + .cancel(contextWithServiceDef, details); } private static HandlerException newUnrecognizedOperationException( diff --git a/nexus-sdk/src/test/java/io/nexusrpc/handler/OperationContextTest.java b/nexus-sdk/src/test/java/io/nexusrpc/handler/OperationContextTest.java index 95148ff..dade05a 100644 --- a/nexus-sdk/src/test/java/io/nexusrpc/handler/OperationContextTest.java +++ b/nexus-sdk/src/test/java/io/nexusrpc/handler/OperationContextTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import io.nexusrpc.Link; +import io.nexusrpc.example.TestServices; import java.net.URI; import java.net.URISyntaxException; import java.time.Instant; @@ -10,6 +11,15 @@ import org.junit.jupiter.api.Test; public class OperationContextTest { + + @ServiceImpl(service = TestServices.IntegerService.class) + public class IntegerServiceImpl { + @OperationImpl + public OperationHandler operation() { + return OperationHandler.sync((ctx, details, input) -> 0); + } + } + @Test void linkTest() throws URISyntaxException { OperationContext octx = @@ -34,4 +44,22 @@ void deadlineTest() { .build(); assertEquals(deadline, octx.getDeadline()); } + + @Test + void contextBuilderTest() { + OperationMethodCanceller omc = new OperationMethodCanceller(); + ServiceImplInstance serviceImpl = ServiceImplInstance.fromInstance(new IntegerServiceImpl()); + Instant deadline = Instant.now().plusMillis(1000); + OperationContext octx = + OperationContext.newBuilder() + .setService("service") + .setOperation("operation") + .setDeadline(deadline) + .putHeader("key", "value") + .setMethodCanceller(omc) + .setServiceDefinition(serviceImpl.getDefinition()) + .build(); + + assertEquals(octx, OperationContext.newBuilder(octx).build()); + } } From 41adef68218677e880e32d7f9ed0eda70d3dc888 Mon Sep 17 00:00:00 2001 From: Quinn Klassen Date: Mon, 18 Aug 2025 15:20:38 -0700 Subject: [PATCH 2/4] Make sure links is passed through --- .../io/nexusrpc/handler/OperationContext.java | 15 ++++++++++++--- .../io/nexusrpc/handler/OperationContextTest.java | 3 ++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/nexus-sdk/src/main/java/io/nexusrpc/handler/OperationContext.java b/nexus-sdk/src/main/java/io/nexusrpc/handler/OperationContext.java index cee2c13..2a2c728 100644 --- a/nexus-sdk/src/main/java/io/nexusrpc/handler/OperationContext.java +++ b/nexus-sdk/src/main/java/io/nexusrpc/handler/OperationContext.java @@ -24,8 +24,8 @@ public static Builder newBuilder(OperationContext context) { private final Map headers; // This is not included in equals, hashCode, or toString private final @Nullable OperationMethodCanceller methodCanceller; - private final List links = new ArrayList<>(); private final Instant deadline; + private final List links; private final @Nullable ServiceDefinition serviceDefinition; private OperationContext( @@ -34,13 +34,15 @@ private OperationContext( Map headers, @Nullable OperationMethodCanceller methodCanceller, Instant deadline, - @Nullable ServiceDefinition serviceDefinition) { + @Nullable ServiceDefinition serviceDefinition, + List links) { this.service = service; this.operation = operation; this.headers = headers; this.methodCanceller = methodCanceller; this.deadline = deadline; this.serviceDefinition = serviceDefinition; + this.links = links; } /** Service name for the call. */ @@ -188,9 +190,14 @@ public static class Builder { private @Nullable OperationMethodCanceller methodCanceller; private @Nullable Instant deadline; private @Nullable ServiceDefinition serviceDefinition; + // Currently links are not set in the builder, but they need to be passed though to go from + // OperationContext -> Builder + // and back to OperationContext, so we keep them here. + private final List links; private Builder() { headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + links = new ArrayList<>(); } private Builder(OperationContext context) { @@ -200,6 +207,7 @@ private Builder(OperationContext context) { methodCanceller = context.methodCanceller; deadline = context.deadline; serviceDefinition = context.serviceDefinition; + links = context.links; } /** Set service. Required. */ @@ -260,7 +268,8 @@ public OperationContext build() { Collections.unmodifiableMap(new TreeMap<>(normalizedHeaders)), methodCanceller, deadline, - serviceDefinition); + serviceDefinition, + links); } } } diff --git a/nexus-sdk/src/test/java/io/nexusrpc/handler/OperationContextTest.java b/nexus-sdk/src/test/java/io/nexusrpc/handler/OperationContextTest.java index dade05a..0ae2e63 100644 --- a/nexus-sdk/src/test/java/io/nexusrpc/handler/OperationContextTest.java +++ b/nexus-sdk/src/test/java/io/nexusrpc/handler/OperationContextTest.java @@ -59,7 +59,8 @@ void contextBuilderTest() { .setMethodCanceller(omc) .setServiceDefinition(serviceImpl.getDefinition()) .build(); - + URI url = new URI("http://somepath?k=v"); + octx.setLinks(Link.newBuilder().setUri(url).setType("com.example.MyResource").build()); assertEquals(octx, OperationContext.newBuilder(octx).build()); } } From 59052f6fbe7a530595d78e9c046f48f1db77f2c4 Mon Sep 17 00:00:00 2001 From: Quinn Klassen Date: Mon, 18 Aug 2025 15:26:25 -0700 Subject: [PATCH 3/4] run spotless --- .../src/main/java/io/nexusrpc/handler/OperationContext.java | 2 +- .../src/test/java/io/nexusrpc/handler/OperationContextTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nexus-sdk/src/main/java/io/nexusrpc/handler/OperationContext.java b/nexus-sdk/src/main/java/io/nexusrpc/handler/OperationContext.java index 2a2c728..a8537af 100644 --- a/nexus-sdk/src/main/java/io/nexusrpc/handler/OperationContext.java +++ b/nexus-sdk/src/main/java/io/nexusrpc/handler/OperationContext.java @@ -1,8 +1,8 @@ package io.nexusrpc.handler; import io.nexusrpc.Link; -import java.time.Instant; import io.nexusrpc.ServiceDefinition; +import java.time.Instant; import java.util.*; import java.util.stream.Collectors; import org.jspecify.annotations.Nullable; diff --git a/nexus-sdk/src/test/java/io/nexusrpc/handler/OperationContextTest.java b/nexus-sdk/src/test/java/io/nexusrpc/handler/OperationContextTest.java index 0ae2e63..72a60fc 100644 --- a/nexus-sdk/src/test/java/io/nexusrpc/handler/OperationContextTest.java +++ b/nexus-sdk/src/test/java/io/nexusrpc/handler/OperationContextTest.java @@ -49,7 +49,7 @@ void deadlineTest() { void contextBuilderTest() { OperationMethodCanceller omc = new OperationMethodCanceller(); ServiceImplInstance serviceImpl = ServiceImplInstance.fromInstance(new IntegerServiceImpl()); - Instant deadline = Instant.now().plusMillis(1000); + Instant deadline = Instant.now().plusMillis(1000); OperationContext octx = OperationContext.newBuilder() .setService("service") From 1574df03837fb21178e91d17f5a8fc7c314c867d Mon Sep 17 00:00:00 2001 From: Quinn Klassen Date: Mon, 18 Aug 2025 15:28:39 -0700 Subject: [PATCH 4/4] Add some more comments --- .../main/java/io/nexusrpc/handler/OperationContext.java | 7 ++++++- .../java/io/nexusrpc/handler/OperationContextTest.java | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/nexus-sdk/src/main/java/io/nexusrpc/handler/OperationContext.java b/nexus-sdk/src/main/java/io/nexusrpc/handler/OperationContext.java index a8537af..9e4b64e 100644 --- a/nexus-sdk/src/main/java/io/nexusrpc/handler/OperationContext.java +++ b/nexus-sdk/src/main/java/io/nexusrpc/handler/OperationContext.java @@ -153,13 +153,15 @@ public boolean equals(Object o) { && Objects.equals(operation, that.operation) && Objects.equals(headers, that.headers) && Objects.equals(methodCanceller, that.methodCanceller) + && Objects.equals(deadline, that.deadline) && Objects.equals(links, that.links) && Objects.equals(serviceDefinition, that.serviceDefinition); } @Override public int hashCode() { - return Objects.hash(service, operation, headers, methodCanceller, links, serviceDefinition); + return Objects.hash( + service, operation, headers, methodCanceller, deadline, links, serviceDefinition); } @Override @@ -175,6 +177,8 @@ public String toString() { + headers + ", methodCanceller=" + methodCanceller + + ", deadline=" + + deadline + ", links=" + links + ", serviceDefinition=" @@ -245,6 +249,7 @@ public Builder setDeadline(Instant deadline) { return this; } + /** Sets the service definition. */ public Builder setServiceDefinition(ServiceDefinition serviceDefinition) { this.serviceDefinition = serviceDefinition; return this; diff --git a/nexus-sdk/src/test/java/io/nexusrpc/handler/OperationContextTest.java b/nexus-sdk/src/test/java/io/nexusrpc/handler/OperationContextTest.java index 72a60fc..013b54e 100644 --- a/nexus-sdk/src/test/java/io/nexusrpc/handler/OperationContextTest.java +++ b/nexus-sdk/src/test/java/io/nexusrpc/handler/OperationContextTest.java @@ -46,7 +46,7 @@ void deadlineTest() { } @Test - void contextBuilderTest() { + void contextBuilderTest() throws URISyntaxException { OperationMethodCanceller omc = new OperationMethodCanceller(); ServiceImplInstance serviceImpl = ServiceImplInstance.fromInstance(new IntegerServiceImpl()); Instant deadline = Instant.now().plusMillis(1000);