11//! Shared tests that are meant to be run against both local dev server and cloud
22
3- use crate :: common:: CoreWfStarter ;
4- use std:: sync:: {
5- Arc ,
6- atomic:: { AtomicBool , Ordering :: Relaxed } ,
3+ use crate :: common:: { CoreWfStarter , activity_functions:: StdActivities } ;
4+ use std:: {
5+ sync:: {
6+ Arc ,
7+ atomic:: { AtomicBool , Ordering :: Relaxed } ,
8+ } ,
9+ time:: Duration ,
10+ } ;
11+ use temporalio_client:: {
12+ WorkflowFetchHistoryOptions , WorkflowStartOptions , WorkflowTerminateOptions ,
713} ;
814use temporalio_common:: {
15+ UntypedWorkflow ,
916 protos:: temporal:: api:: {
1017 enums:: v1:: { EventType , WorkflowTaskFailedCause :: GrpcMessageTooLarge } ,
1118 history:: v1:: history_event:: Attributes :: {
@@ -15,7 +22,7 @@ use temporalio_common::{
1522 worker:: WorkerTaskTypes ,
1623} ;
1724use temporalio_macros:: { workflow, workflow_methods} ;
18- use temporalio_sdk:: { WorkflowContext , WorkflowResult } ;
25+ use temporalio_sdk:: { ActivityOptions , WorkflowContext , WorkflowResult , WorkflowTermination } ;
1926
2027pub ( crate ) mod priority;
2128
@@ -92,3 +99,107 @@ pub(crate) fn is_oversize_grpc_event(
9299 false
93100 }
94101}
102+
103+ #[ workflow]
104+ #[ derive( Default ) ]
105+ struct ShutdownTimerActivityLoopWf ;
106+
107+ #[ workflow_methods]
108+ impl ShutdownTimerActivityLoopWf {
109+ #[ run]
110+ async fn run ( ctx : & mut WorkflowContext < Self > ) -> WorkflowResult < ( ) > {
111+ loop {
112+ ctx. timer ( Duration :: from_millis ( 10 ) ) . await ;
113+ ctx. start_activity (
114+ StdActivities :: no_op,
115+ ( ) ,
116+ ActivityOptions :: start_to_close_timeout ( Duration :: from_secs ( 10 ) ) ,
117+ )
118+ . await
119+ . map_err ( |e| WorkflowTermination :: from ( anyhow:: Error :: from ( e) ) ) ?;
120+ }
121+ }
122+ }
123+
124+ /// Starts 10 workflows that each run a tight timer+activity loop, then shuts down the worker
125+ /// and verifies:
126+ /// 1. Shutdown completes rapidly (< 5s)
127+ /// 2. No workflow task failures or timeouts appear in any workflow's history
128+ pub ( crate ) async fn shutdown_during_active_timer_activity_workflows ( ) {
129+ let wf_name = "shutdown_during_active_timer_activity_workflows" ;
130+ let num_workflows = 10 ;
131+
132+ let mut starter = CoreWfStarter :: new ( wf_name) ;
133+ starter. sdk_config . register_activities ( StdActivities ) ;
134+ let mut worker = starter. worker ( ) . await ;
135+ worker. register_workflow :: < ShutdownTimerActivityLoopWf > ( ) ;
136+
137+ let core = worker. core_worker ( ) ;
138+ core. validate ( ) . await . unwrap ( ) ;
139+ assert ! (
140+ core. get_namespace_capabilities( ) . graceful_poll_shutdown( ) ,
141+ "Server must support graceful poll shutdown for this test"
142+ ) ;
143+
144+ let task_queue = starter. get_task_queue ( ) . to_owned ( ) ;
145+ let mut wf_ids = Vec :: with_capacity ( num_workflows) ;
146+ for i in 0 ..num_workflows {
147+ let wf_id = format ! ( "{task_queue}-{i}" ) ;
148+ worker
149+ . submit_workflow (
150+ ShutdownTimerActivityLoopWf :: run,
151+ ( ) ,
152+ WorkflowStartOptions :: new ( task_queue. clone ( ) , wf_id. clone ( ) ) . build ( ) ,
153+ )
154+ . await
155+ . unwrap ( ) ;
156+ wf_ids. push ( wf_id) ;
157+ }
158+ // Don't wait for workflow completion — these loop forever
159+ worker. fetch_results = false ;
160+
161+ let shutdown_handle = worker. inner_mut ( ) . shutdown_handle ( ) ;
162+ let run_fut = async { worker. run_until_done ( ) . await . unwrap ( ) } ;
163+
164+ let shutdown_fut = async {
165+ // Let workflows run a few iterations
166+ tokio:: time:: sleep ( Duration :: from_secs ( 2 ) ) . await ;
167+ shutdown_handle ( ) ;
168+ } ;
169+
170+ let shutdown_start = std:: time:: Instant :: now ( ) ;
171+ tokio:: join!( run_fut, shutdown_fut) ;
172+ let shutdown_elapsed = shutdown_start. elapsed ( ) ;
173+
174+ assert ! (
175+ shutdown_elapsed < Duration :: from_secs( 5 ) ,
176+ "Worker shutdown took {shutdown_elapsed:?}, expected < 5s"
177+ ) ;
178+
179+ let client = starter. get_client ( ) . await ;
180+ for wf_id in & wf_ids {
181+ client
182+ . get_workflow_handle :: < UntypedWorkflow > ( wf_id)
183+ . terminate ( WorkflowTerminateOptions :: default ( ) )
184+ . await
185+ . unwrap ( ) ;
186+
187+ let history = client
188+ . get_workflow_handle :: < UntypedWorkflow > ( wf_id)
189+ . fetch_history ( WorkflowFetchHistoryOptions :: default ( ) )
190+ . await
191+ . unwrap ( ) ;
192+ let bad_events: Vec < _ > = history
193+ . events ( )
194+ . iter ( )
195+ . filter ( |e| {
196+ e. event_type ( ) == EventType :: WorkflowTaskFailed
197+ || e. event_type ( ) == EventType :: WorkflowTaskTimedOut
198+ } )
199+ . collect ( ) ;
200+ assert ! (
201+ bad_events. is_empty( ) ,
202+ "Workflow {wf_id} had unexpected WFT failures/timeouts: {bad_events:?}"
203+ ) ;
204+ }
205+ }
0 commit comments