@@ -363,6 +363,41 @@ public function testAwaitWithOneTimer_Leaks(): void
363363 $ this ->assertSame (0 , $ after - $ before );
364364 }
365365
366+ public function testDetachedScope_Leaks (): void
367+ {
368+ $ worker = WorkerMock::createMock ();
369+
370+ // Run the workflow $i times
371+ for ($ id = 9000 , $ i = 0 ; $ i < 100 ; ++$ i ) {
372+ $ uuid1 = Uuid::v4 ();
373+ $ uuid2 = Uuid::v4 ();
374+ $ id1 = ++$ id ;
375+ $ id2 = ++$ id ;
376+ $ log = <<<LOG
377+ [0m [{"command":"StartWorkflow","options":{"info":{"WorkflowExecution":{"ID":" $ uuid1","RunID":" $ uuid2"},"WorkflowType":{"Name":"DetachedScopeWorkflow"},"TaskQueueName":"default","WorkflowExecutionTimeout":315360000000000000,"WorkflowRunTimeout":315360000000000000,"WorkflowTaskTimeout":0,"Namespace":"default","Attempt":1,"CronSchedule":"","ContinuedExecutionRunID":"","ParentWorkflowNamespace":"","ParentWorkflowExecution":null,"Memo":null,"SearchAttributes":null,"BinaryChecksum":"4301710877bf4b107429ee12de0922be"}},"payloads":"CicKFgoIZW5jb2RpbmcSCmpzb24vcGxhaW4SDSJIZWxsbyBXb3JsZCI="}] {"taskQueue":"default","tickTime":"2021-01-12T15:21:52.2672785Z"}
378+ # Run a timer
379+ [0m [{"id": $ id1,"command":"NewTimer","options":{"ms":5000000},"payloads":"","header":""},{"payloads":"ChkKFwoIZW5jb2RpbmcSC2JpbmFyeS9udWxs"},{"id": $ id2,"command":"CompleteWorkflow","options":{},"payloads":"Ch4KFgoIZW5jb2RpbmcSCmpzb24vcGxhaW4SBCJvayI=","header":""}] {"receive": true}
380+ # Destroy workflow
381+ [0m [{"command":"DestroyWorkflow","options":{"runId":" $ uuid2"}}] {"taskQueue":"default","tickTime":"2021-01-12T15:21:53.3838443Z","replay":true}
382+ [0m [{"payloads":"ChkKFwoIZW5jb2RpbmcSC2JpbmFyeS9udWxs"}] {"receive": true}
383+ LOG ;
384+
385+ $ worker ->run ($ this , Splitter::createFromString ($ log )->getQueue ());
386+ if ($ i === 3 ) {
387+ \gc_collect_cycles ();
388+ $ before = \memory_get_usage ();
389+ }
390+ }
391+ $ after = \memory_get_usage ();
392+
393+ $ factory = self ::getPrivate ($ worker , 'factory ' );
394+ $ client = self ::getPrivate ($ factory , 'client ' );
395+ $ requests = self ::getPrivate ($ client , 'requests ' );
396+ self ::assertCount (0 , $ requests );
397+
398+ $ this ->assertSame (0 , $ after - $ before );
399+ }
400+
366401 /**
367402 * Test case when an external Temporal SDK returns empty payload that doesn't contain even NULL value.
368403 *
@@ -399,4 +434,14 @@ public function setUp(): void
399434 // emulate connection to parent server
400435 $ _SERVER ['RR_RPC ' ] = 'tcp://127.0.0.1:6001 ' ;
401436 }
437+
438+ /**
439+ * Fetch a private property from an object.
440+ *
441+ * @param non-empty-string $key Property name
442+ */
443+ private static function getPrivate (object $ object , string $ key ): mixed
444+ {
445+ return (fn (object $ value ) => $ value ->{$ key } ?? null )->call ($ object , $ object );
446+ }
402447}
0 commit comments