github.com/chenbh/concourse/v6@v6.4.2/worker/runtime/killer.go (about)

     1  package runtime
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"syscall"
     8  	"time"
     9  
    10  	"github.com/containerd/containerd"
    11  	"github.com/containerd/containerd/cio"
    12  	"github.com/containerd/containerd/runtime/v2/runc/options"
    13  	"github.com/containerd/typeurl"
    14  )
    15  
    16  const (
    17  	// GracefulSignal is the signal sent to processes when giving them the
    18  	// opportunity to shut themselves down by their own means.
    19  	//
    20  	GracefulSignal = syscall.SIGTERM
    21  
    22  	// UngracefulSignal is the signal sent to the init process in the pid
    23  	// namespace to force its shutdown.
    24  	//
    25  	UngracefulSignal = syscall.SIGKILL
    26  
    27  	// GracePeriod is the duration by which a graceful killer would let a
    28  	// set of processes finish by themselves before going ungraceful.
    29  	//
    30  	GracePeriod = 10 * time.Second
    31  )
    32  
    33  type KillBehaviour bool
    34  
    35  const (
    36  	KillGracefully   KillBehaviour = false
    37  	KillUngracefully KillBehaviour = true
    38  )
    39  
    40  //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 . Killer
    41  
    42  // Killer terminates tasks.
    43  //
    44  type Killer interface {
    45  	// Kill terminates a task either with a specific behaviour.
    46  	//
    47  	Kill(
    48  		ctx context.Context,
    49  		task containerd.Task,
    50  		behaviour KillBehaviour,
    51  	) error
    52  }
    53  
    54  // killer terminates the processes exec'ed in a task.
    55  //
    56  // Only processes created through `task.Exec` are targetted to receive the the
    57  // first signals it delivers.
    58  //
    59  type killer struct {
    60  	gracePeriod   time.Duration
    61  	processKiller ProcessKiller
    62  }
    63  
    64  // KillerOpt is a functional option that modifies the behavior of a killer.
    65  //
    66  type KillerOpt func(k *killer)
    67  
    68  // WithProcessKiller modifies the default process killer used by the task
    69  // killer.
    70  //
    71  func WithProcessKiller(f ProcessKiller) KillerOpt {
    72  	return func(k *killer) {
    73  		k.processKiller = f
    74  	}
    75  }
    76  
    77  // WithGracePeriod configures the grace period used when waiting for a process
    78  // to be gracefully finished.
    79  //
    80  func WithGracePeriod(p time.Duration) KillerOpt {
    81  	return func(k *killer) {
    82  		k.gracePeriod = p
    83  	}
    84  }
    85  
    86  func NewKiller(opts ...KillerOpt) *killer {
    87  	k := &killer{
    88  		gracePeriod:   GracePeriod,
    89  		processKiller: NewProcessKiller(),
    90  	}
    91  
    92  	for _, opt := range opts {
    93  		opt(k)
    94  	}
    95  
    96  	return k
    97  }
    98  
    99  // Kill delivers a signal to each exec'ed process in the task.
   100  //
   101  func (k killer) Kill(ctx context.Context, task containerd.Task, behaviour KillBehaviour) error {
   102  	switch behaviour {
   103  	case KillGracefully:
   104  		success, err := k.gracefullyKill(ctx, task)
   105  		if err != nil {
   106  			return fmt.Errorf("graceful kill: %w", err)
   107  		}
   108  		if !success {
   109  			err := k.ungracefullyKill(ctx, task)
   110  			if err != nil {
   111  				return fmt.Errorf("ungraceful kill: %w", err)
   112  			}
   113  		}
   114  	case KillUngracefully:
   115  		err := k.ungracefullyKill(ctx, task)
   116  		if err != nil {
   117  			return fmt.Errorf("ungraceful kill: %w", err)
   118  		}
   119  	}
   120  	return nil
   121  }
   122  
   123  func (k killer) ungracefullyKill(ctx context.Context, task containerd.Task) error {
   124  	err := k.killTaskExecedProcesses(ctx, task, UngracefulSignal)
   125  	if err != nil {
   126  		return fmt.Errorf("ungraceful kill task execed processes: %w", err)
   127  	}
   128  
   129  	return nil
   130  }
   131  
   132  func (k killer) gracefullyKill(ctx context.Context, task containerd.Task) (bool, error) {
   133  	err := k.killTaskExecedProcesses(ctx, task, GracefulSignal)
   134  	switch {
   135  	case errors.Is(err, ErrGracePeriodTimeout):
   136  		return false, nil
   137  	case err != nil:
   138  		return false, fmt.Errorf("kill task execed processes: %w", err)
   139  	}
   140  
   141  	return true, nil
   142  }
   143  
   144  // killTaskProcesses delivers a signal to every live process that has been
   145  // created through a `task.Exec`.
   146  //
   147  func (k killer) killTaskExecedProcesses(ctx context.Context, task containerd.Task, signal syscall.Signal) error {
   148  	procs, err := taskExecedProcesses(ctx, task)
   149  	if err != nil {
   150  		return fmt.Errorf("task execed processes: %w", err)
   151  	}
   152  
   153  	err = k.killProcesses(ctx, procs, signal)
   154  	if err != nil {
   155  		return fmt.Errorf("kill procs: %w", err)
   156  	}
   157  
   158  	return nil
   159  }
   160  
   161  // taskProcesses retrieves a task's processes.
   162  //
   163  func taskExecedProcesses(ctx context.Context, task containerd.Task) ([]containerd.Process, error) {
   164  	pids, err := task.Pids(context.Background())
   165  	if err != nil {
   166  		return nil, fmt.Errorf("pid listing: %w", err)
   167  	}
   168  
   169  	procs := []containerd.Process{}
   170  	for _, pid := range pids {
   171  		if pid.Info == nil { // init
   172  			continue
   173  		}
   174  
   175  		// the protobuf message has a "catch-all" field for `pid.Info`,
   176  		// thus, we need to unmarshal the message ourselves.
   177  		//
   178  		info, err := typeurl.UnmarshalAny(pid.Info)
   179  		if err != nil {
   180  			return nil, fmt.Errorf("proc details unmarshal: %w", err)
   181  		}
   182  
   183  		pinfo, ok := info.(*options.ProcessDetails)
   184  		if !ok {
   185  			return nil, fmt.Errorf("unknown proc detail type")
   186  		}
   187  
   188  		proc, err := task.LoadProcess(ctx, pinfo.ExecID, cio.Load)
   189  		if err != nil {
   190  			return nil, fmt.Errorf("load process: %w", err)
   191  		}
   192  
   193  		procs = append(procs, proc)
   194  	}
   195  
   196  	return procs, nil
   197  }
   198  
   199  // killProcesses takes care of delivering a termination signal to a set of
   200  // processes and waiting for their statuses.
   201  //
   202  func (k killer) killProcesses(ctx context.Context, procs []containerd.Process, signal syscall.Signal) error {
   203  
   204  	// TODO - this could (probably *should*) be concurrent
   205  	//
   206  
   207  	for _, proc := range procs {
   208  		err := k.processKiller.Kill(ctx, proc, signal, k.gracePeriod)
   209  		if err != nil {
   210  			return fmt.Errorf("proc kill: %w", err)
   211  		}
   212  	}
   213  
   214  	return nil
   215  }