-
Notifications
You must be signed in to change notification settings - Fork 21
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
Conversation
API document (sdk-api.md): - Kotlin idioms: Duration, null safety, coroutines - Stub-less activity/workflow invocation using method references - String-based activity execution for cross-language interop - KWorkflowHandle<T> and KTypedWorkflowHandle<T, R> for typed operations - SignalWithStart and UpdateWithStart operations - KotlinPlugin for enabling coroutine support - K-prefix naming convention for public types - Complete migration guide from Java SDK Implementation document (sdk-implementation.md): - Unified worker architecture with pluggable WorkflowImplementationFactory - KotlinCoroutineDispatcher for deterministic execution - Reified generics for type-safe activity execution - Return type handling for DataConverter (typeOf<R>()) - Detailed open questions with context and recommendations Proposal overview (sdk-proposal.md): - Updated to reflect coroutines as key idiom - Links to API and implementation docs
- Add Cancellation section to API doc covering cooperative cancellation, parallel execution, detached scopes with NonCancellable, and withTimeout - Resolve Open Question temporalio#2: Kotlin built-in cancellation replaces CancellationScope - Update complete example with cleanup activities and cancellation handling
- Rename KWorkflow.condition to KWorkflow.awaitCondition for clarity - Fix parameter order to be consistent: (method/name, options, args...) - Add implementation plan and detailed phase documents
- Add KWorkflowClient with DSL constructor and suspend functions for startWorkflow, executeWorkflow, signalWithStart, updateWithStart - Add KWorkerFactory that automatically enables KotlinPlugin - Add KChildWorkflowHandle<T, R> for child workflow interaction (signal, cancel, result) via startChildWorkflow and getChildWorkflowHandle - Update implementation-plan.md with Phase 1 completion status - Reorganize Phase 2 to include new client and worker APIs - Remove api-implementation-discrepancies.md (was untracked)
Documents the analysis of Java vs Kotlin SDK API differences: - APIs not needed in Kotlin (delay, cancellation scopes, sync primitives, wrap) - Cancellation support integration with Kotlin coroutines - Remaining gaps organized by priority (search attributes, memo, cron APIs) - KActivityInfo missing fields
- Mark Phase 2 as complete in implementation-plan.md - Add KWorkflow.retry() to implemented APIs - Update sdk-api.md to mark retry as implemented
- Mark KUpdateHandle as implemented in implementation-plan.md - Update sdk-api.md Java SDK API Parity section with accurate status - Most "High Priority" and "Medium Priority" gaps are now implemented - Reorganize into "Implemented APIs" and "Remaining Gaps" sections - Clarify KWorkerFactory vs unified architecture in sdk-implementation.md - Remove duplicate phases section from sdk-proposal.md
- Update implementation-plan.md to reflect KWorker is now part of temporal-kotlin worker package (not testing-only) - KWorkerFactory.newWorker() now returns KWorker - Mark Phase 2 commits (4-6) as complete in test framework plan
- Mark updateWithStart as complete with details on implemented methods - Mark mocking support (3.2) as complete
Documents the implemented updateWithStart API: - KWithStartWorkflowOperation class - KUpdateWithStartOptions data class - withStartWorkflowOperation factory methods - startUpdateWithStart and executeUpdateWithStart methods
- Add Dynamic Handler Registration section with usage examples - Add implemented APIs table for all handler registration methods - Remove dynamic handler registration from Remaining Gaps (now implemented) - Remove updateWithStart from Remaining Gaps (was implemented earlier)
Break down the large sdk-api.md (3,479 lines) into a navigable hierarchy: - README.md: Main entry point with navigation - kotlin-idioms.md: Duration, null safety, property syntax - migration.md: Java SDK migration guide - api-parity.md: Java SDK comparison and gaps - workflows/: Workflow definition, signals/queries/updates, child workflows, timers, cancellation, continue-as-new - activities/: Definition, implementation, local activities, KActivity API - client/: KWorkflowClient, workflow handles, signalWithStart, updateWithStart - worker/: KWorkerFactory, KWorker setup - configuration/: KOptions, data conversion, interceptors - implementation/: Moved internal design docs (sdk-proposal, sdk-implementation, implementation-plan, test-framework-*, suspend-activities-design) All internal links verified and updated.
The type-safe version on KActivityContext is the preferred API: fun <T> getHeartbeatDetails(detailsClass: Class<T>): T? inline fun <reified T> KActivityContext.getHeartbeatDetails(): T?
Heartbeat is a short non-blocking operation, so a single heartbeat() API is sufficient for both sync and suspend activities.
- Single heartbeat() API for both sync and suspend activities - Support for both sync and suspend activity methods
Replace unrelated KActivity usage example with getHeartbeatDetails() which demonstrates nullable return type and elvis operator.
Java clients can call Kotlin suspend workflows using untyped stubs, making the parallel interface pattern unnecessary. Updated docs to show the simpler approach with Java interop via untyped stubs.
- Renamed "Next Steps" sections to "Related" across all docs - Added "Next: <topic>" links for linear document traversal - Reading order follows README structure: Idioms → Config → Workflows → Activities → Client → Worker → Migration → API Parity
…tion, timeouts Added sections for: - Suspend functions (workflow and activity methods) - Coroutines and concurrency (async, awaitAll, coroutineScope) - Cancellation patterns (CancellationException, NonCancellable) - Timeouts (withTimeout, withTimeoutOrNull) - Mapping tables showing Kotlin patterns to Temporal/Java equivalents
- Add opening summary table of Java→Kotlin mappings - Streamline code examples (less verbose) - Improve table descriptions (Purpose instead of Temporal Equivalent) - Add Data Classes section for DTOs with @serializable - Remove redundant Property Syntax section (already in Suspend Functions) - Clean up Null Safety section (remove unrelated type mappings)
| arg1: A1, arg2: A2 | ||
| ): R | ||
|
|
||
| // ... up to 6 arguments |
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.
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.
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.
| // 1 argument | ||
| suspend fun <T, A1, R> executeActivity( | ||
| activity: KFunction2<T, A1, R>, | ||
| options: KActivityOptions, |
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.
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.
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.
| ```kotlin | ||
| // Define activity interface | ||
| @ActivityInterface | ||
| interface GreetingActivities { |
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
| ## Registering Activities | ||
|
|
||
| ```kotlin | ||
| worker.registerActivitiesImplementations(OrderActivitiesImpl(paymentService)) |
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.
Am curious how this works without Kotlin in the Java SDK itself
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.
Sorry, I don't understand the question.
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.
I saw later there is a concept of a "plugin" that you register with a worker if you're using Java interop. Note, for a pure Kotlin experience, we don't have to be subject to Java worker approaches even if it wraps a Java worker. For instance, activities, workflows, Nexus services, etc can be worker options. But I understand not wanting to deviate too far from Java.
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.
Agreed. Updated to follow the Python/.NET pattern where workflows and activities are passed at worker construction time via options. This aligns with newer SDKs and provides immutable, all-in-one-place configuration.
kotlin/worker/README.md
Outdated
| val factory = KWorkerFactory(client) | ||
| val worker = factory.newWorker("task-queue") |
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.
Do we really need a factory/constructor separation here? Is there value in the separate steps, or can one create a worker with all options? Will our choice be limited by what Java does here wrt thread pool reuse across workers?
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.
Java is the only SDK that doesn't rely on global variables to share cache and thread pool. It uses factory for this. If we decide that it is not a good pattern we can go with a simpler user experience
kotlin/client/workflow-client.md
Outdated
| val service = WorkflowServiceStubs.newLocalServiceStubs() | ||
|
|
||
| // Create KWorkflowClient with DSL configuration | ||
| val client = KWorkflowClient(service) { | ||
| setNamespace("default") | ||
| setDataConverter(myConverter) | ||
| } |
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.
In newer SDKs, we found requiring these separate steps unnecessary. Would recommend a static call on KWorkflowClient called connect that accepts an option set that is the union of service stub options and client options.
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.
Accepted as proposed. Updated to use KWorkflowClient.connect(options) pattern following .NET SDK conventions. This is cleaner and more explicit than the Java SDK's multi-step construction pattern.
kotlin/worker/README.md
Outdated
| worker.registerActivitiesImplementations(MyActivitiesImpl()) | ||
|
|
||
| // Start the worker | ||
| factory.start() |
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.
We should discourage the start pattern IMO in favor of a run one. We have learned that start+shutdown can swallow fatal worker-runtime errors (which are of course very rare, but technically can occur).
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.
Accepted with modification. Updated the documentation to recommend factory.run() (which blocks until shutdown) as the primary pattern, rather than discouraging start(). The documentation now shows:
// Recommended: blocks until worker shutdown
factory.run()
// Alternative for programmatic shutdown
factory.start()
// ... later ...
factory.shutdown()This follows the pattern from other SDKs where run() is the simpler/preferred approach for most use cases.
kotlin/worker/setup.md
Outdated
| - Use `KWorker` for pure Kotlin implementations (recommended) | ||
| - Use `Worker` (via `kworker.worker`) when mixing Java and Kotlin workflows/activities on the same worker | ||
|
|
||
| ## KotlinPlugin (For Java Main) |
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.
We already have a concept of "plugin" coming for Java SDK, we need a different name here
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.
I think it should be the same concept.
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.
Makes sense to implement Kotlin support on Java workers as a plugin, though may get a bit confusing to call it KotlinPlugin, assuming there will also be KPlugin for users to implement plugins in Kotlin (granted I can't think of a much better name, maybe KotlinToJavaWorkerPlugin or something)
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.
Good point about naming confusion. Renamed to KotlinJavaWorkerPlugin to make it clear this is for integrating Kotlin coroutine support with Java workers.
kotlin/workflows/cancellation.md
Outdated
| val a = async { KWorkflow.executeActivity(...) } | ||
| val b = async { KWorkflow.executeActivity(...) } | ||
|
|
||
| // If either activity fails, the other is cancelled |
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.
I don't understand this statement exactly. Does it mean "if either activity fails, the workflow fails which implicitly cancels any activities" or is there some kind of cooperative cancellation happening here if a workflow fails/
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.
Accepted as proposed. Clarified the cancellation documentation to note that using coroutineScope and other structured concurrency patterns is standard Kotlin idiom - not a Temporal-specific concept. The documentation now emphasizes that Kotlin's structured concurrency naturally maps to workflow cancellation semantics.
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.
Ah, I see this is the difference between coroutineScope and supervisorScope
GreyTeardrop
left a comment
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.
So excited for Kotlin-first Temporal SDK!
| data class KActivityOptions( | ||
| val startToCloseTimeout: Duration? = null, | ||
| val scheduleToCloseTimeout: Duration? = null, | ||
| val scheduleToStartTimeout: Duration? = null, | ||
| val heartbeatTimeout: Duration? = null, | ||
| val taskQueue: String? = null, | ||
| val retryOptions: KRetryOptions? = null, | ||
| val cancellationType: ActivityCancellationType? = null, // Java default: TRY_CANCEL | ||
| val disableEagerExecution: Boolean = false, | ||
| // Experimental | ||
| @Experimental val summary: String? = null, | ||
| @Experimental val priority: Priority? = null | ||
| ) { |
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.
I believe Kotlin team doesn't recommend using data classes as part of library APIs as adding a new field (even an optional one) is always a binary breaking change. I see that some libraries like Jackson started from constructors with named parameters and later deprecated them in favor of a DSL/builder combo.
class KActivityOptions
private constructor(builder: KActivityOptions.Builder) {
val startToCloseTimeout: Duration?
// ...
init {
startToCloseTimeout = builder.startToCloseTimeout
// …
}
class Builder {
var startToCloseTimeout: Duration? = null
// ...
fun build(): KActivityOptions {
require(startToCloseTimeout != null || scheduleToCloseTimeout != null) {
"At least one of startToCloseTimeout or scheduleToCloseTimeout must be specified"
}
return KActivityOptions(this)
}
}
}
inline fun KActivityOptions(init: KActivityOptions.Builder.() -> Unit): KActivityOptions {
return KActivityOptions.Builder().apply(init).build()
}which gives a ABI-safe equivalent for data class named constructor parameters
val options = KActivityOptions {
startToCloseTimeout = 10.minutes
// …
}(if necessary, copy can be implemented the same way).
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.
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.
After thinking about this, I don't understand the value of binary compatibility. Temporal SDK never guaranteed binary compatibility between versions. These structures are never sent over the wire.
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.
The primary use case for keeping ABI compatibility is for libraries that might be built on top of the SDK. Without it, if there's a library built against Kotlin SDK vX, it can't be used with a different version of the SDK, even if that version only has changes that could've been source-compatible, e.g., adding a new optional field to one of the data classes.
If the Kotlin SDK is only meant to be used by direct consumers, that shouldn't matter - consumer code is almost always built with the version of libraries it's supposed to run with, but unstable ABI can be an issue for wider ecosystem around the SDK.
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.
True, and would totally support non-data for options classes for this reason, but at this point, does that mean basically no data classes should be used by libraries ever that may get new fields, only end application, due to this constraint?
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.
It should be fine to use data classes internally, but not as part of public API surface - at least that's what JetBrains suggests (https://kotlinlang.org/docs/api-guidelines-backward-compatibility.html#avoid-using-data-classes-in-your-api).
Add "Decision Needed" sections to activity and workflow definition docs proposing Python SDK-style interfaceless definitions where activities and workflows can be defined directly on implementation classes without requiring separate interfaces.
- Create open-questions.md to centralize API design decisions - Add link to open questions in main README reference section - Link activity and workflow definition docs to full discussion
Document the process for adding API design questions that need discussion before implementation, including format for the central open-questions.md and inline sections in proposal docs.
Propose using typed KArgs classes instead of varargs for compile-time type safety when invoking activities with multiple arguments.
Include examples for activities, child workflows, and client workflow execution with 0, 1, and multiple arguments.
- Option A: Keep current varargs (no type safety) - Option B: Direct overloads (0-7 arguments, no wrapper) - Option C: KArgs wrapper classes (fewer overloads) Include comparison table showing trade-offs between options.
- Add open questions document covering interfaceless workflows/activities, type-safe activity arguments, and data classes vs builder+DSL pattern - Refine API documentation with Kotlin idioms (suspend functions, properties) - Add testing documentation and implementation design docs - Expand migration guide with comprehensive API mapping - Various documentation fixes and improvements
- Remove priority (not in Java) - Add scheduleToStartTimeout - Add doNotIncludeArgumentsIntoMarker
Key changes: - Add KWorkflow.delay() with optional KTimerOptions - Rename typedSearchAttributes to searchAttributes throughout - Rename getWorkflowHandle() to workflowHandle() with result type overload - Add KWorkflowClient.connect(options) pattern - Update handle type hierarchy: KWorkflowHandleUntyped -> KWorkflowHandle<T> -> KWorkflowHandleWithResult<T, R> - Add KStartUpdateOptions/KUpdateOptions with waitForStage required - Add KWorkflowExecutionInfo base class for listWorkflows() - Add KWorkflowClientInterceptor for client-side interception - Clarify cancellation docs re: standard Kotlin behavior - Recommend factory.run() pattern over start()
| ### Benefits of Builder+DSL | ||
|
|
||
| - Adding new optional properties to the `Builder` class is binary compatible | ||
| - The inline factory function provides the same ergonomic DSL syntax as data class constructors |
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.
Theoretically, DSL syntax can even provide slightly better ergonomics. E.g., in KActivityOptions case, retry options can be specified as
val options = KActivityOptions {
startToCloseTimeout = 10.minutes
retryOptions = KRetryOptions {
initialInterval = 10.seconds
backoffCoefficient = 1.5
}
}but with additional method like
inline fun KActivityOptions.Builder.retryOptions(init: KRretryOptions.Builder.() -> Unit) {
this.retryOptions = KRetryOptions(init)
}that can become
val options = KActivityOptions {
startToCloseTimeout = 10.minutes
retryOptions {
initialInterval = 10.seconds
backoffCoefficient = 1.5
}
}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.
Great point! I added your proposal to the open-questions.md document.
Add .gitignore to prevent IDE and tool-specific files from being committed.
mfateev
left a comment
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.
Responses to review comments
| data class KActivityOptions( | ||
| val startToCloseTimeout: Duration? = null, | ||
| val scheduleToCloseTimeout: Duration? = null, | ||
| val scheduleToStartTimeout: Duration? = null, | ||
| val heartbeatTimeout: Duration? = null, | ||
| val taskQueue: String? = null, | ||
| val retryOptions: KRetryOptions? = null, | ||
| val cancellationType: ActivityCancellationType? = null, // Java default: TRY_CANCEL | ||
| val disableEagerExecution: Boolean = false, | ||
| // Experimental | ||
| @Experimental val summary: String? = null, | ||
| @Experimental val priority: Priority? = null | ||
| ) { |
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.
kotlin/open-questions.md
Outdated
| 1. **Replay Safety** - If default values change between deployments, replayed workflows behave differently, violating determinism | ||
| 2. **Serialization Ambiguity** - Unclear whether the caller serializes defaults or the worker applies them | ||
| 3. **Cross-Language Compatibility** - Other languages calling the activity/workflow don't know about Kotlin defaults | ||
| 4. **SDK Consistency** - Python SDK explicitly disallows default parameters; Go/Java don't have them |
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.
While I agree with the decision to disallow because it can be easily walked back if we change our minds, it may be worth noting that IIRC, Python, .NET, and Ruby all support default parameters. Here is a Python test confirming it: https://github.com/temporalio/sdk-python/blob/993de6d0e9b42bb01f24edfdb46e0795b00debcf/tests/worker/test_workflow.py#L3502-L3545.
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.
Good point about other SDKs supporting defaults. Given we're moving to 0-1 parameters (with >1 falling back to untyped), supporting a default for a single parameter is straightforward and aligns with Python/.NET/Ruby. Updated the docs to allow defaults for the 0-1 argument case.
|
|
||
| - Different from Java SDK convention | ||
| - Activity/workflow type names derived from method/class names (convention-based) | ||
| - Respects `@ActivityMethod(name = "...")` and `@WorkflowMethod(name = "...")` annotations if present |
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.
Note, in order to support this in some newer SDKs and not hit diamond problems or other inheritance confusion for when they do want to have interfaces/abstracts, we require every overridden method to have the same matching annotation/attribute/decorator. This prevents ambiguity, is easy to understand, and clarifies author intent clearly to reader without relying on inheritance (at the small cost of having to write duplicate annotations on overrides).
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.
While this approach makes sense for newer SDKs starting fresh, requiring duplicate annotations would break backward compatibility with existing Java activities/workflows and feel unnatural to Java SDK users. Since Kotlin SDK builds on Java SDK, we'll maintain the current convention where annotations on the interface are sufficient.
|
|
||
| --- | ||
|
|
||
| ### Option B: Direct Overloads (0-7 Arguments) |
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.
Or "0-1 Argument(s)", meaning any > 1 argument, which Temporal discourages, can fall back to untyped. This is what we do in some newer SDKs.
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.
Follow-up question: Why do you prefer the type-unsafe fallback for >1 args instead of the KArgs approach which maintains type safety? Is API simplicity the main driver, or are there other considerations?
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.
API simplicity (and overload count) was my main driver in preferring just falling back to untyped, but in some SDKs (namely Python), we actually do support arbitrary multi-param typed as a different overload. Would be fine if we did that here too. And really, with how .NET is lambda expressions, it supports multi-param typed as well. So overall, yeah, ok w/ a typed multi-param, and having 0 or 1 be even simpler forms of that.
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.
Thanks for confirming. We'll go with the KArgs approach (Option C) - provides full type safety for all arities while keeping 0-1 argument cases simple. The kargs() wrapper for 2+ args is a small price for compile-time type checking.
| val options = KActivityOptions(startToCloseTimeout = 10.minutes) | ||
| ``` | ||
|
|
||
| However, the Kotlin team doesn't recommend using data classes as part of library APIs because adding a new field (even an optional one) is always a **binary breaking change**. This happens because: |
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.
Hrmm, not sure we have ever considered binary breaking changes to be breaking changes from our POV (i.e. we expect you to compile against the same JAR you'll run with). Having said that, definitely not against better options patterns. But I don't think we want to discourage e.g. users from using data classes for their need-backwards-compatibility models.
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.
Good point. We won't discourage users from using data classes for their models that need backwards compatibility. The concern about binary breaking changes in the context of SDK options classes is less relevant since users compile against the SDK version they run with.
| ## Registering Activities | ||
|
|
||
| ```kotlin | ||
| worker.registerActivitiesImplementations(OrderActivitiesImpl(paymentService)) |
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.
I saw later there is a concept of a "plugin" that you register with a worker if you're using Java interop. Note, for a pure Kotlin experience, we don't have to be subject to Java worker approaches even if it wraps a Java worker. For instance, activities, workflows, Nexus services, etc can be worker options. But I understand not wanting to deviate too far from Java.
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.
Not understanding "push" cancellation, but yeah so long as I can have heartbeat infallible and cancellation represented as traditional Kotlin coroutine cancellation, I think that is ideal. Arguably both of those could be the default, but I understand if it is confusing to have suspend fun do something completely different than non-suspend fun (this is a struggle we run into w/ Python where cancellation is represented quite differently w/ async def vs just def, but not this differently, heartbeat remained infallible on both).
| ```kotlin | ||
| @ActivityInterface | ||
| interface ValidationActivities { | ||
| suspend fun validate(input: String): Boolean |
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.
Makes sense, was just a bit strange to see it present inconsistently sometimes even w/out name customization in this proposal
kotlin/api-parity.md
Outdated
|
|
||
| | Java SDK API | Reason Not Needed | | ||
| |--------------|-------------------| | ||
| | `Workflow.sleep(Duration)` | Use `kotlinx.coroutines.delay()` - intercepted by dispatcher | |
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.
Using existing async utilities makes sense, but still may need some workflow-specific alternatives for advanced users, such as being able to provide a timer summary for sleep.
kotlin/worker/setup.md
Outdated
| - Use `KWorker` for pure Kotlin implementations (recommended) | ||
| - Use `Worker` (via `kworker.worker`) when mixing Java and Kotlin workflows/activities on the same worker | ||
|
|
||
| ## KotlinPlugin (For Java Main) |
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.
Makes sense to implement Kotlin support on Java workers as a plugin, though may get a bit confusing to call it KotlinPlugin, assuming there will also be KPlugin for users to implement plugins in Kotlin (granted I can't think of a much better name, maybe KotlinToJavaWorkerPlugin or something)
kotlin/workflows/cancellation.md
Outdated
| val a = async { KWorkflow.executeActivity(...) } | ||
| val b = async { KWorkflow.executeActivity(...) } | ||
|
|
||
| // If either activity fails, the other is cancelled |
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.
Ah, I see this is the difference between coroutineScope and supervisorScope
- Q1: Allow default parameters for 0-1 argument case (align with Python/.NET/Ruby) - Q5: Follow Python/.NET pattern for worker options (pass workflows/activities at construction) - Q6: Heartbeat infallible, cancellation via coroutine or CompletableFuture - Q7: Only show annotations when customizing names (consistency cleanup) - Q8: Add KWorkflow.delay() with and without summary parameter - Q9: Rename KotlinPlugin to KotlinJavaWorkerPlugin
kotlin/client/workflow-client.md
Outdated
| @@ -0,0 +1,149 @@ | |||
| # Workflow Client | |||
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.
Forgot to mention this, but in newer SDKs, we found just having one big client that is a workflow client + schedule client (w/ features to make an async activity completion handle) is a bit cleaner from an options POV. This will also help when standalone activity client and Nexus operation client come about. No need to change though if we don't want, there is also value in matching what Java does (though you will duplicate a lot of these client options each time).
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.
Agreed. We'll use a unified KClient matching the Python/.NET pattern - single client covering workflows, schedules, async activity completion, and ready for future Nexus support. Cleaner options and less duplication.
- Q3: Use KArgs approach for type-safe activity/workflow arguments - Q11: Heartbeat throws CancellationException on cancellation (prevents slot eating) - Q12: Unified KClient matching Python/.NET pattern (workflows, schedules, async completion) - R1-Q5: Use KWorkflow.version() method instead of getter-style Key changes: - Rename KWorkflowClient to KClient throughout - Add schedule and async activity completion APIs to KClient - Update heartbeat to throw on cancellation, add TODO for cancellationFuture - Mark KArgs (Option C) as decided in open-questions.md
- Use KActivityContext.current() pattern instead of KActivity (Q20) - Remove KActivity.logger()/KWorkflow.logger(), use MDC with standard logging (Q21) - Keep single-argument heartbeat, rename to lastHeartbeatDetails<T>() (Q22) - Document event loop implementation on top of Java SDK (Q24) Updated files: - activities/implementation.md - activities/local-activities.md - workflows/definition.md - configuration/interceptors.md - migration.md - implementation/implementation-plan.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.
From my POV, this proposal is definitely good enough from a proposal POV (and is much more complete than past SDK lang proposals). So can merge whenever IMO, but may want more feedback.
Note, in all SDKs, the implementation deviates from the proposal as we learn things, so we've never expected/required 100% proposal accuracy. Also, not sure if it is part of the proposal to determine where this will live (I prefer https://github.com/temporalio/sdk-java/tree/master/temporal-kotlin myself where it can be versioned/published alongside the Java SDK dependency and even use internal packages from it if we wanted).
Following Python SDK pattern, KWorker now has: - run() - recommended, blocks until shutdown, propagates fatal errors - start()/shutdown() - for advanced manual control scenarios Updated examples to use run() as the primary pattern.
WARNING
The Kotlin SDK is an experiment in using LLMs for both design and implementation.
Summary
This proposal defines the public API for the Temporal Kotlin SDK, providing an idiomatic Kotlin experience using coroutines and suspend functions.
Formatted reading: https://github.com/mfateev/proposals/tree/kotlin-api-review/kotlin
Start at the README and click "Next" at the bottom of each document to navigate through all 27 files in order.
Key Features
suspend fun30.seconds)Documentation Structure