From 5103bbb94d459222d8364d7ad320ae6d2f4af18d Mon Sep 17 00:00:00 2001 From: Quinn Klassen Date: Sat, 28 Sep 2024 14:08:25 -0700 Subject: [PATCH] Add helper for Nexus to Kotlin Integration (#2230) Add helper for Nexus to Kotlin --- temporal-kotlin/build.gradle | 1 + .../workflow/NexusOperationOptionsExt.kt | 41 +++++++ .../workflow/NexusServiceOptionsExt.kt | 41 +++++++ .../nexus/NexusOperationOptionsExtTest.kt | 42 +++++++ .../nexus/NexusServiceOptionsExtTest.kt | 67 +++++++++++ .../temporal/workflow/KotlinAsyncNexusTest.kt | 112 ++++++++++++++++++ .../workflow/NexusOperationOptions.java | 4 + .../workflow/NexusServiceOptions.java | 35 +++++- 8 files changed, 342 insertions(+), 1 deletion(-) create mode 100644 temporal-kotlin/src/main/kotlin/io/temporal/workflow/NexusOperationOptionsExt.kt create mode 100644 temporal-kotlin/src/main/kotlin/io/temporal/workflow/NexusServiceOptionsExt.kt create mode 100644 temporal-kotlin/src/test/kotlin/io/temporal/nexus/NexusOperationOptionsExtTest.kt create mode 100644 temporal-kotlin/src/test/kotlin/io/temporal/nexus/NexusServiceOptionsExtTest.kt create mode 100644 temporal-kotlin/src/test/kotlin/io/temporal/workflow/KotlinAsyncNexusTest.kt diff --git a/temporal-kotlin/build.gradle b/temporal-kotlin/build.gradle index 7b7359254..9fafadd5d 100644 --- a/temporal-kotlin/build.gradle +++ b/temporal-kotlin/build.gradle @@ -20,6 +20,7 @@ dependencies { // this module shouldn't carry temporal-sdk with it, especially for situations when users may be using a shaded artifact compileOnly project(':temporal-sdk') + implementation "io.nexusrpc:nexus-sdk:$nexusVersion" implementation "org.jetbrains.kotlin:kotlin-reflect" diff --git a/temporal-kotlin/src/main/kotlin/io/temporal/workflow/NexusOperationOptionsExt.kt b/temporal-kotlin/src/main/kotlin/io/temporal/workflow/NexusOperationOptionsExt.kt new file mode 100644 index 000000000..f7fb8369c --- /dev/null +++ b/temporal-kotlin/src/main/kotlin/io/temporal/workflow/NexusOperationOptionsExt.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.workflow + +import io.temporal.kotlin.TemporalDsl + +/** + * @see NexusOperationOptions + */ +inline fun NexusOperationOptions( + options: @TemporalDsl NexusOperationOptions.Builder.() -> Unit +): NexusOperationOptions { + return NexusOperationOptions.newBuilder().apply(options).build() +} + +/** + * Create a new instance of [NexusOperationOptions], optionally overriding some of its properties. + */ +inline fun NexusOperationOptions.copy( + overrides: @TemporalDsl NexusOperationOptions.Builder.() -> Unit +): NexusOperationOptions { + return toBuilder().apply(overrides).build() +} diff --git a/temporal-kotlin/src/main/kotlin/io/temporal/workflow/NexusServiceOptionsExt.kt b/temporal-kotlin/src/main/kotlin/io/temporal/workflow/NexusServiceOptionsExt.kt new file mode 100644 index 000000000..0d3c76649 --- /dev/null +++ b/temporal-kotlin/src/main/kotlin/io/temporal/workflow/NexusServiceOptionsExt.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.workflow + +import io.temporal.kotlin.TemporalDsl + +/** + * @see NexusServiceOptions + */ +inline fun NexusServiceOptions( + options: @TemporalDsl NexusServiceOptions.Builder.() -> Unit +): NexusServiceOptions { + return NexusServiceOptions.newBuilder().apply(options).build() +} + +/** + * Create a new instance of [NexusServiceOptions], optionally overriding some of its properties. + */ +inline fun NexusServiceOptions.copy( + overrides: @TemporalDsl NexusServiceOptions.Builder.() -> Unit +): NexusServiceOptions { + return toBuilder().apply(overrides).build() +} diff --git a/temporal-kotlin/src/test/kotlin/io/temporal/nexus/NexusOperationOptionsExtTest.kt b/temporal-kotlin/src/test/kotlin/io/temporal/nexus/NexusOperationOptionsExtTest.kt new file mode 100644 index 000000000..6dd2af4b5 --- /dev/null +++ b/temporal-kotlin/src/test/kotlin/io/temporal/nexus/NexusOperationOptionsExtTest.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.nexus + +import io.temporal.workflow.NexusOperationOptions +import org.junit.Assert.assertEquals +import org.junit.Test +import java.time.Duration + +class NexusOperationOptionsExtTest { + + @Test + fun `OperationOptions DSL should be equivalent to builder`() { + val dslOperationOptions = NexusOperationOptions { + setScheduleToCloseTimeout(Duration.ofMinutes(1)) + } + + val builderOperationOptions = NexusOperationOptions.newBuilder() + .setScheduleToCloseTimeout(Duration.ofMinutes(1)) + .build() + + assertEquals(builderOperationOptions, dslOperationOptions) + } +} diff --git a/temporal-kotlin/src/test/kotlin/io/temporal/nexus/NexusServiceOptionsExtTest.kt b/temporal-kotlin/src/test/kotlin/io/temporal/nexus/NexusServiceOptionsExtTest.kt new file mode 100644 index 000000000..fe89d0d81 --- /dev/null +++ b/temporal-kotlin/src/test/kotlin/io/temporal/nexus/NexusServiceOptionsExtTest.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.nexus + +import io.temporal.workflow.NexusOperationOptions +import io.temporal.workflow.NexusServiceOptions +import org.junit.Assert.assertEquals +import org.junit.Test +import java.time.Duration + +class NexusServiceOptionsExtTest { + + @Test + fun `ServiceOptions DSL should be equivalent to builder`() { + val dslServiceOptions = NexusServiceOptions { + setEndpoint("TestEndpoint") + setOperationOptions( + NexusOperationOptions { + setScheduleToCloseTimeout(Duration.ofMinutes(1)) + } + ) + setOperationMethodOptions( + mapOf( + "test" to NexusOperationOptions { + setScheduleToCloseTimeout(Duration.ofMinutes(2)) + } + ) + ) + } + + val builderServiceOptions = NexusServiceOptions.newBuilder() + .setEndpoint("TestEndpoint") + .setOperationOptions( + NexusOperationOptions.newBuilder() + .setScheduleToCloseTimeout(Duration.ofMinutes(1)) + .build() + ) + .setOperationMethodOptions( + mapOf( + "test" to NexusOperationOptions.newBuilder() + .setScheduleToCloseTimeout(Duration.ofMinutes(2)) + .build() + ) + ) + .build() + + assertEquals(builderServiceOptions, dslServiceOptions) + } +} diff --git a/temporal-kotlin/src/test/kotlin/io/temporal/workflow/KotlinAsyncNexusTest.kt b/temporal-kotlin/src/test/kotlin/io/temporal/workflow/KotlinAsyncNexusTest.kt new file mode 100644 index 000000000..53ec4c109 --- /dev/null +++ b/temporal-kotlin/src/test/kotlin/io/temporal/workflow/KotlinAsyncNexusTest.kt @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2022 Temporal Technologies, Inc. All Rights Reserved. + * + * Copyright (C) 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Modifications copyright (C) 2017 Uber Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this material except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.temporal.workflow + +import io.nexusrpc.Operation +import io.nexusrpc.Service +import io.nexusrpc.handler.OperationContext +import io.nexusrpc.handler.OperationHandler +import io.nexusrpc.handler.OperationImpl +import io.nexusrpc.handler.OperationStartDetails +import io.nexusrpc.handler.ServiceImpl +import io.nexusrpc.handler.SynchronousOperationFunction +import io.temporal.client.WorkflowClientOptions +import io.temporal.client.WorkflowOptions +import io.temporal.common.converter.DefaultDataConverter +import io.temporal.common.converter.JacksonJsonPayloadConverter +import io.temporal.common.converter.KotlinObjectMapperFactory +import io.temporal.internal.async.FunctionWrappingUtil +import io.temporal.internal.sync.AsyncInternal +import io.temporal.testing.internal.SDKTestWorkflowRule +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import java.time.Duration + +class KotlinAsyncNexusTest { + + @Rule + @JvmField + var testWorkflowRule: SDKTestWorkflowRule = SDKTestWorkflowRule.newBuilder() + .setWorkflowTypes(WorkflowImpl::class.java) + .setNexusServiceImplementation(TestNexusServiceImpl()) + .setWorkflowClientOptions( + WorkflowClientOptions.newBuilder() + .setDataConverter(DefaultDataConverter(JacksonJsonPayloadConverter(KotlinObjectMapperFactory.new()))) + .build() + ) + .build() + + @Service + interface TestNexusService { + @Operation + fun operation(): String? + } + + @ServiceImpl(service = TestNexusService::class) + class TestNexusServiceImpl { + @OperationImpl + fun operation(): OperationHandler { + // Implemented inline + return OperationHandler.sync( + SynchronousOperationFunction { ctx: OperationContext, details: OperationStartDetails, _: Void? -> "Hello Kotlin" } + ) + } + } + + @WorkflowInterface + interface TestWorkflow { + @WorkflowMethod + fun execute() + } + + class WorkflowImpl : TestWorkflow { + override fun execute() { + val nexusService = Workflow.newNexusServiceStub( + TestNexusService::class.java, + NexusServiceOptions { + setOperationOptions( + NexusOperationOptions { + setScheduleToCloseTimeout(Duration.ofSeconds(10)) + } + ) + } + ) + assertTrue( + "This has to be true to make Async.function(nexusService::operation) work correctly as expected", + AsyncInternal.isAsync(nexusService::operation) + ) + assertTrue( + "This has to be true to make Async.function(nexusService::operation) work correctly as expected", + AsyncInternal.isAsync(FunctionWrappingUtil.temporalJavaFunctionalWrapper(nexusService::operation)) + ) + Async.function(nexusService::operation).get() + } + } + + @Test + fun asyncNexusWorkflowTest() { + val client = testWorkflowRule.workflowClient + val options = WorkflowOptions.newBuilder().setTaskQueue(testWorkflowRule.taskQueue).build() + val workflowStub = client.newWorkflowStub(TestWorkflow::class.java, options) + workflowStub.execute() + } +} diff --git a/temporal-sdk/src/main/java/io/temporal/workflow/NexusOperationOptions.java b/temporal-sdk/src/main/java/io/temporal/workflow/NexusOperationOptions.java index efdf04431..e5016f9db 100644 --- a/temporal-sdk/src/main/java/io/temporal/workflow/NexusOperationOptions.java +++ b/temporal-sdk/src/main/java/io/temporal/workflow/NexusOperationOptions.java @@ -95,6 +95,10 @@ private NexusOperationOptions(Duration scheduleToCloseTimeout) { this.scheduleToCloseTimeout = scheduleToCloseTimeout; } + public NexusOperationOptions.Builder toBuilder() { + return new NexusOperationOptions.Builder(this); + } + private Duration scheduleToCloseTimeout; public Duration getScheduleToCloseTimeout() { diff --git a/temporal-sdk/src/main/java/io/temporal/workflow/NexusServiceOptions.java b/temporal-sdk/src/main/java/io/temporal/workflow/NexusServiceOptions.java index ad226101c..ccc6ca87d 100644 --- a/temporal-sdk/src/main/java/io/temporal/workflow/NexusServiceOptions.java +++ b/temporal-sdk/src/main/java/io/temporal/workflow/NexusServiceOptions.java @@ -23,6 +23,7 @@ import io.temporal.common.Experimental; import java.util.Collections; import java.util.Map; +import java.util.Objects; /** * Options for configuring a NexusService in a Workflow. @@ -131,7 +132,7 @@ public NexusServiceOptions.Builder mergeNexusServiceOptions(NexusServiceOptions private final Map operationMethodOptions; private final String endpoint; - NexusServiceOptions( + private NexusServiceOptions( String endpoint, NexusOperationOptions operationOptions, Map operationMethodOptions) { @@ -143,6 +144,10 @@ public NexusServiceOptions.Builder mergeNexusServiceOptions(NexusServiceOptions : Collections.unmodifiableMap(operationMethodOptions); } + public NexusServiceOptions.Builder toBuilder() { + return new NexusServiceOptions.Builder(this); + } + public NexusOperationOptions getOperationOptions() { return operationOptions; } @@ -154,4 +159,32 @@ public String getEndpoint() { public Map getOperationMethodOptions() { return operationMethodOptions; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + NexusServiceOptions that = (NexusServiceOptions) o; + return Objects.equals(operationOptions, that.operationOptions) + && Objects.equals(operationMethodOptions, that.operationMethodOptions) + && Objects.equals(endpoint, that.endpoint); + } + + @Override + public int hashCode() { + return Objects.hash(operationOptions, operationMethodOptions, endpoint); + } + + @Override + public String toString() { + return "NexusServiceOptions{" + + "operationOptions=" + + operationOptions + + ", operationMethodOptions=" + + operationMethodOptions + + ", endpoint='" + + endpoint + + '\'' + + '}'; + } }