@@ -16,6 +16,37 @@ export interface MemoryLoadInput {
1616 payloadBytes ?: number ;
1717}
1818
19+ export interface RunClosureRetentionInput {
20+ runCount ?: number ;
21+ payloadBytes ?: number ;
22+ holdMillis ?: number ;
23+ }
24+
25+ export interface RunClosureRetentionReleaseInput {
26+ invocationId ?: string ;
27+ }
28+
29+ export type RunClosureRetentionPhase =
30+ | "idle"
31+ | "capturing"
32+ | "holding"
33+ | "released"
34+ | "completed" ;
35+
36+ export interface RunClosureRetentionStatus {
37+ invocationId ?: string ;
38+ phase : RunClosureRetentionPhase ;
39+ completedRuns : number ;
40+ runCount : number ;
41+ payloadBytes : number ;
42+ }
43+
44+ export interface RunClosureRetentionResult {
45+ invocationId : string ;
46+ completedRuns : number ;
47+ payloadBytes : number ;
48+ }
49+
1950export interface MemoryStatsInput {
2051 forceGc ?: boolean ;
2152}
@@ -41,10 +72,54 @@ function boundedPayloadBytes(input: MemoryLoadInput | undefined): number {
4172 return Math . max ( 0 , Math . min ( input ?. payloadBytes ?? 512 , 64 * 1024 ) ) ;
4273}
4374
75+ function boundedRunClosureRetentionPayloadBytes (
76+ input : RunClosureRetentionInput | undefined
77+ ) : number {
78+ return Math . max ( 0 , Math . min ( input ?. payloadBytes ?? 128 * 1024 , 512 * 1024 ) ) ;
79+ }
80+
81+ function boundedRunClosureRetentionRunCount (
82+ input : RunClosureRetentionInput | undefined
83+ ) : number {
84+ return Math . max ( 1 , Math . min ( input ?. runCount ?? 200 , 2_000 ) ) ;
85+ }
86+
87+ function boundedRunClosureRetentionHoldMillis (
88+ input : RunClosureRetentionInput | undefined
89+ ) : number {
90+ return Math . max ( 1 , Math . min ( input ?. holdMillis ?? 30_000 , 120_000 ) ) ;
91+ }
92+
4493function allocatePayload ( input : MemoryLoadInput | undefined ) : number {
4594 return "x" . repeat ( boundedPayloadBytes ( input ) ) . length ;
4695}
4796
97+ function allocateRunClosurePayload (
98+ runIndex : number ,
99+ payloadBytes : number
100+ ) : number [ ] {
101+ const elementCount = Math . max ( 1 , Math . ceil ( payloadBytes / 8 ) ) ;
102+ const payload = new Array < number > ( elementCount ) ;
103+
104+ for ( let elementIndex = 0 ; elementIndex < elementCount ; elementIndex ++ ) {
105+ payload [ elementIndex ] = runIndex + elementIndex + 0.5 ;
106+ }
107+
108+ return payload ;
109+ }
110+
111+ async function completeRunWithCapturedPayload (
112+ ctx : restate . Context ,
113+ runIndex : number ,
114+ payloadBytes : number
115+ ) : Promise < number > {
116+ const capturedPayload = allocateRunClosurePayload ( runIndex , payloadBytes ) ;
117+ return ctx . run (
118+ `run-closure-retention-captured-payload-${ runIndex } ` ,
119+ ( ) => capturedPayload . length
120+ ) ;
121+ }
122+
48123function forceGcIfAvailable ( ) : boolean {
49124 const gc = ( globalThis as { gc ?: ( ) => void } ) . gc ;
50125 if ( ! gc ) return false ;
@@ -83,6 +158,41 @@ const passThroughHooks: HooksProvider = () => ({
83158 } ,
84159} ) ;
85160
161+ const runClosureRetentionStatus : RunClosureRetentionStatus = {
162+ phase : "idle" ,
163+ completedRuns : 0 ,
164+ runCount : 0 ,
165+ payloadBytes : 0 ,
166+ } ;
167+ let releaseRunClosureRetentionHold : ( ( ) => void ) | undefined ;
168+
169+ function updateRunClosureRetentionStatus (
170+ update : Partial < RunClosureRetentionStatus >
171+ ) : RunClosureRetentionStatus {
172+ Object . assign ( runClosureRetentionStatus , update ) ;
173+ return { ...runClosureRetentionStatus } ;
174+ }
175+
176+ function waitForRunClosureRetentionRelease ( holdMillis : number ) : Promise < void > {
177+ return new Promise < void > ( ( resolve ) => {
178+ let settled = false ;
179+ const timeout = setTimeout ( release , holdMillis ) ;
180+
181+ function release ( ) {
182+ if ( settled ) return ;
183+
184+ settled = true ;
185+ clearTimeout ( timeout ) ;
186+ if ( releaseRunClosureRetentionHold === release ) {
187+ releaseRunClosureRetentionHold = undefined ;
188+ }
189+ resolve ( ) ;
190+ }
191+
192+ releaseRunClosureRetentionHold = release ;
193+ } ) ;
194+ }
195+
86196function createMemoryLeakProbe ( name : string ) {
87197 return restate . service ( {
88198 name,
@@ -167,6 +277,90 @@ function createMemoryLeakProbe(name: string) {
167277 }
168278 ) ,
169279
280+ runClosureRetention : async (
281+ ctx : restate . Context ,
282+ input : RunClosureRetentionInput
283+ ) : Promise < RunClosureRetentionResult > => {
284+ const invocationId = ctx . request ( ) . id ;
285+ const runCount = boundedRunClosureRetentionRunCount ( input ) ;
286+ const payloadBytes = boundedRunClosureRetentionPayloadBytes ( input ) ;
287+ const holdMillis = boundedRunClosureRetentionHoldMillis ( input ) ;
288+
289+ await ctx . run ( "run-closure-retention-start" , ( ) => {
290+ releaseRunClosureRetentionHold ?.( ) ;
291+ releaseRunClosureRetentionHold = undefined ;
292+ return updateRunClosureRetentionStatus ( {
293+ invocationId,
294+ phase : "capturing" ,
295+ completedRuns : 0 ,
296+ runCount,
297+ payloadBytes,
298+ } ) ;
299+ } ) ;
300+
301+ for ( let runIndex = 0 ; runIndex < runCount ; runIndex ++ ) {
302+ await completeRunWithCapturedPayload ( ctx , runIndex , payloadBytes ) ;
303+
304+ const completedRuns = runIndex + 1 ;
305+ if ( completedRuns % 25 === 0 || completedRuns === runCount ) {
306+ await ctx . run (
307+ `run-closure-retention-progress-${ completedRuns } ` ,
308+ ( ) =>
309+ updateRunClosureRetentionStatus ( {
310+ completedRuns,
311+ } )
312+ ) ;
313+ }
314+ }
315+
316+ await ctx . run ( "run-closure-retention-holding" , ( ) =>
317+ updateRunClosureRetentionStatus ( {
318+ phase : "holding" ,
319+ } )
320+ ) ;
321+ await ctx . run ( "run-closure-retention-hold" , ( ) =>
322+ waitForRunClosureRetentionRelease ( holdMillis )
323+ ) ;
324+ await ctx . run ( "run-closure-retention-completed" , ( ) =>
325+ updateRunClosureRetentionStatus ( {
326+ phase : "completed" ,
327+ completedRuns : runCount ,
328+ } )
329+ ) ;
330+
331+ return { invocationId, completedRuns : runCount , payloadBytes } ;
332+ } ,
333+
334+ runClosureRetentionStatus : async (
335+ ctx : restate . Context
336+ ) : Promise < RunClosureRetentionStatus > => {
337+ return ctx . run ( "run-closure-retention-status" , ( ) => ( {
338+ ...runClosureRetentionStatus ,
339+ } ) ) ;
340+ } ,
341+
342+ releaseRunClosureRetention : async (
343+ ctx : restate . Context ,
344+ input : RunClosureRetentionReleaseInput
345+ ) : Promise < boolean > => {
346+ return ctx . run ( "release-run-closure-retention" , ( ) => {
347+ if (
348+ input ?. invocationId !== undefined &&
349+ input . invocationId !== runClosureRetentionStatus . invocationId
350+ ) {
351+ return false ;
352+ }
353+
354+ const release = releaseRunClosureRetentionHold ;
355+ if ( release === undefined ) return false ;
356+
357+ releaseRunClosureRetentionHold = undefined ;
358+ updateRunClosureRetentionStatus ( { phase : "released" } ) ;
359+ release ( ) ;
360+ return true ;
361+ } ) ;
362+ } ,
363+
170364 abortTimeoutZero : restate . createServiceHandler (
171365 {
172366 inactivityTimeout : 0 ,
0 commit comments