From d05695fd0e7a4e09bd262365c973e7966bd1117b Mon Sep 17 00:00:00 2001 From: joshjennings98 Date: Thu, 21 Aug 2025 12:00:34 +0100 Subject: [PATCH 1/2] :sparkles: Add support for gracefully killing processes with specific interrupts --- changes/20250821120026.feature | 1 + utils/proc/interrupt.go | 54 ++++++++++++++++++++++++++-------- utils/proc/interrupt_test.go | 12 ++++++++ 3 files changed, 54 insertions(+), 13 deletions(-) create mode 100644 changes/20250821120026.feature diff --git a/changes/20250821120026.feature b/changes/20250821120026.feature new file mode 100644 index 0000000000..5b1bd3361a --- /dev/null +++ b/changes/20250821120026.feature @@ -0,0 +1 @@ +:sparkles: `proc` Add support for gracefully killing processes with specific interrupts diff --git a/utils/proc/interrupt.go b/utils/proc/interrupt.go index 621c6f9322..c6983b0ff5 100644 --- a/utils/proc/interrupt.go +++ b/utils/proc/interrupt.go @@ -47,13 +47,29 @@ func InterruptProcess(ctx context.Context, pid int, signal InterruptType) (err e return } -// TerminateGracefullyWithChildren follows the pattern set by [kubernetes](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination) and terminates processes gracefully by first sending a SIGTERM and then a SIGKILL after the grace period has elapsed. +// TerminateGracefullyWithChildren follows the pattern set by [kubernetes](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination) and terminates processes gracefully by first sending a SIGINT+SIGTERM and then a SIGKILL after the grace period has elapsed. // It does not attempt to terminate the process group. If you wish to terminate the process group directly then send -pgid to TerminateGracefully but // this does not guarantee that the group will be terminated gracefully. // Instead, this function lists each child and attempts to kill them gracefully concurrently. It will then attempt to gracefully terminate itself. // Due to the multi-stage process and the fact that the full grace period must pass for each stage specified above, the total maximum length of this // function will be 2*gracePeriod not gracePeriod. func TerminateGracefullyWithChildren(ctx context.Context, pid int, gracePeriod time.Duration) (err error) { + return terminateGracefullyWithChildren(ctx, pid, gracePeriod, TerminateGracefully) +} + +// TerminateGracefullyWithChildrenWithSpecificInterrupts follows the pattern set by [kubernetes](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination) and terminates processes gracefully by first sending the specified interrupts and then a SIGKILL after the grace period has elapsed. +// It does not attempt to terminate the process group. If you wish to terminate the process group directly then send -pgid to TerminateGracefully but +// this does not guarantee that the group will be terminated gracefully. +// Instead, this function lists each child and attempts to kill them gracefully concurrently. It will then attempt to gracefully terminate itself. +// Due to the multi-stage process and the fact that the full grace period must pass for each stage specified above, the total maximum length of this +// function will be 2*gracePeriod not gracePeriod. +func TerminateGracefullyWithChildrenWithSpecificInterrupts(ctx context.Context, pid int, gracePeriod time.Duration, interrupts ...InterruptType) (err error) { + return terminateGracefullyWithChildren(ctx, pid, gracePeriod, func(ctx context.Context, pid int, gracePeriod time.Duration) error { + return TerminateGracefullyWithSpecificInterrupts(ctx, pid, gracePeriod, interrupts...) + }) +} + +func terminateGracefullyWithChildren(ctx context.Context, pid int, gracePeriod time.Duration, terminate func(context.Context, int, time.Duration) error) (err error) { err = parallelisation.DetermineContextError(ctx) if err != nil { return @@ -77,7 +93,7 @@ func TerminateGracefullyWithChildren(ctx context.Context, pid int, gracePeriod t } if len(children) == 0 { - err = TerminateGracefully(ctx, pid, gracePeriod) + err = terminate(ctx, pid, gracePeriod) return } @@ -85,7 +101,9 @@ func TerminateGracefullyWithChildren(ctx context.Context, pid int, gracePeriod t childGroup.SetLimit(len(children)) for _, child := range children { if child.IsRunning() { - childGroup.Go(func() error { return TerminateGracefullyWithChildren(terminateCtx, child.Pid(), gracePeriod) }) + childGroup.Go(func() error { + return terminateGracefullyWithChildren(terminateCtx, child.Pid(), gracePeriod, terminate) + }) } } err = childGroup.Wait() @@ -93,18 +111,20 @@ func TerminateGracefullyWithChildren(ctx context.Context, pid int, gracePeriod t return } - err = TerminateGracefully(ctx, pid, gracePeriod) + err = terminate(ctx, pid, gracePeriod) return } -func terminateGracefully(ctx context.Context, pid int, gracePeriod time.Duration) (err error) { - err = InterruptProcess(ctx, pid, SigInt) - if err != nil { - return +func terminateGracefully(ctx context.Context, pid int, gracePeriod time.Duration, interrupts ...InterruptType) (err error) { + if len(interrupts) == 0 { + err = commonerrors.New(commonerrors.ErrInvalid, "at least one interrupt must be provided") } - err = InterruptProcess(ctx, pid, SigTerm) - if err != nil { - return + + for _, interrupt := range interrupts { + err = InterruptProcess(ctx, pid, interrupt) + if err != nil { + return + } } return parallelisation.RunActionWithParallelCheck(ctx, @@ -119,10 +139,18 @@ func terminateGracefully(ctx context.Context, pid int, gracePeriod time.Duration }, 200*time.Millisecond) } -// TerminateGracefully follows the pattern set by [kubernetes](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination) and terminates processes gracefully by first sending a SIGTERM and then a SIGKILL after the grace period has elapsed. +// TerminateGracefully follows the pattern set by [kubernetes](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination) and terminates processes gracefully by first sending a SIGINT+SIGTERM and then a SIGKILL after the grace period has elapsed. func TerminateGracefully(ctx context.Context, pid int, gracePeriod time.Duration) (err error) { defer func() { _ = InterruptProcess(context.Background(), pid, SigKill) }() - _ = terminateGracefully(ctx, pid, gracePeriod) + _ = terminateGracefully(ctx, pid, gracePeriod, SigInt, SigTerm) + err = InterruptProcess(ctx, pid, SigKill) + return +} + +// TerminateGracefullyWithSpecificInterrupts follows the pattern set by [kubernetes](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination) and terminates processes gracefully by first sending the specified interrupts and then a SIGKILL after the grace period has elapsed. +func TerminateGracefullyWithSpecificInterrupts(ctx context.Context, pid int, gracePeriod time.Duration, interrupts ...InterruptType) (err error) { + defer func() { _ = InterruptProcess(context.Background(), pid, SigKill) }() + _ = terminateGracefully(ctx, pid, gracePeriod, interrupts...) err = InterruptProcess(ctx, pid, SigKill) return } diff --git a/utils/proc/interrupt_test.go b/utils/proc/interrupt_test.go index 7a845e6207..f2da8a3df7 100644 --- a/utils/proc/interrupt_test.go +++ b/utils/proc/interrupt_test.go @@ -26,10 +26,22 @@ func TestTerminateGracefully(t *testing.T) { name: "TerminateGracefully", testFunc: TerminateGracefully, }, + { + name: "TerminateGracefullyWithSpecificInterrupts", + testFunc: func(ctx context.Context, pid int, gracePeriod time.Duration) error { + return TerminateGracefullyWithSpecificInterrupts(ctx, pid, gracePeriod, SigInt) + }, + }, { name: "TerminateGracefullyWithChildren", testFunc: TerminateGracefullyWithChildren, }, + { + name: "TerminateGracefullyWithChildrenWithSpecificInterrupts", + testFunc: func(ctx context.Context, pid int, gracePeriod time.Duration) error { + return TerminateGracefullyWithChildrenWithSpecificInterrupts(ctx, pid, gracePeriod, SigInt) + }, + }, } { t.Run(test.name, func(t *testing.T) { defer goleak.VerifyNone(t) From d1550b3f73659c557380be693e4a1b4454536b0d Mon Sep 17 00:00:00 2001 From: joshjennings98 Date: Thu, 21 Aug 2025 12:05:14 +0100 Subject: [PATCH 2/2] lint --- utils/proc/interrupt.go | 1 + 1 file changed, 1 insertion(+) diff --git a/utils/proc/interrupt.go b/utils/proc/interrupt.go index c6983b0ff5..3da3acd8cf 100644 --- a/utils/proc/interrupt.go +++ b/utils/proc/interrupt.go @@ -118,6 +118,7 @@ func terminateGracefullyWithChildren(ctx context.Context, pid int, gracePeriod t func terminateGracefully(ctx context.Context, pid int, gracePeriod time.Duration, interrupts ...InterruptType) (err error) { if len(interrupts) == 0 { err = commonerrors.New(commonerrors.ErrInvalid, "at least one interrupt must be provided") + return } for _, interrupt := range interrupts {