@@ -8,7 +8,6 @@ use std::{
8
8
9
9
use anyhow:: { anyhow, bail, Context , Result } ;
10
10
use clap:: { CommandFactory , Parser } ;
11
- use itertools:: Itertools ;
12
11
use reqwest:: Url ;
13
12
use spin_app:: locked:: LockedApp ;
14
13
use spin_common:: ui:: quoted_path;
@@ -17,14 +16,23 @@ use spin_oci::OciLoader;
17
16
use spin_trigger:: cli:: { SPIN_LOCAL_APP_DIR , SPIN_LOCKED_URL , SPIN_WORKING_DIR } ;
18
17
use tempfile:: TempDir ;
19
18
20
- use futures:: StreamExt ;
21
-
22
19
use crate :: opts:: * ;
23
20
24
21
use self :: app_source:: { AppSource , ResolvedAppSource } ;
25
22
26
23
const APPLICATION_OPT : & str = "APPLICATION" ;
27
24
25
+ // If multiple triggers start very close together, there is a race condition
26
+ // where if one trigger fails during startup, other external triggers may
27
+ // not have their cancellation hooked up. (kill_on_drop doesn't fully solve
28
+ // this because it kills the Spin process but that doesn't cascade to the
29
+ // child plugin trigger process.) So add a hopefully insignificant delay
30
+ // between them to reduce the chance of this happening.
31
+ const MULTI_TRIGGER_START_OFFSET : tokio:: time:: Duration = tokio:: time:: Duration :: from_millis ( 20 ) ;
32
+ // And just in case wait a few moments before performing the first "have
33
+ // any exited" check.
34
+ const MULTI_TRIGGER_LET_ALL_START : tokio:: time:: Duration = tokio:: time:: Duration :: from_millis ( 500 ) ;
35
+
28
36
/// Start the Fermyon runtime.
29
37
#[ derive( Parser , Debug , Default ) ]
30
38
#[ clap(
@@ -181,20 +189,30 @@ impl UpCommand {
181
189
local_app_dir,
182
190
} ;
183
191
184
- let mut trigger_processes = self . start_trigger_processes ( trigger_cmds, run_opts) . await ?;
192
+ let trigger_processes = self . start_trigger_processes ( trigger_cmds, run_opts) . await ?;
193
+ let is_multi = trigger_processes. len ( ) > 1 ;
194
+ let pids = get_pids ( & trigger_processes) ;
185
195
186
- set_kill_on_ctrl_c ( & trigger_processes ) ?;
196
+ set_kill_on_ctrl_c ( & pids ) ?;
187
197
188
- let mut trigger_tasks = trigger_processes
189
- . iter_mut ( )
190
- . map ( |ch| ch. wait ( ) )
191
- . collect :: < futures:: stream:: FuturesUnordered < _ > > ( ) ;
198
+ let trigger_tasks = trigger_processes
199
+ . into_iter ( )
200
+ . map ( |mut ch| tokio:: task:: spawn ( async move { ch. wait ( ) . await } ) )
201
+ . collect :: < Vec < _ > > ( ) ;
202
+
203
+ if is_multi {
204
+ tokio:: time:: sleep ( MULTI_TRIGGER_LET_ALL_START ) . await ;
205
+ }
192
206
193
- let first_to_finish = trigger_tasks . next ( ) . await ;
207
+ let ( first_to_finish, _index , _rest ) = futures :: future :: select_all ( trigger_tasks ) . await ;
194
208
195
- if let Some ( process_result) = first_to_finish {
209
+ if let Ok ( process_result) = first_to_finish {
196
210
let status = process_result?;
197
211
if !status. success ( ) {
212
+ if is_multi {
213
+ println ! ( "A trigger exited unexpectedly. Terminating." ) ;
214
+ kill_child_processes ( & pids) ;
215
+ }
198
216
return Err ( crate :: subprocess:: ExitStatusError :: new ( status) . into ( ) ) ;
199
217
}
200
218
}
@@ -223,6 +241,7 @@ impl UpCommand {
223
241
trigger_cmds : Vec < Vec < String > > ,
224
242
run_opts : RunTriggerOpts ,
225
243
) -> anyhow:: Result < Vec < tokio:: process:: Child > > {
244
+ let is_multi = trigger_cmds. len ( ) > 1 ;
226
245
let mut trigger_processes = Vec :: with_capacity ( trigger_cmds. len ( ) ) ;
227
246
228
247
for cmd in trigger_cmds {
@@ -231,6 +250,13 @@ impl UpCommand {
231
250
. await
232
251
. context ( "Failed to start trigger process" ) ?;
233
252
trigger_processes. push ( child) ;
253
+
254
+ if is_multi {
255
+ // Allow time for the child `spin` process to launch the trigger
256
+ // and hook up its cancellation. Mitigates the race condition
257
+ // noted on the constant (see there for more info).
258
+ tokio:: time:: sleep ( MULTI_TRIGGER_START_OFFSET ) . await ;
259
+ }
234
260
}
235
261
236
262
Ok ( trigger_processes)
@@ -387,25 +413,45 @@ impl UpCommand {
387
413
}
388
414
389
415
#[ cfg( windows) ]
390
- fn set_kill_on_ctrl_c ( trigger_processes : & Vec < tokio :: process :: Child > ) -> Result < ( ) , anyhow:: Error > {
416
+ fn set_kill_on_ctrl_c ( _pids : & [ usize ] ) -> Result < ( ) , anyhow:: Error > {
391
417
Ok ( ( ) )
392
418
}
393
419
394
420
#[ cfg( not( windows) ) ]
395
- fn set_kill_on_ctrl_c ( trigger_processes : & [ tokio:: process:: Child ] ) -> Result < ( ) , anyhow:: Error > {
421
+ fn set_kill_on_ctrl_c ( pids : & [ nix:: unistd:: Pid ] ) -> Result < ( ) , anyhow:: Error > {
422
+ let pids = pids. to_owned ( ) ;
423
+ ctrlc:: set_handler ( move || {
424
+ kill_child_processes ( & pids) ;
425
+ } ) ?;
426
+ Ok ( ( ) )
427
+ }
428
+
429
+ #[ cfg( windows) ]
430
+ fn get_pids ( _trigger_processes : & [ tokio:: process:: Child ] ) -> Vec < usize > {
431
+ vec ! [ ]
432
+ }
433
+
434
+ #[ cfg( not( windows) ) ]
435
+ fn get_pids ( trigger_processes : & [ tokio:: process:: Child ] ) -> Vec < nix:: unistd:: Pid > {
436
+ use itertools:: Itertools ;
396
437
// https://github.com/nix-rust/nix/issues/656
397
- let pids = trigger_processes
438
+ trigger_processes
398
439
. iter ( )
399
440
. flat_map ( |child| child. id ( ) . map ( |id| nix:: unistd:: Pid :: from_raw ( id as i32 ) ) )
400
- . collect_vec ( ) ;
401
- ctrlc:: set_handler ( move || {
402
- for pid in & pids {
403
- if let Err ( err) = nix:: sys:: signal:: kill ( * pid, nix:: sys:: signal:: SIGTERM ) {
404
- tracing:: warn!( "Failed to kill trigger handler process: {:?}" , err)
405
- }
441
+ . collect_vec ( )
442
+ }
443
+
444
+ #[ cfg( windows) ]
445
+ fn kill_child_processes ( _pids : & [ usize ] ) { }
446
+
447
+ #[ cfg( not( windows) ) ]
448
+ fn kill_child_processes ( pids : & [ nix:: unistd:: Pid ] ) {
449
+ // https://github.com/nix-rust/nix/issues/656
450
+ for pid in pids {
451
+ if let Err ( err) = nix:: sys:: signal:: kill ( * pid, nix:: sys:: signal:: SIGTERM ) {
452
+ tracing:: warn!( "Failed to kill trigger handler process: {:?}" , err)
406
453
}
407
- } ) ?;
408
- Ok ( ( ) )
454
+ }
409
455
}
410
456
411
457
#[ derive( Clone ) ]
0 commit comments