Skip to content

Commit aa1f7d5

Browse files
slop slop
1 parent 46df47e commit aa1f7d5

9 files changed

Lines changed: 790 additions & 764 deletions

File tree

sdk-core/src/main/java/dev/restate/sdk/core/HandlerContextImpl.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ class HandlerContextImpl implements HandlerContextInternal {
6868
new HandlerRequest(
6969
new InvocationIdImpl(input.invocationId(), input.randomSeed()),
7070
otelContext,
71-
Slice.wrap(input.input()),
71+
input.input(),
7272
input.headersAsMap(),
7373
serviceName,
7474
handlerName);
@@ -456,7 +456,11 @@ private void pollAsyncResultInner(AsyncResultInternal<?> asyncResult) {
456456
try {
457457
response = this.stateMachine.doAwait(future);
458458
} catch (Throwable e) {
459-
this.failWithoutContextSwitch(e);
459+
// doAwait sneaky-throws AbortedExecutionException on suspension. In this case, no need to
460+
// fail twice.
461+
if (!ExceptionUtils.containsAbortedExecutionException(e)) {
462+
this.failWithoutContextSwitch(e);
463+
}
460464
asyncResult.publicFuture().completeExceptionally(AbortedExecutionException.INSTANCE);
461465
return;
462466
}

sdk-core/src/main/java/dev/restate/sdk/core/statemachine/JavaStateMachine.java

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
import dev.restate.common.Target;
1818
import dev.restate.sdk.common.RetryPolicy;
1919
import dev.restate.sdk.common.TerminalException;
20-
import dev.restate.sdk.core.ExceptionUtils;
2120
import dev.restate.sdk.core.ProtocolException;
2221
import dev.restate.sdk.core.generated.protocol.Protocol;
2322
import dev.restate.sdk.endpoint.HeadersAccessor;
@@ -183,7 +182,8 @@ public AwaitResult doAwait(UnresolvedFuture future) {
183182
}
184183
AwaitResult resolve = doProgress(List.of(tracked.handle));
185184
if (!(resolve instanceof AwaitResult.AnyCompleted)) {
186-
// Can't resolve the invocation id yet (e.g. suspended); propagate.
185+
// Can't resolve the invocation id yet (still waiting on external progress); propagate.
186+
// (Suspension would have already thrown AbortedExecutionException out of doProgress.)
187187
return resolve;
188188
}
189189
NotificationValue value = takeNotification(tracked.handle);
@@ -209,21 +209,8 @@ public AwaitResult doAwait(UnresolvedFuture future) {
209209
* Single step of {@code do_progress}, translating the legacy {@link State.DoProgressResponse}.
210210
*/
211211
private AwaitResult doProgress(List<Integer> anyHandle) {
212-
State.DoProgressResponse response;
213-
try {
214-
response = this.stateContext.getCurrentState().doProgress(anyHandle, this.stateContext);
215-
} catch (Throwable t) {
216-
// The legacy state machine signalled both suspension (after writing the
217-
// SuspensionMessage) and replay journal mismatches (after writing the ErrorMessage)
218-
// by sneaky-throwing AbortedExecutionException out of doProgress. In both cases the
219-
// state has already transitioned to ClosedState and the relevant message was written
220-
// out, so we surface it as SUSPENDED: the driving handler context will observe the
221-
// CLOSED state on the next loop iteration and abort the user code.
222-
if (ExceptionUtils.containsAbortedExecutionException(t)) {
223-
return AwaitResult.SUSPENDED;
224-
}
225-
throw t;
226-
}
212+
State.DoProgressResponse response =
213+
this.stateContext.getCurrentState().doProgress(anyHandle, this.stateContext);
227214

228215
if (response instanceof State.DoProgressResponse.AnyCompleted) {
229216
return AwaitResult.ANY_COMPLETED;

sdk-core/src/main/java/dev/restate/sdk/core/statemachine/ReplayingState.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import com.google.protobuf.ByteString;
1414
import com.google.protobuf.MessageLite;
15+
import dev.restate.common.Slice;
1516
import dev.restate.sdk.common.AbortedExecutionException;
1617
import dev.restate.sdk.core.ExceptionUtils;
1718
import dev.restate.sdk.core.ProtocolException;
@@ -195,7 +196,7 @@ public StateMachine.Input processInputCommand(StateContext stateContext) {
195196
startInfo.debugId(),
196197
startInfo.objectKey(),
197198
headers,
198-
inputCommandMessage.getValue().getContent().toByteArray(),
199+
Slice.wrap(inputCommandMessage.getValue().getContent().toByteArray()),
199200
Util.randomSeed(startInfo.debugId(), startInfo.randomSeed()),
200201
// scope/limitKey/idempotencyKey are V7-only; the legacy (V6) state machine doesn't carry
201202
// them.

sdk-core/src/main/java/dev/restate/sdk/core/statemachine/StateMachine.java

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@ public interface StateMachine extends AutoCloseable {
6666

6767
// --- Async results
6868

69+
/**
70+
* Make progress on the given await tree.
71+
*
72+
* <p>On suspension this does not return a value: it sneaky-throws {@code
73+
* AbortedExecutionException} (the VM is now closed and its output already pumped). The driving
74+
* handler context treats that sentinel as a clean abort of the user code, not a failure to
75+
* re-report.
76+
*/
6977
AwaitResult doAwait(UnresolvedFuture future);
7078

7179
@Nullable NotificationValue takeNotification(int handle);
@@ -76,7 +84,7 @@ record Input(
7684
String invocationId,
7785
String key,
7886
List<String[]> headers,
79-
byte[] input,
87+
Slice input,
8088
long randomSeed,
8189
// V7 optionals, propagated from the native core; null when absent. Surfaced to the public API
8290
// separately at a later point.
@@ -181,12 +189,15 @@ void proposeRunCompletion(
181189
// Value types
182190
// =========================================================================
183191

184-
/** Outcome of {@link #doAwait(UnresolvedFuture)}. */
192+
/**
193+
* Outcome of {@link #doAwait(UnresolvedFuture)}. Suspension is not represented here: on
194+
* suspension {@code doAwait} sneaky-throws {@code AbortedExecutionException} instead of
195+
* returning.
196+
*/
185197
sealed interface AwaitResult {
186198
AwaitResult ANY_COMPLETED = new AnyCompleted();
187199
AwaitResult WAIT_EXTERNAL_PROGRESS = new WaitExternalProgress();
188200
AwaitResult CANCEL_SIGNAL_RECEIVED = new CancelSignalReceived();
189-
AwaitResult SUSPENDED = new Suspended();
190201

191202
record AnyCompleted() implements AwaitResult {}
192203

@@ -195,8 +206,6 @@ record WaitExternalProgress() implements AwaitResult {}
195206
record ExecuteRun(int handle) implements AwaitResult {}
196207

197208
record CancelSignalReceived() implements AwaitResult {}
198-
199-
record Suspended() implements AwaitResult {}
200209
}
201210

202211
/**

sdk-core/src/main/java23/dev/restate/sdk/core/statemachine/ffm/FfmEncoding.java

Lines changed: 102 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212
import dev.restate.sdk.common.RetryPolicy;
1313
import dev.restate.sdk.common.TerminalException;
1414
import dev.restate.sdk.core.statemachine.StateMachine;
15+
import dev.restate.sdk.core.statemachine.ffm.generated.CallArguments;
16+
import dev.restate.sdk.core.statemachine.ffm.generated.ForeignSlice;
1517
import dev.restate.sdk.core.statemachine.ffm.generated.SharedCoreNative;
1618
import dev.restate.sdk.core.statemachine.ffm.generated.Slice;
17-
import dev.restate.sdk.core.statemachine.ffm.generated.TargetAbi;
1819
import java.io.ByteArrayOutputStream;
1920
import java.lang.foreign.Arena;
2021
import java.lang.foreign.MemorySegment;
@@ -57,39 +58,111 @@ static MemorySegment allocateBytes(SegmentAllocator alloc, byte @Nullable [] byt
5758
}
5859

5960
/**
60-
* Allocate a native segment holding the bytes of {@code slice}, copied straight from the slice's
61-
* buffer (no intermediate heap {@code byte[]}), or {@link MemorySegment#NULL} when empty.
61+
* Allocate a native segment holding the UTF-8 bytes of {@code s}, or {@link MemorySegment#NULL}
62+
* when {@code s} is null. An empty (non-null) string yields a zero-length, non-null segment.
6263
*/
63-
static MemorySegment allocateSlice(SegmentAllocator alloc, dev.restate.common.Slice slice) {
64+
static MemorySegment allocateUtf8(SegmentAllocator alloc, @Nullable String s) {
65+
if (s == null) {
66+
return MemorySegment.NULL;
67+
}
68+
return allocateBytes(alloc, s.getBytes(StandardCharsets.UTF_8));
69+
}
70+
71+
/**
72+
* Allocate a Rust-owned native buffer (via {@code alloc_buffer}) and copy {@code slice} into it,
73+
* transferring ownership to the native callee. The returned segment is tied to no {@link Arena},
74+
* so Java never frees it: the native side takes ownership on the call and frees it when the core
75+
* is done with it (see {@code MEMORY_MODEL.md}). Used for opaque payloads only. Returns {@link
76+
* MemorySegment#NULL} for an empty slice.
77+
*/
78+
static MemorySegment transferSlice(dev.restate.common.Slice slice) {
6479
int len = slice.readableBytes();
6580
if (len == 0) {
6681
return MemorySegment.NULL;
6782
}
68-
MemorySegment seg = alloc.allocate(len);
69-
slice.copyTo(seg.asByteBuffer());
70-
return seg;
83+
MemorySegment buf = SharedCoreNative.alloc_buffer(len).reinterpret(len);
84+
slice.copyTo(buf.asByteBuffer());
85+
return buf;
7186
}
7287

7388
/**
74-
* Allocate a native segment holding the UTF-8 bytes of {@code s}, or {@link MemorySegment#NULL}
75-
* when {@code s} is null. An empty (non-null) string yields a zero-length, non-null segment.
89+
* Like {@link #transferSlice} but for the UTF-8 bytes of {@code s}: allocates a Rust-owned buffer
90+
* via {@code alloc_buffer}, copies the encoded bytes in, and transfers ownership to the native
91+
* callee (which re-owns it as a {@code String} zero-copy, see {@code take_string}). Used for the
92+
* {@code notify_error} message/stacktrace. Returns {@link MemorySegment#NULL} for a null/empty
93+
* string (the Rust side maps that to the empty string / absent stacktrace).
7694
*/
77-
static MemorySegment allocateUtf8(SegmentAllocator alloc, @Nullable String s) {
95+
static MemorySegment transferUtf8(@Nullable String s) {
7896
if (s == null) {
7997
return MemorySegment.NULL;
8098
}
81-
return allocateBytes(alloc, s.getBytes(StandardCharsets.UTF_8));
82-
}
83-
84-
static long len(MemorySegment seg) {
85-
return seg.byteSize();
99+
byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
100+
if (bytes.length == 0) {
101+
return MemorySegment.NULL;
102+
}
103+
MemorySegment buf = SharedCoreNative.alloc_buffer(bytes.length).reinterpret(bytes.length);
104+
MemorySegment.copy(bytes, 0, buf, ValueLayout.JAVA_BYTE, 0, bytes.length);
105+
return buf;
86106
}
87107

88108
/** Allocates a result {@link Slice} struct out-param (for calls that write a bare Slice). */
89109
static MemorySegment allocateSliceStruct(SegmentAllocator alloc) {
90110
return Slice.allocate(alloc);
91111
}
92112

113+
// -------------------------------------------------------------------------
114+
// By-value boundary slices. A ForeignSlice borrows Java-arena memory (Rust reads it in-call and
115+
// never frees it); a Slice is alloc_buffer-backed memory whose ownership transfers into the
116+
// native callee. Both cross by value as a {ptr,len} struct; we keep the construction here so the
117+
// state machine never touches the (name-colliding) generated Slice type directly.
118+
// -------------------------------------------------------------------------
119+
120+
/** Writes {@code buf}'s ptr+len into an already-allocated ForeignSlice field segment. */
121+
private static void setForeign(MemorySegment foreignSliceField, MemorySegment buf) {
122+
ForeignSlice.ptr(foreignSliceField, buf);
123+
ForeignSlice.len(foreignSliceField, buf.byteSize());
124+
}
125+
126+
/** Writes {@code buf}'s ptr+len into an already-allocated (ownership-transfer) Slice field. */
127+
private static void setOwned(MemorySegment sliceField, MemorySegment buf) {
128+
Slice.ptr(sliceField, buf);
129+
Slice.len(sliceField, buf.byteSize());
130+
}
131+
132+
/** A by-value ForeignSlice borrowing a fresh UTF-8 copy of {@code s} (null/empty → null ptr). */
133+
static MemorySegment foreignUtf8(SegmentAllocator alloc, @Nullable String s) {
134+
MemorySegment fs = ForeignSlice.allocate(alloc);
135+
setForeign(fs, allocateUtf8(alloc, s));
136+
return fs;
137+
}
138+
139+
/** A by-value ForeignSlice borrowing a fresh copy of {@code bytes} (null/empty → null ptr). */
140+
static MemorySegment foreignBytes(SegmentAllocator alloc, byte @Nullable [] bytes) {
141+
MemorySegment fs = ForeignSlice.allocate(alloc);
142+
setForeign(fs, allocateBytes(alloc, bytes));
143+
return fs;
144+
}
145+
146+
/**
147+
* A by-value Slice transferring {@code slice}'s bytes (alloc_buffer buffer the callee re-owns).
148+
*/
149+
static MemorySegment transferToSlice(SegmentAllocator alloc, dev.restate.common.Slice slice) {
150+
MemorySegment buf = transferSlice(slice);
151+
MemorySegment s = Slice.allocate(alloc);
152+
Slice.ptr(s, buf);
153+
Slice.len(s, buf.byteSize());
154+
return s;
155+
}
156+
157+
/** A by-value Slice transferring the UTF-8 bytes of {@code s} (callee re-owns it as a String). */
158+
static MemorySegment transferUtf8ToSlice(SegmentAllocator alloc, @Nullable String s) {
159+
MemorySegment buf = transferUtf8(s);
160+
MemorySegment slice = Slice.allocate(alloc);
161+
Slice.ptr(slice, buf);
162+
Slice.len(slice, buf.byteSize());
163+
return slice;
164+
}
165+
93166
// -------------------------------------------------------------------------
94167
// Result Slice reading (outputs: owned by Rust, must be freed)
95168
// -------------------------------------------------------------------------
@@ -121,12 +194,6 @@ static String takeSliceString(MemorySegment sliceStruct) {
121194
return new String(bytes, StandardCharsets.UTF_8);
122195
}
123196

124-
/** Like {@link #takeSliceString} but returns {@code null} for the empty/absent slice. */
125-
static @Nullable String takeSliceStringOrNull(MemorySegment sliceStruct) {
126-
byte[] bytes = takeSliceBytes(sliceStruct);
127-
return bytes.length == 0 ? null : new String(bytes, StandardCharsets.UTF_8);
128-
}
129-
130197
/**
131198
* Wraps an owned result {@link Slice} (Rust-allocated; must be released via {@code free_buffer})
132199
* in a {@link MemorySegmentSlice} read zero-copy — no copy-out into a heap {@code byte[]}. The
@@ -157,44 +224,26 @@ static dev.restate.common.Slice wrapOwnedSlice(MemorySegment sliceStruct) {
157224
* (ptr,len)}; a null ptr means the optional field is absent. {@code headers} is the encoded
158225
* header-list blob.
159226
*/
160-
static MemorySegment buildTarget(
227+
static MemorySegment buildCallArguments(
161228
SegmentAllocator alloc,
162229
Target target,
230+
dev.restate.common.Slice payload,
163231
@Nullable String idempotencyKey,
164232
@Nullable String scope,
165233
@Nullable String limitKey,
166234
@Nullable Collection<Map.Entry<String, String>> headers) {
167-
MemorySegment t = TargetAbi.allocate(alloc);
168-
169-
MemorySegment service = allocateUtf8(alloc, target.getService());
170-
TargetAbi.service_ptr(t, service);
171-
TargetAbi.service_len(t, len(service));
172-
173-
MemorySegment handler = allocateUtf8(alloc, target.getHandler());
174-
TargetAbi.handler_ptr(t, handler);
175-
TargetAbi.handler_len(t, len(handler));
176-
177-
MemorySegment key = allocateUtf8(alloc, target.getKey());
178-
TargetAbi.key_ptr(t, key);
179-
TargetAbi.key_len(t, len(key));
180-
181-
MemorySegment idem = allocateUtf8(alloc, idempotencyKey);
182-
TargetAbi.idempotency_key_ptr(t, idem);
183-
TargetAbi.idempotency_key_len(t, len(idem));
184-
185-
// V7 optionals: a null/empty segment maps to `None` on the Rust side.
186-
MemorySegment scopeSeg = allocateUtf8(alloc, scope);
187-
TargetAbi.scope_ptr(t, scopeSeg);
188-
TargetAbi.scope_len(t, len(scopeSeg));
189-
190-
MemorySegment limitKeySeg = allocateUtf8(alloc, limitKey);
191-
TargetAbi.limit_key_ptr(t, limitKeySeg);
192-
TargetAbi.limit_key_len(t, len(limitKeySeg));
193-
194-
MemorySegment headersBlob = allocateBytes(alloc, encodeHeaderList(headers));
195-
TargetAbi.headers_ptr(t, headersBlob);
196-
TargetAbi.headers_len(t, len(headersBlob));
197-
235+
MemorySegment t = CallArguments.allocate(alloc);
236+
// Target fields are nested ForeignSlices borrowing a fresh copy in `alloc`; a null/empty buffer
237+
// maps to `None` (V7 optionals) / the empty slice on the Rust side.
238+
setForeign(CallArguments.service(t), allocateUtf8(alloc, target.getService()));
239+
setForeign(CallArguments.handler(t), allocateUtf8(alloc, target.getHandler()));
240+
setForeign(CallArguments.key(t), allocateUtf8(alloc, target.getKey()));
241+
setForeign(CallArguments.idempotency_key(t), allocateUtf8(alloc, idempotencyKey));
242+
setForeign(CallArguments.scope(t), allocateUtf8(alloc, scope));
243+
setForeign(CallArguments.limit_key(t), allocateUtf8(alloc, limitKey));
244+
setForeign(CallArguments.headers(t), allocateBytes(alloc, encodeHeaderList(headers)));
245+
// input is a Slice: ownership of the alloc_buffer-backed payload transfers into the core.
246+
setOwned(CallArguments.input(t), transferSlice(payload));
198247
return t;
199248
}
200249

0 commit comments

Comments
 (0)