@@ -71,7 +71,7 @@ import type { AnyDatabaseProvider } from "@/common/database/config";
7171import { wrapJsNativeDatabase } from "@/common/database/native-database" ;
7272import type { Encoding } from "@/common/encoding" ;
7373import { decodeWorkflowHistoryTransport } from "@/common/inspector-transport" ;
74- import { deconstructError } from "@/common/utils" ;
74+ import { deconstructError , stringifyError } from "@/common/utils" ;
7575import type {
7676 RivetCloseEvent ,
7777 RivetEvent ,
@@ -2590,13 +2590,32 @@ export class NativeActorContextAdapter {
25902590 }
25912591
25922592 keepAwake < T > ( promise : Promise < T > ) : Promise < T > {
2593- // Forward to core `keep_awake`, which increments the keep_awake counter
2594- // for the duration of the promise. This blocks both idle sleep and
2595- // grace finalize until the promise settles. The promise value is
2596- // returned unchanged; core only observes the settle signal.
2597- void callNative ( ( ) =>
2593+ // Forward to core `keep_awake`, which holds the keep_awake counter
2594+ // for the duration of the promise (blocks both idle sleep and grace
2595+ // finalize). The promise value is returned unchanged; core only
2596+ // observes the settle signal.
2597+ //
2598+ // Counter-arm race (acceptable): the NAPI `keep_awake` call is async,
2599+ // so the Rust `keep_awake_guard()` increment happens on first poll of
2600+ // the Rust future, not synchronously when JS calls this method. There
2601+ // is a sub-millisecond window where idle-sleep evaluation could
2602+ // observe `keep_awake_count == 0`. In practice the idle timer runs on
2603+ // `sleep_timeout` (default 30s), so the next poll always observes the
2604+ // counter before the timer fires. Same race exists for `waitUntil`.
2605+ // We accept this trade-off in exchange for keeping the JS API
2606+ // fire-and-forget; core stays the single source of truth for sleep
2607+ // gating logic. Logging the rejection avoids unhandled-promise warnings
2608+ // without blocking the caller.
2609+ callNative ( ( ) =>
25982610 this . #ctx. keepAwake ( Promise . resolve ( promise ) . then ( ( ) => null ) ) ,
2599- ) ;
2611+ ) . catch ( ( error ) => {
2612+ if ( ! isClosedTaskRegistrationError ( error ) ) {
2613+ logger ( ) . warn ( {
2614+ msg : "keepAwake bridge to native runtime failed" ,
2615+ error : stringifyError ( error ) ,
2616+ } ) ;
2617+ }
2618+ } ) ;
26002619 return promise ;
26012620 }
26022621
@@ -2620,7 +2639,20 @@ export class NativeActorContextAdapter {
26202639 }
26212640
26222641 waitUntil ( promise : Promise < unknown > ) : void {
2623- void callNative ( ( ) => this . #ctx. waitUntil ( Promise . resolve ( promise ) ) ) ;
2642+ // Same counter-arm race as `keepAwake`: increment of the
2643+ // shutdown_counter happens on first poll of the Rust future. Acceptable
2644+ // because the only consumer is the grace-finalize predicate, which
2645+ // debounces through `activity_notify` and re-checks the counter.
2646+ callNative ( ( ) => this . #ctx. waitUntil ( Promise . resolve ( promise ) ) ) . catch (
2647+ ( error ) => {
2648+ if ( ! isClosedTaskRegistrationError ( error ) ) {
2649+ logger ( ) . warn ( {
2650+ msg : "waitUntil bridge to native runtime failed" ,
2651+ error : stringifyError ( error ) ,
2652+ } ) ;
2653+ }
2654+ } ,
2655+ ) ;
26242656 }
26252657
26262658 beginWebSocketCallback ( ) : number {
0 commit comments