mit einem Klick
translate-from-shared-core
// Translate Rust changes from restatedev/sdk-shared-core into equivalent Java code in this repo. Use when the user mentions translating, porting, or syncing commits from sdk-shared-core.
// Translate Rust changes from restatedev/sdk-shared-core into equivalent Java code in this repo. Use when the user mentions translating, porting, or syncing commits from sdk-shared-core.
| name | translate-from-shared-core |
| description | Translate Rust changes from restatedev/sdk-shared-core into equivalent Java code in this repo. Use when the user mentions translating, porting, or syncing commits from sdk-shared-core. |
| argument-hint | <commit-sha or range e.g. abc123 or abc123..def456> |
You are translating Rust code changes from restatedev/sdk-shared-core into equivalent Java code in this repository.
$ARGUMENTS contains one or more commit references from sdk-shared-core. These can be:
abc123)abc123 def456)abc123..def456)For each commit or range, fetch the diff from GitHub:
gh api repos/restatedev/sdk-shared-core/commits/<sha> --header "Accept: application/vnd.github.diff"gh api repos/restatedev/sdk-shared-core/compare/<base>...<head> --header "Accept: application/vnd.github.diff"Also fetch the commit message(s) for context:
gh api repos/restatedev/sdk-shared-core/commits/<sha> --jq '.commit.message'Before translating, analyze:
The primary mapping target is:
sdk-core/src/main/java/dev/restate/sdk/core/statemachine/
The two codebases implement the same state machine but with fundamentally different architectural patterns. Read the existing Java code before making changes. Don't blindly transliterate — adapt to the Java architecture while preserving semantics.
VM trait in src/lib.rs with sys_* methods (e.g., sys_state_get, sys_call, sys_run)StateMachine interface in StateMachine.java with plain method names (e.g., stateGet, call, run)NotificationHandle in Rust, int in Java) for async operationsdoProgress() / do_progress() as the core async drivertakeNotification() / take_notification() for retrieving resultsThis is the biggest architectural difference.
Rust: States are variants of a single State enum in src/vm/mod.rs. Each variant carries its own data inline:
enum State {
WaitingStart,
WaitingReplayEntries { received_entries: u32, commands: VecDeque<RawMessage>, async_results: AsyncResultsState },
Replaying { commands: VecDeque<RawMessage>, run_state: RunState, async_results: AsyncResultsState },
Processing { processing_first_entry: bool, run_state: RunState, async_results: AsyncResultsState },
Closed,
}
Java: States are classes implementing a sealed State interface. Each state class contains its own data as fields:
WaitingStartState → WaitingReplayEntriesState → ReplayingState → ProcessingState → ClosedStateState interface declares default methods that throw ProtocolException.badState() for unsupported operationsRust: Transitions are structs (e.g., NewMessage, SysGetState, DoProgress) that implement Transition<Context, Event> or TransitionAndReturn<Context, Event> for State. Inside each impl, you pattern match on the current state:
impl TransitionAndReturn<Context, PopJournalEntry<M>> for State {
fn transition_and_return(self, context: &mut Context, event) -> Result<(Self, Output), Error> {
match self {
State::Replaying { mut commands, run_state, async_results } => { ... }
State::Processing { ... } => { ... }
s => Err(s.as_unexpected_state(...))
}
}
}
Transitions live in separate modules: transitions/input.rs, transitions/journal.rs, transitions/async_results.rs, transitions/terminal.rs.
Java: Transitions are methods on the state classes themselves. Each state class implements the transitions it supports:
// In ReplayingState.java
int processStateGetCommand(String key, StateContext ctx) { ... }
// In ProcessingState.java
int processStateGetCommand(String key, StateContext ctx) { ... }
The StateMachineImpl delegates to the current state: this.stateContext.getCurrentState().processStateGetCommand(key, this.stateContext).
Key implication: When a Rust commit adds or modifies a transition struct, in Java you need to add or modify the corresponding method across multiple state classes (typically ReplayingState and ProcessingState).
Rust: CoreVM.do_transition(event) uses mem::replace to extract the current state, calls the transition, and stores the new state. Errors automatically send an error message and close output.
Java: StateHolder.transition(newState) simply swaps the current state reference. Error handling is done explicitly in state methods. StateContext acts as the central hub holding StateHolder, Journal, EagerState, etc.
Context in src/vm/context.rs — holds start_info, journal, output, eager_state, input_is_closedStateContext in StateContext.java — holds StateHolder, Journal, EagerState, StartInfo, inputClosed, outputSubscriberThese are structurally very similar between both codebases:
Journal tracks commandIndex, notificationIndex, completionIndex, signalIndexAsyncResultsState maps handles to NotificationIds, with toProcess queue and ready mapRunState tracks pending/executing run blocksEagerState caches state values from StartMessage| Rust file | Java file |
|---|---|
src/lib.rs (VM trait) | StateMachine.java |
src/vm/mod.rs (CoreVM) | StateMachineImpl.java |
src/vm/context.rs (Context) | StateContext.java, Journal.java, EagerState.java, StartInfo.java |
src/vm/context.rs (AsyncResultsState) | AsyncResultsState.java |
src/vm/context.rs (RunState) | RunState.java |
src/vm/transitions/input.rs | Logic in WaitingStartState.java, WaitingReplayEntriesState.java |
src/vm/transitions/journal.rs | Methods across ReplayingState.java and ProcessingState.java |
src/vm/transitions/async_results.rs | Methods in ReplayingState.java, ProcessingState.java, AsyncResultsState.java |
src/vm/transitions/terminal.rs | hitError()/hitSuspended() methods on State interface |
src/vm/errors.rs | ProtocolException.java (factory methods, not separate error classes) |
src/error.rs (CommandMetadata, NotificationMetadata) | CommandMetadata.java (record); notification metadata is built as strings inline |
src/service_protocol/ | MessageDecoder.java, MessageEncoder.java, MessageType.java, ServiceProtocol.java |
Both codebases distinguish two kinds of commands:
stateSet, stateClear — no handle returnedstateGet, call, run — returns an int handle mapped to a NotificationIdIn Rust, these are generic transitions like SysNonCompletableEntry<M> and SysCompletableEntry<M>.
In Java, these are processNonCompletableCommand() and processCompletableCommand() methods on the state classes.
Rust tests are low-level, directly exercising the VM:
VMTestCase::new()
.input(StartMessage { known_entries: 1, .. })
.input(input_entry_message(b"my-data"))
.run(|vm| {
let input = vm.sys_input().unwrap();
vm.sys_write_output(NonEmptyValue::Success(input), ...).unwrap();
vm.sys_end().unwrap();
});
Java tests use a 3-layer architecture:
StateTestSuite, CallTestSuite) define test scenarios as TestDefinition streamsStateTest) extend suites with actual handler code using the SDK's context APIMockRequestResponse, MockBidiStream) run each test in both buffered and streaming modesJava test inputs are built with ProtoUtils helpers (startMessage(), inputCmd(), getLazyStateCmd(), etc.) and assertions use AssertUtils.
Key implication: When a Rust commit adds a new VM-level test, in Java you typically need to add a handler-level test in the appropriate test suite, not a direct state machine test.
When translating Rust VM tests to Java:
Identify the right test suite: Match the Rust test module to the Java abstract test suite:
src/tests/failures.rs (journal_mismatch) → StateMachineFailuresTestSuitesrc/tests/async_result.rs → AsyncResultTestSuitesrc/tests/run.rs → SideEffectTestSuitesrc/tests/state.rs → StateTestSuite / EagerStateTestSuiteAdd abstract method + test definition: Add the abstract handler method to the suite, then add test definitions using withInput(...) and assertion patterns like assertingOutput(containsOnly(errorMessage(...))) or expectingOutput(...).
Implement in both Java and Kotlin: The suite is extended in both:
sdk-core/src/test/java/dev/restate/sdk/core/javaapi/<TestName>.javasdk-core/src/test/kotlin/dev/restate/sdk/core/kotlinapi/<TestName>.ktKotlin API differences:
import dev.restate.sdk.kotlin.* for reified extension functions (runAsync, runBlock, etc.)ctx.runAsync<String>(name) { ... } (reified, not ctx.runAsync(name, String.class, ...))ctx.awakeable(TestSerdes.STRING) (needs a serde, not String::class.java)ctx.timer(0.milliseconds) (uses Kotlin Duration)testDefinitionForService<Unit, String?>("Name") { ctx, _: Unit -> ... }Cancel signal is always included: The HandlerContextImpl automatically appends CANCEL_HANDLE (handle=1, mapping to SignalId(1)) to every doProgress call. This matches Rust's CoreVM.do_progress which appends cancel_signal_handle. So in test assertions, the cancel signal notification ID will always be part of the awaited notifications.
ProtoUtils helpers: Use startMessage(n), inputCmd(), runCmd(completionId, name), suspensionMessage(completionIds...), etc. For messages without helpers (e.g., SleepCommandMessage, SleepCompletionNotificationMessage), build them directly with the protobuf builders.
Util.awakeableIdStr(invocationId, signalId) — computes the awakeable ID string from invocation ID and signal ID. Used in both StateMachineImpl (for creating awakeables) and ReplayingState (for error messages).StateMachineImpl.CANCEL_SIGNAL_ID — the signal ID for the built-in cancel signal (value: 1). Package-private, available via static import.case Type t ->) in Java source; use instanceof chains instead../gradlew :sdk-core:compileJavaAfter translating, provide: