Skip to content

Commit

Permalink
Add temporal-kotlin providing correct support for Kotlin in Async
Browse files Browse the repository at this point in the history
  • Loading branch information
Spikhalskiy committed Aug 14, 2021
1 parent 58f02fe commit e370202
Show file tree
Hide file tree
Showing 18 changed files with 657 additions and 33 deletions.
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ include 'temporal-testing'
include 'temporal-testing-junit4'
include 'temporal-testing-junit5'
include 'temporal-opentracing'
include 'temporal-kotlin'
2 changes: 2 additions & 0 deletions temporal-kotlin/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[*.{kt,kts}]
indent_size = 2
18 changes: 18 additions & 0 deletions temporal-kotlin/LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
Temporal Java SDK

Copyright (c) 2020 Temporal Technologies, Inc. All Rights Reserved

Copyright (c) 2017 Uber Technologies, Inc. All Rights Reserved

AWS Simple Workflow Flow Library
Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
file 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.
20 changes: 20 additions & 0 deletions temporal-kotlin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Temporal Kotlin support module

This module added to classpath provides some support for Kotlin specific language features:
- Support for Kotlin method references for Temporal client stubs passed into Async

## Usage

Add temporal-kotlin as a dependency to your pom.xml:
```
<dependency>
<groupId>io.temporal</groupId>
<artifactId>temporal-kotlin</artifactId>
<version>N.N.N</version>
</dependency>
```

or to build.gradle:
```
compile group: 'io.temporal', name: 'temporal-kotlin', version: 'N.N.N'
```
42 changes: 42 additions & 0 deletions temporal-kotlin/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
plugins {
// id 'org.jetbrains.kotlin.jvm' version '1.3.72'
// id 'org.jetbrains.kotlin.jvm' version '1.4.21'
id 'org.jetbrains.kotlin.jvm' version '1.5.21'

id 'org.jlleitschuh.gradle.ktlint' version '10.1.0'
}

description = '''Temporal Workflow Java SDK Kotlin'''

ext {
// kotlinVersion = '1.3.72'
// kotlinVersion = '1.4.21'
kotlinVersion = '1.5.21'
}

compileKotlin {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
}

compileTestKotlin {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
}

dependencies {
compileOnly group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib', version: kotlinVersion

implementation project(':temporal-sdk')

implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.12.4'
implementation group: 'com.fasterxml.jackson.module', name: 'jackson-module-kotlin', version: '2.12.4'

testImplementation project(':temporal-testing')
testImplementation project(':temporal-testing-junit4')

testImplementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.5'
testImplementation group: 'junit', name: 'junit', version: '4.13.2'
}
16 changes: 16 additions & 0 deletions temporal-kotlin/license-header.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved.

Copyright 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 file except in compliance with the License. A copy of the License is
located at

http://aws.amazon.com/apache2.0

or in the "license" file accompanying this file. This file 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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved.
*
* Copyright 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 file except in compliance with the License. A copy of the License is
* located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file 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.common.converter

import com.fasterxml.jackson.annotation.JsonAutoDetect
import com.fasterxml.jackson.annotation.PropertyAccessor
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.KotlinModule

class KotlinObjectMapperFactory {
companion object {
@JvmStatic
fun new(): ObjectMapper {
val mapper = ObjectMapper()
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
mapper.registerModule(JavaTimeModule())
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
mapper.registerModule(KotlinModule())
return mapper
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved.
*
* Copyright 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 file except in compliance with the License. A copy of the License is
* located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file 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.internal.async

import io.temporal.internal.async.spi.MethodReferenceDisassemblyService
import io.temporal.workflow.Functions
import kotlin.jvm.internal.CallableReference

class KotlinMethodReferenceDisassemblyService : MethodReferenceDisassemblyService {
override fun getMethodReferenceTarget(methodReference: Any): Any? {
if (methodReference is CallableReference) {
return unwrapCallableReference(methodReference)
}

if (methodReference is Functions.TemporalFunctionalInterfaceMarker) {
return unwrapTemporalFunctionalInterfaceInKotlin(methodReference)
}

return null
}

/**
* Strategy 1
* We unwrap simple native Kotlin [CallableReference] (which is used for method references)
*/
private fun unwrapCallableReference(callableReference: CallableReference): Any {
return callableReference.boundReceiver
}

/**
* Strategy 2
* Kotlin bumped into one of [io.temporal.workflow.Async] calls that have one of our [io.temporal.workflow.Functions]
* as a parameter and has to implement/wrap the method reference as one of our function interfaces
*/
private fun unwrapTemporalFunctionalInterfaceInKotlin(temporalFunction: Functions.TemporalFunctionalInterfaceMarker): Any? {
val declaredFields = temporalFunction.javaClass.declaredFields
if (declaredFields.size != 1) {
return null
}

val proxiedField = declaredFields[0]
proxiedField.isAccessible = true
val proxiedValue = proxiedField[temporalFunction]
if ("function" == proxiedField.name) {
/**
* Strategy 2.1
* Kotlin 1.4 and earlier wraps Kotlin's [CallableReference]
* into one of [io.temporal.workflow.Functions] wrappers.
* This will be a generated class handling the Callable Reference in 'function' field.
*/
return if (proxiedValue is CallableReference) {
unwrapCallableReference(proxiedValue)
} else {
null
}
} else {
/**
* Strategy 2.2
* Kotlin 1.5 generates one of [io.temporal.workflow.Functions] directly over the target
* without any [CallableReference] in between, in that case our target is persisted directly
* in the single field of the generated Function.
*/
return proxiedValue
}
}

override fun getLanguageName(): String {
return MethodReferenceDisassemblyService.KOTLIN
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
io.temporal.internal.async.KotlinMethodReferenceDisassemblyService
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved.
*
* Copyright 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 file except in compliance with the License. A copy of the License is
* located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file 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.internal.async;

import io.temporal.workflow.Functions;

public class FunctionWrappingUtil {
/**
* We emulate here what happens in Async/AsyncInternal when {@link
* io.temporal.workflow.Async#function(Functions.Func)} accepts {@link Functions.Func<R>} as a
* parameter and kotlin method reference is getting wrapped into {@link Functions.Func<R>}
*/
public static <R> Functions.Func<R> temporalJavaFunctionalWrapper(Functions.Func<R> function) {
return function;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved.
*
* Copyright 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 file except in compliance with the License. A copy of the License is
* located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file 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.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.TestWorkflowRule
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test

class KotlinAsyncChildWorkflowTest {

@Rule @JvmField
var testWorkflowRule: TestWorkflowRule = TestWorkflowRule.newBuilder()
.setWorkflowTypes(NaiveParentWorkflowImpl::class.java, ChildWorkflowImpl::class.java)
.setWorkflowClientOptions(
WorkflowClientOptions.newBuilder()
.setDataConverter(DefaultDataConverter(JacksonJsonPayloadConverter(KotlinObjectMapperFactory.new())))
.build()
)
.build()

@WorkflowInterface
interface ChildWorkflow {
@WorkflowMethod
fun execute(): Int
}

class ChildWorkflowImpl : ChildWorkflow {
override fun execute(): Int {
return 0
}
}

@WorkflowInterface
interface NaiveParentWorkflow {
@WorkflowMethod
fun execute()
}

class NaiveParentWorkflowImpl : NaiveParentWorkflow {
override fun execute() {
val childWorkflow = Workflow.newChildWorkflowStub(ChildWorkflow::class.java)
assertTrue(
"This has to be true to make Async.function(childWorkflow::execute) work correctly as expected",
AsyncInternal.isAsync(childWorkflow::execute)
)
assertTrue(
"This has to be true to make Async.function(childWorkflow::execute) work correctly as expected",
AsyncInternal.isAsync(FunctionWrappingUtil.temporalJavaFunctionalWrapper(childWorkflow::execute))
)
Async.function(childWorkflow::execute).get()
}
}

@Test
fun asyncChildWorkflowTest() {
val client = testWorkflowRule.workflowClient
val options = WorkflowOptions.newBuilder().setTaskQueue(testWorkflowRule.taskQueue).build()
val workflowStub = client.newWorkflowStub(NaiveParentWorkflow::class.java, options)
workflowStub.execute()
}
}
Loading

0 comments on commit e370202

Please sign in to comment.