Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/20250821120026.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:sparkles: `proc` Add support for gracefully killing processes with specific interrupts
53 changes: 41 additions & 12 deletions utils/proc/interrupt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -77,34 +93,39 @@ func TerminateGracefullyWithChildren(ctx context.Context, pid int, gracePeriod t
}

if len(children) == 0 {
err = TerminateGracefully(ctx, pid, gracePeriod)
err = terminate(ctx, pid, gracePeriod)
return
}

childGroup, terminateCtx := errgroup.WithContext(ctx)
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()
if err != nil {
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 {
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
}
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,
Expand All @@ -119,10 +140,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
}
Expand Down
12 changes: 12 additions & 0 deletions utils/proc/interrupt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading