Skip to content

v2.6.0

Latest

Choose a tag to compare

@slinkydeveloper slinkydeveloper released this 03 Feb 12:59
· 1 commit to main since this release

Release 2.6.0

We're excited to announce Restate Java SDK 2.6.0!

Virtual Threads by default (Java 21+)

The default executor for Java services now uses virtual threads on Java 21+, falling back to Executors.newCachedThreadPool() for older Java versions.

You can still customize the executor providing HandlerRunner.Options.withExecutor() to Endpoint#bind, or for Spring users, use the new Spring properties (restate.executor or restate.components.<name>.executor).

Note: This change only applies to Java services. Kotlin services continue to Dispatchers.Default as default coroutine dispatcher.

[Spring Boot] Configure services using Spring's Properties

You can now configure Restate services directly from Spring's application.properties or application.yml files. This provides a centralized way to configure service behavior without modifying code.

Example Configuration

# Global executor for all services (Java only)
restate.executor=myExecutorBean

# Configuration for a service named "MyService"
restate.components.MyService.executor=myServiceExecutor
restate.components.MyService.inactivity-timeout=10m
restate.components.MyService.abort-timeout=1m
restate.components.MyService.idempotency-retention=7d
restate.components.MyService.journal-retention=1d
restate.components.MyService.ingress-private=false
restate.components.MyService.enable-lazy-state=true
restate.components.MyService.documentation=My service description
restate.components.MyService.metadata.version=1.0
restate.components.MyService.metadata.team=platform
restate.components.MyService.retry-policy.initial-interval=100ms
restate.components.MyService.retry-policy.exponentiation-factor=2.0
restate.components.MyService.retry-policy.max-interval=10s
restate.components.MyService.retry-policy.max-attempts=10
restate.components.MyService.retry-policy.on-max-attempts=PAUSE

# Per-handler configuration
restate.components.MyService.handlers.myHandler.inactivity-timeout=5m
restate.components.MyService.handlers.myHandler.ingress-private=true
restate.components.MyService.handlers.myHandler.documentation=Handler description
restate.components.MyService.handlers.myWorkflowHandler.workflow-retention=30d

Annotation based configuration

The configuration attribute on @RestateService, @RestateVirtualObject, and @RestateWorkflow annotations now accepts a bean name of type RestateComponentProperties (previously RestateServiceConfigurator):

@RestateService(configuration = "myServiceConfig")
public class MyService {
    // ...
}

@Bean
public RestateComponentProperties myServiceConfig() {
    return new RestateComponentProperties()
        .setInactivityTimeout(Duration.ofMinutes(10))
        .setDocumentation("My service");
}

[Kotlin] New experimental API

Following the introduction of the new experimental API for Java in v2.5.0, we're now bringing the same experience to Kotlin!

The new API removes the need for the KSP code generator, making it significantly simpler to use the Restate SDK:

  • No KSP code generator required: The SDK now uses reflection to discover and bind your services, eliminating the need for build-time code generation and making your build simpler and faster.
  • No more Context parameters: Handler methods no longer need to accept a Context parameter. Instead, use top-level functions from dev.restate.sdk.kotlin to access state, promises, and other Restate functionality.
  • Cleaner method signatures: Your handler methods now have cleaner signatures that focus on your business logic rather than Restate infrastructure.

The new API is opt-in and can be incrementally adopted in an existing project, meaning the existing API will continue to work as usual.
It is marked as experimental and subject to change in the next releases.

We're looking for your feedback to evolve it and stabilize it!

How to use it

  1. Remove the KSP code generator dependency sdk-api-kotlin-gen

  2. Remove the Context, ObjectContext, SharedObjectContext, WorkflowContext, SharedWorkflowContext parameters from your @Handler annotated methods. For example:

    @VirtualObject
    class Counter {
    
      @Handler
      suspend fun add(ctx: ObjectContext, value: Long) {}
    
      @Shared
      @Handler
      suspend fun get(ctx: SharedObjectContext): Long {}
    }

    Becomes:

    import dev.restate.sdk.kotlin.*
    
    @VirtualObject
    class Counter {
    
      @Handler
      suspend fun add(value: Long) {}
    
      @Shared
      @Handler
      suspend fun get(): Long {}
    }

    The same applies for interfaces using Restate annotations.

  3. Replace all the usages of ctx. with the top-level functions. For example:

    @Handler
    suspend fun add(ctx: ObjectContext, value: Long) {
        val currentValue = ctx.get(TOTAL) ?: 0L
        val newValue = currentValue + value
        ctx.set(TOTAL, newValue)
    }

    Becomes:

    @Handler
    suspend fun add(value: Long) {
        val state = state()
        val currentValue = state.get(TOTAL) ?: 0L
        val newValue = currentValue + value
        state.set(TOTAL, newValue)
    }
  4. Replace all the usages of code-generated clients. There are two ways to invoke services:

    Simple proxy (for direct calls):

    The top-level functions let you create proxies to call services directly using service<T>(), virtualObject<T>(key) and workflow<T>(key):

    virtualObject<Counter>("my-key").add(1) // Direct method call

    Handle-based (for advanced patterns):

    For asynchronous handling, request composition, or invocation options (such as idempotency keys), use toService<T>(), toVirtualObject<T>(key) and toWorkflow<T>(key):

    // Use call() with a lambda to return a DurableFuture you can await asynchronously and/or compose with other futures
    val count = toVirtualObject<Counter>("my-counter")
        .request { add(1) }
        .call()
        .await()
    
    // Use send() for one-way invocation without waiting
    val handle = toVirtualObject<Counter>("my-counter")
        .request { add(1) }
        .send()
    
    // Add request options such as idempotency key
    val count = toVirtualObject<Counter>("my-counter")
        .request { add(1) }
        .options { idempotencyKey = "my-idempotency-key" }
        .call()
        .await()

Reference sheet

Context API (2.5.x) New reflection API (2.6.0)
ctx.runBlock { ... }/ctx.runAsync { ... } runBlock { ... }/runAsync { ... }
ctx.random() random()
ctx.timer() timer()
ctx.awakeable() awakeable<T>()
ctx.get(key) / ctx.set(key, value) state().get(key) / state().set(key, value)
ctx.promise(key) promise(key)
Code generated clients (Services) service<T>() / toService<T>()
Code generated clients (Virtual Objects) virtualObject<T>(key) / toVirtualObject<T>(key)
Code generated clients (Workflows) workflow<T>(key) / toWorkflow<T>(key)

Using concrete classes with proxy clients

By default, Kotlin classes are final, preventing libraries like Restate to generate runtime client proxies.

We generally suggest one of the following three options:

  • Define an interface with all Restate annotations and have your class implement it. Use the interface type for proxy calls, e.g. service<MyServiceInterface>().
  • Use open in all Restate annotated classes/methods.
  • Setup the Kotlin allopen compiler plugin with the following configuration:

Gradle (Kotlin DSL)

plugins {
    kotlin("plugin.allopen") version "<kotlin-version>"
}

allOpen {
    annotations("dev.restate.sdk.annotation.Service", "dev.restate.sdk.annotation.VirtualObject", "dev.restate.sdk.annotation.Workflow")
}

This configuration automatically makes any class annotated with Restate annotations open, along with all their methods.

Gradual migration

You can gradually migrate to the new API by disabling the KSP code generator for specific classes.
For example, if you have a project with a my.example.Greeter and a my.example.Counter, you can decide to migrate only my.example.Counter to use the new API.

To do so, keep the KSP code generator dependency and pass the KSP option dev.restate.codegen.disabledClasses as follows:

Gradle (Kotlin DSL)

ksp {
  val disabledClassesCodegen =
    listOf(
      // Ignore Counter in the KSP code generator
      "my.example.Counter")

  arg("dev.restate.codegen.disabledClasses", disabledClassesCodegen.joinToString(","))
}

New API for Deterministic Time

We've added a new API to get the current time deterministically that's consistent across replays.

Java

import java.time.Instant;

@Handler
public String myHandler() {
    Instant now = Restate.instantNow();
    // or using the context-based API:
    // Instant now = ctx.instantNow();
    return "Current time: " + now;
}

Kotlin

import kotlin.time.Clock
import kotlin.time.ExperimentalTime
import dev.restate.sdk.kotlin.*

@OptIn(ExperimentalTime::class)
@Handler
suspend fun myHandler(): String {
    val now = Clock.Restate.now()
    return "Current time: $now"
}

Note: in Kotlin you need to opt in the Kotlin experimental time APIs.

Other changes

  • Fix detection of @Raw annotation in Java Reflection API
  • Serde.RAW.contentType() now correctly returns application/octet-stream
  • Dependency updates:
    • Jackson 2.19.4
    • Spring Boot 3.4.13
    • Vert.x 4.5.24
    • OpenTelemetry 1.58.0
    • JUnit 5.14.1

Full Changelog: v2.5.0...v2.6.0