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,112 @@ 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 =
133+ if let Some ( wfs) = CoreWfStarter :: new_cloud_or_local ( wf_name, ">=1.6.3-serverless" ) . await {
134+ wfs
135+ } else {
136+ return ;
137+ } ;
138+ starter. sdk_config . register_activities ( StdActivities ) ;
139+ let mut worker = starter. worker ( ) . await ;
140+ worker. register_workflow :: < ShutdownTimerActivityLoopWf > ( ) ;
141+
142+ let core = worker. core_worker ( ) ;
143+ core. validate ( ) . await . unwrap ( ) ;
144+ assert ! (
145+ core. get_namespace_capabilities( ) . graceful_poll_shutdown( ) ,
146+ "Server must support graceful poll shutdown for this test"
147+ ) ;
148+
149+ let task_queue = starter. get_task_queue ( ) . to_owned ( ) ;
150+ let mut wf_ids = Vec :: with_capacity ( num_workflows) ;
151+ for i in 0 ..num_workflows {
152+ let wf_id = format ! ( "{task_queue}-{i}" ) ;
153+ worker
154+ . submit_workflow (
155+ ShutdownTimerActivityLoopWf :: run,
156+ ( ) ,
157+ WorkflowStartOptions :: new ( task_queue. clone ( ) , wf_id. clone ( ) ) . build ( ) ,
158+ )
159+ . await
160+ . unwrap ( ) ;
161+ wf_ids. push ( wf_id) ;
162+ }
163+ // Don't wait for workflow completion — these loop forever
164+ worker. fetch_results = false ;
165+
166+ let shutdown_handle = worker. inner_mut ( ) . shutdown_handle ( ) ;
167+ let run_fut = async { worker. run_until_done ( ) . await . unwrap ( ) } ;
168+
169+ let shutdown_fut = async {
170+ // Let workflows run a few iterations
171+ tokio:: time:: sleep ( Duration :: from_secs ( 2 ) ) . await ;
172+ shutdown_handle ( ) ;
173+ } ;
174+
175+ let shutdown_start = std:: time:: Instant :: now ( ) ;
176+ tokio:: join!( run_fut, shutdown_fut) ;
177+ let shutdown_elapsed = shutdown_start. elapsed ( ) ;
178+
179+ assert ! (
180+ shutdown_elapsed < Duration :: from_secs( 5 ) ,
181+ "Worker shutdown took {shutdown_elapsed:?}, expected < 5s"
182+ ) ;
183+
184+ let client = starter. get_client ( ) . await ;
185+ for wf_id in & wf_ids {
186+ client
187+ . get_workflow_handle :: < UntypedWorkflow > ( wf_id)
188+ . terminate ( WorkflowTerminateOptions :: default ( ) )
189+ . await
190+ . unwrap ( ) ;
191+
192+ let history = client
193+ . get_workflow_handle :: < UntypedWorkflow > ( wf_id)
194+ . fetch_history ( WorkflowFetchHistoryOptions :: default ( ) )
195+ . await
196+ . unwrap ( ) ;
197+ let bad_events: Vec < _ > = history
198+ . events ( )
199+ . iter ( )
200+ . filter ( |e| {
201+ e. event_type ( ) == EventType :: WorkflowTaskFailed
202+ || e. event_type ( ) == EventType :: WorkflowTaskTimedOut
203+ } )
204+ . collect ( ) ;
205+ assert ! (
206+ bad_events. is_empty( ) ,
207+ "Workflow {wf_id} had unexpected WFT failures/timeouts: {bad_events:?}"
208+ ) ;
209+ }
210+ }
0 commit comments