@@ -33,6 +33,10 @@ public actor ServiceGroup: Sendable {
33
33
private let logger : Logger
34
34
/// The logging configuration.
35
35
private let loggingConfiguration : ServiceGroupConfiguration . LoggingConfiguration
36
+ /// The maximum amount of time that graceful shutdown is allowed to take.
37
+ private let maximumGracefulShutdownDuration : ( secondsComponent: Int64 , attosecondsComponent: Int64 ) ?
38
+ /// The maximum amount of time that task cancellation is allowed to take.
39
+ private let maximumCancellationDuration : ( secondsComponent: Int64 , attosecondsComponent: Int64 ) ?
36
40
/// The signals that lead to graceful shutdown.
37
41
private let gracefulShutdownSignals : [ UnixSignal ]
38
42
/// The signals that lead to cancellation.
@@ -57,6 +61,8 @@ public actor ServiceGroup: Sendable {
57
61
self . cancellationSignals = configuration. cancellationSignals
58
62
self . logger = configuration. logger
59
63
self . loggingConfiguration = configuration. logging
64
+ self . maximumGracefulShutdownDuration = configuration. _maximumGracefulShutdownDuration
65
+ self . maximumCancellationDuration = configuration. _maximumCancellationDuration
60
66
}
61
67
62
68
/// Initializes a new ``ServiceGroup``.
@@ -94,6 +100,8 @@ public actor ServiceGroup: Sendable {
94
100
self . cancellationSignals = configuration. cancellationSignals
95
101
self . logger = logger
96
102
self . loggingConfiguration = configuration. logging
103
+ self . maximumGracefulShutdownDuration = configuration. _maximumGracefulShutdownDuration
104
+ self . maximumCancellationDuration = configuration. _maximumCancellationDuration
97
105
}
98
106
99
107
/// Runs all the services by spinning up a child task per service.
@@ -176,6 +184,8 @@ public actor ServiceGroup: Sendable {
176
184
case signalSequenceFinished
177
185
case gracefulShutdownCaught
178
186
case gracefulShutdownFinished
187
+ case gracefulShutdownTimedOut
188
+ case cancellationCaught
179
189
}
180
190
181
191
private func _run(
@@ -191,6 +201,10 @@ public actor ServiceGroup: Sendable {
191
201
]
192
202
)
193
203
204
+ // A task that is spawned when we got cancelled or
205
+ // we cancel the task group to keep track of a timeout.
206
+ var cancellationTimeoutTask : Task < Void , Never > ?
207
+
194
208
// Using a result here since we want a task group that has non-throwing child tasks
195
209
// but the body itself is throwing
196
210
let result = try await withThrowingTaskGroup ( of: ChildTaskResult . self, returning: Result< Void, Error> . self ) { group in
@@ -267,6 +281,13 @@ public actor ServiceGroup: Sendable {
267
281
}
268
282
}
269
283
284
+ group. addTask {
285
+ // This child task is waiting forever until the group gets cancelled.
286
+ let ( stream, _) = AsyncStream . makeStream ( of: Void . self)
287
+ await stream. first { _ in true }
288
+ return . cancellationCaught
289
+ }
290
+
270
291
// We are storing the services in an optional array now. When a slot in the array is
271
292
// empty it indicates that the service has been shutdown.
272
293
var services = services. map { Optional ( $0) }
@@ -293,7 +314,7 @@ public actor ServiceGroup: Sendable {
293
314
self . loggingConfiguration. keys. serviceKey: " \( service. service) " ,
294
315
]
295
316
)
296
- group . cancelAll ( )
317
+ self . cancelGroupAndSpawnTimeoutIfNeeded ( group : & group , cancellationTimeoutTask : & cancellationTimeoutTask )
297
318
return . failure( ServiceGroupError . serviceFinishedUnexpectedly ( ) )
298
319
299
320
case . gracefullyShutdownGroup:
@@ -307,6 +328,7 @@ public actor ServiceGroup: Sendable {
307
328
do {
308
329
try await self . shutdownGracefully (
309
330
services: services,
331
+ cancellationTimeoutTask: & cancellationTimeoutTask,
310
332
group: & group,
311
333
gracefulShutdownManagers: gracefulShutdownManagers
312
334
)
@@ -327,7 +349,7 @@ public actor ServiceGroup: Sendable {
327
349
self . logger. debug (
328
350
" All services finished. "
329
351
)
330
- group . cancelAll ( )
352
+ self . cancelGroupAndSpawnTimeoutIfNeeded ( group : & group , cancellationTimeoutTask : & cancellationTimeoutTask )
331
353
return . success( ( ) )
332
354
}
333
355
}
@@ -342,7 +364,7 @@ public actor ServiceGroup: Sendable {
342
364
self . loggingConfiguration. keys. errorKey: " \( serviceError) " ,
343
365
]
344
366
)
345
- group . cancelAll ( )
367
+ self . cancelGroupAndSpawnTimeoutIfNeeded ( group : & group , cancellationTimeoutTask : & cancellationTimeoutTask )
346
368
return . failure( serviceError)
347
369
348
370
case . gracefullyShutdownGroup:
@@ -358,6 +380,7 @@ public actor ServiceGroup: Sendable {
358
380
do {
359
381
try await self . shutdownGracefully (
360
382
services: services,
383
+ cancellationTimeoutTask: & cancellationTimeoutTask,
361
384
group: & group,
362
385
gracefulShutdownManagers: gracefulShutdownManagers
363
386
)
@@ -381,7 +404,7 @@ public actor ServiceGroup: Sendable {
381
404
" All services finished. "
382
405
)
383
406
384
- group . cancelAll ( )
407
+ self . cancelGroupAndSpawnTimeoutIfNeeded ( group : & group , cancellationTimeoutTask : & cancellationTimeoutTask )
385
408
return . success( ( ) )
386
409
}
387
410
}
@@ -398,6 +421,7 @@ public actor ServiceGroup: Sendable {
398
421
do {
399
422
try await self . shutdownGracefully (
400
423
services: services,
424
+ cancellationTimeoutTask: & cancellationTimeoutTask,
401
425
group: & group,
402
426
gracefulShutdownManagers: gracefulShutdownManagers
403
427
)
@@ -413,7 +437,7 @@ public actor ServiceGroup: Sendable {
413
437
]
414
438
)
415
439
416
- group . cancelAll ( )
440
+ self . cancelGroupAndSpawnTimeoutIfNeeded ( group : & group , cancellationTimeoutTask : & cancellationTimeoutTask )
417
441
}
418
442
419
443
case . gracefulShutdownCaught:
@@ -423,19 +447,29 @@ public actor ServiceGroup: Sendable {
423
447
do {
424
448
try await self . shutdownGracefully (
425
449
services: services,
450
+ cancellationTimeoutTask: & cancellationTimeoutTask,
426
451
group: & group,
427
452
gracefulShutdownManagers: gracefulShutdownManagers
428
453
)
429
454
} catch {
430
455
return . failure( error)
431
456
}
432
457
458
+ case . cancellationCaught:
459
+ // We caught cancellation in our child task so we have to spawn
460
+ // our cancellation timeout task if needed
461
+ self . logger. debug ( " Caught cancellation. " )
462
+ self . cancelGroupAndSpawnTimeoutIfNeeded ( group: & group, cancellationTimeoutTask: & cancellationTimeoutTask)
463
+
433
464
case . signalSequenceFinished, . gracefulShutdownFinished:
434
465
// This can happen when we are either cancelling everything or
435
466
// when the user did not specify any shutdown signals. We just have to tolerate
436
467
// this.
437
468
continue
438
469
470
+ case . gracefulShutdownTimedOut:
471
+ fatalError ( " Received gracefulShutdownTimedOut but never triggered a graceful shutdown " )
472
+
439
473
case nil :
440
474
fatalError ( " Invalid result from group.next(). We checked if the group is empty before and still got nil " )
441
475
}
@@ -447,18 +481,30 @@ public actor ServiceGroup: Sendable {
447
481
self . logger. debug (
448
482
" Service lifecycle ended "
449
483
)
484
+ cancellationTimeoutTask? . cancel ( )
450
485
try result. get ( )
451
486
}
452
487
453
488
private func shutdownGracefully(
454
489
services: [ ServiceGroupConfiguration . ServiceConfiguration ? ] ,
490
+ cancellationTimeoutTask: inout Task < Void , Never > ? ,
455
491
group: inout ThrowingTaskGroup < ChildTaskResult , Error > ,
456
492
gracefulShutdownManagers: [ GracefulShutdownManager ]
457
493
) async throws {
458
494
guard case . running = self . state else {
459
495
fatalError ( " Unexpected state " )
460
496
}
461
497
498
+ if #available( macOS 13 . 0 , iOS 16 . 0 , watchOS 9 . 0 , tvOS 16 . 0 , * ) , let maximumGracefulShutdownDuration = self . maximumGracefulShutdownDuration {
499
+ group. addTask {
500
+ try ? await Task . sleep ( for: Duration (
501
+ secondsComponent: maximumGracefulShutdownDuration. secondsComponent,
502
+ attosecondsComponent: maximumGracefulShutdownDuration. attosecondsComponent
503
+ ) )
504
+ return . gracefulShutdownTimedOut
505
+ }
506
+ }
507
+
462
508
// We are storing the first error of a service that threw here.
463
509
var error : Error ?
464
510
@@ -509,7 +555,7 @@ public actor ServiceGroup: Sendable {
509
555
]
510
556
)
511
557
512
- group . cancelAll ( )
558
+ self . cancelGroupAndSpawnTimeoutIfNeeded ( group : & group , cancellationTimeoutTask : & cancellationTimeoutTask )
513
559
throw ServiceGroupError . serviceFinishedUnexpectedly ( )
514
560
}
515
561
@@ -561,9 +607,26 @@ public actor ServiceGroup: Sendable {
561
607
]
562
608
)
563
609
564
- group . cancelAll ( )
610
+ self . cancelGroupAndSpawnTimeoutIfNeeded ( group : & group , cancellationTimeoutTask : & cancellationTimeoutTask )
565
611
}
566
612
613
+ case . gracefulShutdownTimedOut:
614
+ // Gracefully shutting down took longer than the user configured
615
+ // so we have to escalate it now.
616
+ self . logger. debug (
617
+ " Graceful shutdown took longer than allowed by the configuration. Cancelling the group now. " ,
618
+ metadata: [
619
+ self . loggingConfiguration. keys. serviceKey: " \( service. service) " ,
620
+ ]
621
+ )
622
+ self . cancelGroupAndSpawnTimeoutIfNeeded ( group: & group, cancellationTimeoutTask: & cancellationTimeoutTask)
623
+
624
+ case . cancellationCaught:
625
+ // We caught cancellation in our child task so we have to spawn
626
+ // our cancellation timeout task if needed
627
+ self . logger. debug ( " Caught cancellation. " )
628
+ self . cancelGroupAndSpawnTimeoutIfNeeded ( group: & group, cancellationTimeoutTask: & cancellationTimeoutTask)
629
+
567
630
case . signalSequenceFinished, . gracefulShutdownCaught, . gracefulShutdownFinished:
568
631
// We just have to tolerate this since signals and parent graceful shutdowns downs can race.
569
632
continue
@@ -575,7 +638,9 @@ public actor ServiceGroup: Sendable {
575
638
576
639
// If we hit this then all services are shutdown. The only thing remaining
577
640
// are the tasks that listen to the various graceful shutdown signals. We
578
- // just have to cancel those
641
+ // just have to cancel those.
642
+ // In this case we don't have to spawn our cancellation timeout task since
643
+ // we are sure all other child tasks are handling cancellation appropriately.
579
644
group. cancelAll ( )
580
645
581
646
// If we saw an error during graceful shutdown from a service that triggers graceful
@@ -584,6 +649,45 @@ public actor ServiceGroup: Sendable {
584
649
throw error
585
650
}
586
651
}
652
+
653
+ private func cancelGroupAndSpawnTimeoutIfNeeded(
654
+ group: inout ThrowingTaskGroup < ChildTaskResult , Error > ,
655
+ cancellationTimeoutTask: inout Task < Void , Never > ?
656
+ ) {
657
+ guard cancellationTimeoutTask == nil else {
658
+ // We already have a cancellation timeout task running.
659
+ self . logger. debug (
660
+ " Task cancellation timeout task already running. "
661
+ )
662
+ return
663
+ }
664
+ group. cancelAll ( )
665
+
666
+ if #available( macOS 13 . 0 , iOS 16 . 0 , watchOS 9 . 0 , tvOS 16 . 0 , * ) , let maximumCancellationDuration = self . maximumCancellationDuration {
667
+ // We have to spawn an unstructured task here because the call to our `run`
668
+ // method might have already been cancelled and we need to protect the sleep
669
+ // from being cancelled.
670
+ cancellationTimeoutTask = Task {
671
+ do {
672
+ self . logger. debug (
673
+ " Task cancellation timeout task started. "
674
+ )
675
+ try await Task . sleep ( for: Duration (
676
+ secondsComponent: maximumCancellationDuration. secondsComponent,
677
+ attosecondsComponent: maximumCancellationDuration. attosecondsComponent
678
+ ) )
679
+ self . logger. debug (
680
+ " Cancellation took longer than allowed by the configuration. "
681
+ )
682
+ fatalError ( " Cancellation took longer than allowed by the configuration. " )
683
+ } catch {
684
+ // We got cancelled so our services must have finished up.
685
+ }
686
+ }
687
+ } else {
688
+ cancellationTimeoutTask = nil
689
+ }
690
+ }
587
691
}
588
692
589
693
// This should be removed once we support Swift 5.9+
0 commit comments