3030import java .util .LinkedHashMap ;
3131import java .util .List ;
3232import java .util .Map ;
33+ import org .apache .logging .log4j .Logger ;
3334import org .jspecify .annotations .Nullable ;
3435
3536/**
4041 * <p>Each call is a direct FFM downcall. Results come back either register-packed into the scalar
4142 * return value (the invocation state, plus a handle for handle-returning calls) or, when too wide
4243 * to pack, written into a caller-provided out-param struct whose first field is the state. Owned
43- * {@link Slice}s are copied out and freed by the caller. On error the state field carries the
44- * {@link SharedCoreNative#ERROR_STATE()} sentinel; the detail is then fetched via {@code
45- * vm_take_last_vm_error} and thrown as a {@link ProtocolException}.
44+ * {@link Slice}s are copied out and freed by the caller. On the {@link
45+ * SharedCoreNative#ERROR_STATE()} sentinel the core has already written its terminal message and
46+ * closed, so we sneaky-throw {@link AbortedExecutionException} ({@link #abortAfterCoreClosed()});
47+ * construction and {@code isReadyToExecute} errors surface as a {@link ProtocolException}.
4648 *
4749 * <p>An instance is driven by a single thread at a time (no reentrancy, per the contract); only
4850 * {@link #state()} — a volatile read — is safe from any thread. Each downcall allocates its
4951 * out-param and inputs in a confined {@link Arena} for the duration of the call.
5052 */
5153public final class FfmStateMachine implements StateMachine {
5254
55+ // Same logger as the native tracing bridge, so SDK- and core-emitted lines interleave.
56+ private static final Logger LOG = NativeLogging .LOG ;
57+
58+ private static final int SUSPENDED_ERROR_CODE = 599 ;
59+
5360 private final MemorySegment vmHandle ;
5461 private final String responseContentType ;
5562 private boolean freed = false ;
@@ -186,12 +193,9 @@ public AwaitResult doAwait(UnresolvedFuture future) {
186193 packed = SharedCoreNative .vm_do_await (vmHandle , futureSeg );
187194 }
188195
189- // Register-packed result (a bare u64, no out-param struct): the low byte is the variant tag,
190- // the
191- // high 32 bits carry the ExecuteRun run handle. On error the core stashed the detail and we
192- // fetch
193- // it via vm_take_last_vm_error. Suspension isn't a variant here — the driver surfaces it by
194- // sneaky-throwing AbortedExecutionException per the StateMachine contract.
196+ // Register-packed u64: low byte = variant tag, high 32 bits = ExecuteRun handle. The ERROR
197+ // variant also covers suspension (the core folds it in); either way the core already wrote its
198+ // terminal message and closed, so we abort cleanly rather than fetch the discarded detail.
195199 int variant = (int ) packed ;
196200 if (variant == SharedCoreNative .AWAIT_VARIANT_ANY_COMPLETED ()) {
197201 return AwaitResult .ANY_COMPLETED ;
@@ -202,22 +206,11 @@ public AwaitResult doAwait(UnresolvedFuture future) {
202206 } else if (variant == SharedCoreNative .AWAIT_VARIANT_CANCEL_SIGNAL_RECEIVED ()) {
203207 return AwaitResult .CANCEL_SIGNAL_RECEIVED ;
204208 } else if (variant == SharedCoreNative .AWAIT_VARIANT_ERROR ()) {
205- throw takeLastVmError ();
209+ abortAfterCoreClosed ();
206210 }
207211 throw new IllegalStateException ("Unknown do_await variant: " + variant );
208212 }
209213
210- /**
211- * Fetches + maps the error the core stashed on the last failing packed-result call (cold path).
212- */
213- private ProtocolException takeLastVmError () {
214- try (Arena arena = Arena .ofConfined ()) {
215- MemorySegment err = VmError .allocate (arena );
216- SharedCoreNative .vm_take_last_vm_error (vmHandle , err );
217- return closeVmAndMapError (err );
218- }
219- }
220-
221214 @ Override
222215 public @ Nullable NotificationValue takeNotification (int handle ) {
223216 verifyNotFreed ();
@@ -293,7 +286,7 @@ public Input input() {
293286 SharedCoreNative .vm_sys_input (vmHandle , out );
294287 int state = InputResult .state (out );
295288 if (state == SharedCoreNative .ERROR_STATE ()) {
296- throw takeLastVmError ();
289+ abortAfterCoreClosed ();
297290 }
298291 updateState (state );
299292
@@ -429,7 +422,7 @@ public CallHandle call(
429422 SharedCoreNative .vm_sys_call (vmHandle , args , out );
430423 int state = CallResult .state (out );
431424 if (state == SharedCoreNative .ERROR_STATE ()) {
432- throw takeLastVmError ();
425+ abortAfterCoreClosed ();
433426 }
434427 updateState (state );
435428 return new CallHandle (CallResult .invocation_id_handle (out ), CallResult .result_handle (out ));
@@ -469,7 +462,7 @@ public Awakeable awakeable() {
469462 SharedCoreNative .vm_sys_awakeable (vmHandle , out );
470463 int state = AwakeableResult .state (out );
471464 if (state == SharedCoreNative .ERROR_STATE ()) {
472- throw takeLastVmError ();
465+ abortAfterCoreClosed ();
473466 }
474467 updateState (state );
475468 return new Awakeable (
@@ -592,7 +585,7 @@ public RunResultHandle run(String name) {
592585 SharedCoreNative .vm_sys_run (vmHandle , FfmEncoding .foreignUtf8 (arena , name ), out );
593586 int state = RunResult .state (out );
594587 if (state == SharedCoreNative .ERROR_STATE ()) {
595- throw takeLastVmError ();
588+ abortAfterCoreClosed ();
596589 }
597590 updateState (state );
598591 return new RunResultHandle (RunResult .replayed (out ) != 0 , RunResult .handle (out ));
@@ -715,25 +708,25 @@ public void end() {
715708 /**
716709 * Decodes a register-packed handle result ({@code u64}): low 32 bits = state (or the {@link
717710 * SharedCoreNative#ERROR_STATE()} sentinel), high 32 bits = handle. On the error sentinel we
718- * fetch + throw the stashed error ; otherwise update the cached state and return the handle.
711+ * {@link #abortAfterCoreClosed()} ; otherwise update the cached state and return the handle.
719712 */
720713 private int parseHandleResult (long packed ) {
721714 int state = (int ) packed ;
722715 if (state == SharedCoreNative .ERROR_STATE ()) {
723- throw takeLastVmError ();
716+ abortAfterCoreClosed ();
724717 }
725718 updateState (state );
726719 return (int ) (packed >>> 32 );
727720 }
728721
729722 /**
730723 * Applies a register-packed empty result (a bare {@code u32}): the new state, or the {@link
731- * SharedCoreNative#ERROR_STATE()} sentinel. On the error sentinel we fetch + throw the stashed
732- * error ; otherwise update the cached state.
724+ * SharedCoreNative#ERROR_STATE()} sentinel. On the error sentinel we {@link
725+ * #abortAfterCoreClosed()} ; otherwise update the cached state.
733726 */
734727 private void consumeEmptyResult (int packed ) {
735728 if (packed == SharedCoreNative .ERROR_STATE ()) {
736- throw takeLastVmError ();
729+ abortAfterCoreClosed ();
737730 }
738731 updateState (packed );
739732 }
@@ -766,6 +759,29 @@ private void verifyNotFreed() {
766759 }
767760 }
768761
762+ /**
763+ * Mark the state machine as closed and throw AbortedExecutionException.
764+ *
765+ * <p>A subsequent notifyError will no-op. Before aborting we surface the failure the core stashed
766+ * (skipping the fetch when WARN is off, and skipping suspensions, which ride the same channel).
767+ */
768+ private void abortAfterCoreClosed () {
769+ cachedState = InvocationState .CLOSED ;
770+ if (LOG .isWarnEnabled ()) {
771+ try (Arena arena = Arena .ofConfined ()) {
772+ MemorySegment err = VmError .allocate (arena );
773+ SharedCoreNative .vm_take_last_vm_error (vmHandle , err );
774+ int code = VmError .code (err );
775+ // takeSliceString also frees the owned message Slice, so decode it even when not logging.
776+ String message = FfmEncoding .takeSliceString (VmError .message (err ));
777+ if (code != SUSPENDED_ERROR_CODE ) {
778+ LOG .warn ("Invocation failed: {}" , message );
779+ }
780+ }
781+ }
782+ AbortedExecutionException .sneakyThrow ();
783+ }
784+
769785 private static String formatThrowableMessage (Throwable throwable ) {
770786 return throwable .toString ();
771787 }
0 commit comments