-
Notifications
You must be signed in to change notification settings - Fork 22
Kotlin SDK API Proposal #104
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
8b9724a
b3cb4e5
57152b1
2f08871
2153e4c
4a10992
a42eb80
5b410e0
914d0d4
b46e530
42610af
956c9a9
7e2cb86
6422b7e
164fa22
701f306
b927c06
516b837
e25c017
e6ab5ee
234b072
94417b5
66e6ee5
56d8e0c
7a2b51f
c70697c
9152ca6
d713d0c
9ae3ffb
917baaa
901cf2b
37af43d
72d707e
1d282d7
5fef37f
636edde
5ab8006
95e7985
1d0a468
60981f0
813ae18
614b176
f2d2a50
e541eb6
71000b3
8411e2e
c336312
ba584ce
011eae2
e259002
08bc977
a35e497
9f492c2
75878d6
553d34b
b70eac1
8638229
2a727c8
3a7ac10
eaaec83
f95b7bf
9603691
dfb633e
8273a67
c86195b
c5c1e1e
e1c6c05
fcaaa01
de7cbad
a45e8de
d63d702
c978a16
f967a07
a98bf12
4306ffa
fb99a76
99b668e
8ae3077
3828995
c6370b1
43c7014
dbfc512
edf8cbf
8d3d46f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| # Kotlin SDK API Proposal | ||
|
|
||
| This document describes the public API and developer experience for the Temporal Kotlin SDK. | ||
|
|
||
| ## Overview | ||
|
|
||
| The Kotlin SDK provides an idiomatic Kotlin experience for building Temporal workflows using coroutines and suspend functions. | ||
|
|
||
| **Key Features:** | ||
|
|
||
| * Coroutine-based workflows with `suspend fun` | ||
| * Full interoperability with Java SDK | ||
| * Kotlin Duration support (`30.seconds`) | ||
| * DSL builders for configuration | ||
| * Null safety (no `Optional<T>`) | ||
|
|
||
| **Requirements:** | ||
|
|
||
| * Minimum Kotlin version: 1.8.x | ||
| * Coroutines library: kotlinx-coroutines-core 1.7.x+ | ||
|
|
||
| ## Design Principle | ||
|
|
||
| **Use idiomatic Kotlin language patterns wherever possible instead of custom APIs.** | ||
|
|
||
| The Kotlin SDK should feel natural to Kotlin developers by leveraging standard `kotlinx.coroutines` primitives. Custom APIs should only be introduced when Temporal-specific semantics cannot be achieved through standard patterns. | ||
|
|
||
| | Pattern | Standard Kotlin | Temporal Integration | | ||
| |---------|-----------------|----------------------| | ||
| | Parallel execution | `coroutineScope { async { ... } }` | Works via deterministic dispatcher | | ||
| | Await multiple | `awaitAll(d1, d2)` | Standard kotlinx.coroutines | | ||
| | Sleep/delay | `delay(duration)` | Intercepted via `Delay` interface | | ||
| | Deferred results | `Deferred<T>` | Standard + `Promise<T>.toDeferred()` | | ||
|
|
||
| This approach provides: | ||
| - **Familiar patterns**: Kotlin developers use patterns they already know | ||
| - **IDE support**: Full autocomplete and documentation for standard APIs | ||
| - **Ecosystem compatibility**: Works with existing coroutine libraries and utilities | ||
| - **Smaller API surface**: Less custom code to learn and maintain | ||
|
|
||
| ## Documentation Structure | ||
|
|
||
| ### Core Concepts | ||
|
|
||
| - **[Kotlin Idioms](./kotlin-idioms.md)** - Duration, null safety, property syntax for queries | ||
| - **[Configuration](./configuration/README.md)** - KOptions classes, data conversion, interceptors | ||
|
|
||
| ### Building Blocks | ||
|
|
||
| - **[Workflows](./workflows/README.md)** - Defining and implementing workflows | ||
| - [Definition](./workflows/definition.md) - Interfaces, suspend methods, Java interop | ||
| - [Signals, Queries & Updates](./workflows/signals-queries.md) - Communication patterns | ||
| - [Child Workflows](./workflows/child-workflows.md) - Orchestrating child workflows | ||
| - [Timers & Parallel Execution](./workflows/timers-parallel.md) - Delays, async patterns | ||
| - [Cancellation](./workflows/cancellation.md) - Handling cancellation, cleanup | ||
| - [Continue-As-New](./workflows/continue-as-new.md) - Long-running workflow patterns | ||
|
|
||
| - **[Activities](./activities/README.md)** - Defining and implementing activities | ||
| - [Definition](./activities/definition.md) - Interfaces, typed/string-based execution | ||
| - [Implementation](./activities/implementation.md) - Suspend activities, heartbeating | ||
| - [Local Activities](./activities/local-activities.md) - Short-lived local activities | ||
|
|
||
| ### Infrastructure | ||
|
|
||
| - **[Client](./client/README.md)** - Interacting with workflows | ||
| - [Workflow Client](./client/workflow-client.md) - KWorkflowClient, starting workflows | ||
| - [Workflow Handles](./client/workflow-handle.md) - Signals, queries, results | ||
| - [Advanced Operations](./client/advanced.md) - SignalWithStart, UpdateWithStart | ||
|
|
||
| - **[Worker](./worker/README.md)** - Running workflows and activities | ||
| - [Setup](./worker/setup.md) - KWorkerFactory, KWorker, registration | ||
|
|
||
| ### Reference | ||
|
|
||
| - **[Testing](./testing.md)** - Unit testing, mocking activities, time skipping | ||
| - **[Migration Guide](./migration.md)** - Migrating from Java SDK | ||
| - **[API Parity](./api-parity.md)** - Java SDK comparison, gaps, not-needed APIs | ||
|
|
||
| ## Quick Start | ||
|
|
||
| ```kotlin | ||
| // Define activity interface | ||
| @ActivityInterface | ||
| interface GreetingActivities { | ||
| @ActivityMethod | ||
| suspend fun composeGreeting(greeting: String, name: String): String | ||
| } | ||
|
|
||
| // Implement activity | ||
| class GreetingActivitiesImpl : GreetingActivities { | ||
| override suspend fun composeGreeting(greeting: String, name: String): String { | ||
| return "$greeting, $name!" | ||
| } | ||
| } | ||
|
|
||
| // Define workflow interface | ||
| @WorkflowInterface | ||
| interface GreetingWorkflow { | ||
| @WorkflowMethod | ||
| suspend fun getGreeting(name: String): String | ||
| } | ||
|
|
||
| // Implement workflow | ||
| class GreetingWorkflowImpl : GreetingWorkflow { | ||
| override suspend fun getGreeting(name: String): String { | ||
| return KWorkflow.executeActivity( | ||
| GreetingActivities::composeGreeting, | ||
| KActivityOptions(startToCloseTimeout = 10.seconds), | ||
| "Hello", name | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| // Start worker | ||
| val factory = KWorkerFactory(client) | ||
| val worker = factory.newWorker("greetings") | ||
| worker.registerWorkflowImplementationTypes(GreetingWorkflowImpl::class) | ||
| worker.registerActivitiesImplementations(GreetingActivitiesImpl()) | ||
| factory.start() | ||
|
|
||
| // Execute workflow | ||
| val result = client.executeWorkflow( | ||
| GreetingWorkflow::getGreeting, | ||
| KWorkflowOptions(workflowId = "greeting-123", taskQueue = "greetings"), | ||
| "Temporal" | ||
| ) | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| **[Start Review →](./kotlin-idioms.md)** |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| # Activities | ||
|
|
||
| This section covers defining and implementing Temporal activities in Kotlin. | ||
|
|
||
| ## Overview | ||
|
|
||
| Activities are the building blocks for interacting with external systems. The Kotlin SDK provides type-safe activity execution with suspend function support. | ||
|
|
||
| ## Documents | ||
|
|
||
| | Document | Description | | ||
| |----------|-------------| | ||
| | [Definition](./definition.md) | Activity interfaces, typed and string-based execution | | ||
| | [Implementation](./implementation.md) | Implementing activities, KActivity API, heartbeating | | ||
| | [Local Activities](./local-activities.md) | Short-lived local activities | | ||
|
|
||
| ## Quick Reference | ||
|
|
||
| ### Basic Activity | ||
|
|
||
| ```kotlin | ||
| // Define activity interface | ||
| @ActivityInterface | ||
| interface GreetingActivities { | ||
| @ActivityMethod | ||
| suspend fun composeGreeting(greeting: String, name: String): String | ||
| } | ||
|
|
||
| // Implement activity | ||
| class GreetingActivitiesImpl : GreetingActivities { | ||
| override suspend fun composeGreeting(greeting: String, name: String): String { | ||
| return "$greeting, $name!" | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### Calling Activities from Workflows | ||
|
|
||
| ```kotlin | ||
| // Type-safe method reference | ||
| val greeting = KWorkflow.executeActivity( | ||
| GreetingActivities::composeGreeting, | ||
| KActivityOptions(startToCloseTimeout = 30.seconds), | ||
| "Hello", "World" | ||
| ) | ||
|
|
||
| // String-based (for cross-language interop) | ||
| val result = KWorkflow.executeActivity<String>( | ||
| "composeGreeting", | ||
| KActivityOptions(startToCloseTimeout = 30.seconds), | ||
| "Hello", "World" | ||
| ) | ||
| ``` | ||
|
|
||
| ### Key Patterns | ||
|
|
||
| | Pattern | API | | ||
| |---------|-----| | ||
| | Execute activity | `KWorkflow.executeActivity(Interface::method, options, args)` | | ||
| | Execute by name | `KWorkflow.executeActivity<R>("name", options, args)` | | ||
| | Local activity | `KWorkflow.executeLocalActivity(Interface::method, options, args)` | | ||
| | Heartbeat | `KActivity.context.heartbeat(details)` | | ||
|
|
||
| ## Related | ||
|
|
||
| - [Implementation](./implementation.md) - Suspend activity patterns | ||
| - [Local Activities](./local-activities.md) - Short-lived activities | ||
|
|
||
| --- | ||
|
|
||
| **Next:** [Activity Definition](./definition.md) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| # Activity Definition | ||
|
|
||
| ## String-based Activity Execution | ||
|
|
||
| For calling activities by name (useful for cross-language interop or dynamic activity names): | ||
|
|
||
| ```kotlin | ||
| // Execute activity by string name - suspend function, awaits result | ||
| val result = KWorkflow.executeActivity<String>( | ||
| "activityName", | ||
| KActivityOptions( | ||
| startToCloseTimeout = 30.seconds, | ||
| retryOptions = KRetryOptions( | ||
| initialInterval = 1.seconds, | ||
| maximumAttempts = 3 | ||
| ) | ||
| ), | ||
| arg1, arg2 | ||
| ) | ||
|
|
||
| // Parallel execution - use standard coroutineScope { async {} } | ||
| val results = coroutineScope { | ||
| val d1 = async { KWorkflow.executeActivity<String>("activity1", options, arg1) } | ||
| val d2 = async { KWorkflow.executeActivity<String>("activity2", options, arg2) } | ||
| awaitAll(d1, d2) // Returns List<String> | ||
| } | ||
| ``` | ||
|
|
||
| ## Typed Activities | ||
|
|
||
| The typed activity API uses direct method references - no stub creation needed. This approach: | ||
| - Provides full compile-time type safety for arguments and return types | ||
| - Allows different options (timeouts, retry policies) per activity call | ||
| - Works with both Kotlin `suspend` and Java non-suspend activity interfaces | ||
| - Similar to TypeScript and Python SDK patterns | ||
|
|
||
| ```kotlin | ||
| // Define activity interface | ||
| @ActivityInterface | ||
| interface GreetingActivities { | ||
| @ActivityMethod | ||
| suspend fun composeGreeting(greeting: String, name: String): String | ||
|
|
||
| @ActivityMethod | ||
| suspend fun sendEmail(email: Email): SendResult | ||
|
|
||
| @ActivityMethod | ||
| suspend fun log(message: String) | ||
| } | ||
|
|
||
| // In workflow - direct method reference, no stub needed | ||
| val greeting = KWorkflow.executeActivity( | ||
| GreetingActivities::composeGreeting, // Direct reference to interface method | ||
| KActivityOptions(startToCloseTimeout = 30.seconds), | ||
| "Hello", "World" | ||
| ) | ||
|
|
||
| // Different activity, different options | ||
| val result = KWorkflow.executeActivity( | ||
| GreetingActivities::sendEmail, | ||
| KActivityOptions( | ||
| startToCloseTimeout = 2.minutes, | ||
| retryOptions = KRetryOptions(maximumAttempts = 5) | ||
| ), | ||
| ) | ||
|
|
||
| // Void activities work too | ||
| KWorkflow.executeActivity( | ||
| GreetingActivities::log, | ||
| KActivityOptions(startToCloseTimeout = 5.seconds), | ||
| "Processing started" | ||
| ) | ||
| ``` | ||
|
|
||
| ## Type Safety | ||
|
|
||
| The API uses `KFunction` reflection to extract method metadata and provides compile-time type checking: | ||
|
|
||
| ```kotlin | ||
| // Compile error! Wrong argument types | ||
| KWorkflow.executeActivity( | ||
| GreetingActivities::composeGreeting, | ||
| options, | ||
| 123, true // ✗ Type mismatch: expected String, String | ||
| ) | ||
| ``` | ||
|
|
||
| ## Parallel Execution | ||
|
|
||
| Use standard `coroutineScope { async { } }` for concurrent execution: | ||
|
|
||
| ```kotlin | ||
| override suspend fun parallelGreetings(names: List<String>): List<String> = coroutineScope { | ||
| names.map { name -> | ||
| async { | ||
| KWorkflow.executeActivity( | ||
| GreetingActivities::composeGreeting, | ||
| KActivityOptions(startToCloseTimeout = 10.seconds), | ||
| "Hello", name | ||
| ) | ||
| } | ||
| }.awaitAll() // Standard kotlinx.coroutines.awaitAll | ||
| } | ||
|
|
||
| // Multiple different activities in parallel | ||
| val (result1, result2) = coroutineScope { | ||
| val d1 = async { KWorkflow.executeActivity(Activities::operation1, options, arg1) } | ||
| val d2 = async { KWorkflow.executeActivity(Activities::operation2, options, arg2) } | ||
| awaitAll(d1, d2) | ||
| } | ||
| ``` | ||
|
|
||
| > **Note:** We use standard Kotlin `async` instead of a custom `startActivity` method. The workflow's deterministic dispatcher ensures correct replay behavior. | ||
|
|
||
| ## Java Activity Interoperability | ||
|
|
||
| Method references work regardless of whether the activity is defined in Kotlin or Java: | ||
|
|
||
| ```kotlin | ||
| // Java activity interface works seamlessly | ||
| // public interface JavaPaymentActivities { | ||
| // PaymentResult processPayment(String orderId, BigDecimal amount); | ||
| // } | ||
|
|
||
| val result: PaymentResult = KWorkflow.executeActivity( | ||
| JavaPaymentActivities::processPayment, | ||
| KActivityOptions(startToCloseTimeout = 2.minutes), | ||
| orderId, amount | ||
| ) | ||
| ``` | ||
|
|
||
| ## Activity Execution API | ||
|
|
||
| The `KWorkflow` object provides type-safe overloads using `KFunction` types: | ||
|
|
||
| ```kotlin | ||
| object KWorkflow { | ||
| // 1 argument | ||
|
mfateev marked this conversation as resolved.
|
||
| suspend fun <T, A1, R> executeActivity( | ||
| activity: KFunction2<T, A1, R>, | ||
| options: KActivityOptions, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Arguably options as the trailing argument is clearer to read from a caller POV (i.e. meaning the args right after the function can make sense). Since Kotlin supports named parameters and parameter defaults, I considered the value of just putting all of the activity options as parameters, but it's kinda a bad practice since the list is so large.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| arg1: A1 | ||
| ): R | ||
|
|
||
| // 2 arguments | ||
| suspend fun <T, A1, A2, R> executeActivity( | ||
| activity: KFunction3<T, A1, A2, R>, | ||
| options: KActivityOptions, | ||
| arg1: A1, arg2: A2 | ||
| ): R | ||
|
|
||
| // ... up to 6 arguments | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In newer SDKs that don't have advanced generics for arbitrary arity, we have often just supported overloads with 0 or 1 parameter since we recommend no more than 1, but technically harmless to support multiple here except that we're encouraging multiple.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| // String-based overloads | ||
| suspend inline fun <reified R> executeActivity( | ||
| activityName: String, | ||
| options: KActivityOptions, | ||
| vararg args: Any? | ||
| ): R | ||
| } | ||
| ``` | ||
|
|
||
| ## Related | ||
|
|
||
| - [Local Activities](./local-activities.md) - Short-lived local activities | ||
| - [Workflows](../workflows/README.md) - Calling activities from workflows | ||
|
|
||
| --- | ||
|
|
||
| **Next:** [Activity Implementation](./implementation.md) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why require an interface at all? What we do in newer SDKs is even though we support interfaces (and/or abstract classes and/or never-implemented stubs), we don't require them. I think the only argument would be so it's usable from Java workflows, but at that point, the user can tease out an interface.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree, I've added this option to the proposal.
https://github.com/mfateev/proposals/blob/kotlin-api-review/kotlin/open-questions.md