github.com/maier/nomad@v0.4.1-0.20161110003312-a9e3d0b8549d/client/alloc_runner.go (about)

     1  package client
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"os"
     7  	"path/filepath"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/hashicorp/go-multierror"
    12  	"github.com/hashicorp/nomad/client/allocdir"
    13  	"github.com/hashicorp/nomad/client/config"
    14  	"github.com/hashicorp/nomad/client/driver"
    15  	"github.com/hashicorp/nomad/client/vaultclient"
    16  	"github.com/hashicorp/nomad/nomad/structs"
    17  
    18  	cstructs "github.com/hashicorp/nomad/client/structs"
    19  )
    20  
    21  const (
    22  	// taskReceivedSyncLimit is how long the client will wait before sending
    23  	// that a task was received to the server. The client does not immediately
    24  	// send that the task was received to the server because another transition
    25  	// to running or failed is likely to occur immediately after and a single
    26  	// update will transfer all past state information. If not other transition
    27  	// has occurred up to this limit, we will send to the server.
    28  	taskReceivedSyncLimit = 30 * time.Second
    29  )
    30  
    31  // AllocStateUpdater is used to update the status of an allocation
    32  type AllocStateUpdater func(alloc *structs.Allocation)
    33  
    34  type AllocStatsReporter interface {
    35  	LatestAllocStats(taskFilter string) (*cstructs.AllocResourceUsage, error)
    36  }
    37  
    38  // AllocRunner is used to wrap an allocation and provide the execution context.
    39  type AllocRunner struct {
    40  	config  *config.Config
    41  	updater AllocStateUpdater
    42  	logger  *log.Logger
    43  
    44  	alloc                  *structs.Allocation
    45  	allocClientStatus      string // Explicit status of allocation. Set when there are failures
    46  	allocClientDescription string
    47  	allocLock              sync.Mutex
    48  
    49  	dirtyCh chan struct{}
    50  
    51  	ctx        *driver.ExecContext
    52  	ctxLock    sync.Mutex
    53  	tasks      map[string]*TaskRunner
    54  	taskStates map[string]*structs.TaskState
    55  	restored   map[string]struct{}
    56  	taskLock   sync.RWMutex
    57  
    58  	taskStatusLock sync.RWMutex
    59  
    60  	updateCh chan *structs.Allocation
    61  
    62  	vaultClient vaultclient.VaultClient
    63  
    64  	otherAllocDir *allocdir.AllocDir
    65  
    66  	destroy     bool
    67  	destroyCh   chan struct{}
    68  	destroyLock sync.Mutex
    69  	waitCh      chan struct{}
    70  
    71  	// serialize saveAllocRunnerState calls
    72  	persistLock sync.Mutex
    73  }
    74  
    75  // allocRunnerState is used to snapshot the state of the alloc runner
    76  type allocRunnerState struct {
    77  	Version                string
    78  	Alloc                  *structs.Allocation
    79  	AllocClientStatus      string
    80  	AllocClientDescription string
    81  	Context                *driver.ExecContext
    82  }
    83  
    84  // NewAllocRunner is used to create a new allocation context
    85  func NewAllocRunner(logger *log.Logger, config *config.Config, updater AllocStateUpdater,
    86  	alloc *structs.Allocation, vaultClient vaultclient.VaultClient) *AllocRunner {
    87  	ar := &AllocRunner{
    88  		config:      config,
    89  		updater:     updater,
    90  		logger:      logger,
    91  		alloc:       alloc,
    92  		dirtyCh:     make(chan struct{}, 1),
    93  		tasks:       make(map[string]*TaskRunner),
    94  		taskStates:  copyTaskStates(alloc.TaskStates),
    95  		restored:    make(map[string]struct{}),
    96  		updateCh:    make(chan *structs.Allocation, 64),
    97  		destroyCh:   make(chan struct{}),
    98  		waitCh:      make(chan struct{}),
    99  		vaultClient: vaultClient,
   100  	}
   101  	return ar
   102  }
   103  
   104  // stateFilePath returns the path to our state file
   105  func (r *AllocRunner) stateFilePath() string {
   106  	r.allocLock.Lock()
   107  	defer r.allocLock.Unlock()
   108  	path := filepath.Join(r.config.StateDir, "alloc", r.alloc.ID, "state.json")
   109  	return path
   110  }
   111  
   112  // RestoreState is used to restore the state of the alloc runner
   113  func (r *AllocRunner) RestoreState() error {
   114  	// Load the snapshot
   115  	var snap allocRunnerState
   116  	if err := restoreState(r.stateFilePath(), &snap); err != nil {
   117  		return err
   118  	}
   119  
   120  	// Restore fields
   121  	r.alloc = snap.Alloc
   122  	r.ctx = snap.Context
   123  	r.allocClientStatus = snap.AllocClientStatus
   124  	r.allocClientDescription = snap.AllocClientDescription
   125  
   126  	var snapshotErrors multierror.Error
   127  	if r.alloc == nil {
   128  		snapshotErrors.Errors = append(snapshotErrors.Errors, fmt.Errorf("alloc_runner snapshot includes a nil allocation"))
   129  	}
   130  	if r.ctx == nil {
   131  		snapshotErrors.Errors = append(snapshotErrors.Errors, fmt.Errorf("alloc_runner snapshot includes a nil context"))
   132  	}
   133  	if e := snapshotErrors.ErrorOrNil(); e != nil {
   134  		return e
   135  	}
   136  
   137  	r.taskStates = snap.Alloc.TaskStates
   138  
   139  	// Restore the task runners
   140  	var mErr multierror.Error
   141  	for name, state := range r.taskStates {
   142  		// Mark the task as restored.
   143  		r.restored[name] = struct{}{}
   144  
   145  		task := &structs.Task{Name: name}
   146  		tr := NewTaskRunner(r.logger, r.config, r.setTaskState, r.ctx, r.Alloc(),
   147  			task, r.vaultClient)
   148  		r.tasks[name] = tr
   149  
   150  		// Skip tasks in terminal states.
   151  		if state.State == structs.TaskStateDead {
   152  			continue
   153  		}
   154  
   155  		if err := tr.RestoreState(); err != nil {
   156  			r.logger.Printf("[ERR] client: failed to restore state for alloc %s task '%s': %v", r.alloc.ID, name, err)
   157  			mErr.Errors = append(mErr.Errors, err)
   158  		} else if !r.alloc.TerminalStatus() {
   159  			// Only start if the alloc isn't in a terminal status.
   160  			go tr.Run()
   161  		}
   162  	}
   163  
   164  	return mErr.ErrorOrNil()
   165  }
   166  
   167  // GetAllocDir returns the alloc dir for the alloc runner
   168  func (r *AllocRunner) GetAllocDir() *allocdir.AllocDir {
   169  	if r.ctx == nil {
   170  		return nil
   171  	}
   172  	return r.ctx.AllocDir
   173  }
   174  
   175  // SaveState is used to snapshot the state of the alloc runner
   176  // if the fullSync is marked as false only the state of the Alloc Runner
   177  // is snapshotted. If fullSync is marked as true, we snapshot
   178  // all the Task Runners associated with the Alloc
   179  func (r *AllocRunner) SaveState() error {
   180  	if err := r.saveAllocRunnerState(); err != nil {
   181  		return err
   182  	}
   183  
   184  	// Save state for each task
   185  	runners := r.getTaskRunners()
   186  	var mErr multierror.Error
   187  	for _, tr := range runners {
   188  		if err := r.saveTaskRunnerState(tr); err != nil {
   189  			mErr.Errors = append(mErr.Errors, err)
   190  		}
   191  	}
   192  	return mErr.ErrorOrNil()
   193  }
   194  
   195  func (r *AllocRunner) saveAllocRunnerState() error {
   196  	r.persistLock.Lock()
   197  	defer r.persistLock.Unlock()
   198  
   199  	// Create the snapshot.
   200  	alloc := r.Alloc()
   201  
   202  	r.allocLock.Lock()
   203  	allocClientStatus := r.allocClientStatus
   204  	allocClientDescription := r.allocClientDescription
   205  	r.allocLock.Unlock()
   206  
   207  	r.ctxLock.Lock()
   208  	ctx := r.ctx
   209  	r.ctxLock.Unlock()
   210  
   211  	snap := allocRunnerState{
   212  		Version:                r.config.Version,
   213  		Alloc:                  alloc,
   214  		Context:                ctx,
   215  		AllocClientStatus:      allocClientStatus,
   216  		AllocClientDescription: allocClientDescription,
   217  	}
   218  	return persistState(r.stateFilePath(), &snap)
   219  }
   220  
   221  func (r *AllocRunner) saveTaskRunnerState(tr *TaskRunner) error {
   222  	if err := tr.SaveState(); err != nil {
   223  		return fmt.Errorf("failed to save state for alloc %s task '%s': %v",
   224  			r.alloc.ID, tr.task.Name, err)
   225  	}
   226  	return nil
   227  }
   228  
   229  // DestroyState is used to cleanup after ourselves
   230  func (r *AllocRunner) DestroyState() error {
   231  	return os.RemoveAll(filepath.Dir(r.stateFilePath()))
   232  }
   233  
   234  // DestroyContext is used to destroy the context
   235  func (r *AllocRunner) DestroyContext() error {
   236  	return r.ctx.AllocDir.Destroy()
   237  }
   238  
   239  // copyTaskStates returns a copy of the passed task states.
   240  func copyTaskStates(states map[string]*structs.TaskState) map[string]*structs.TaskState {
   241  	copy := make(map[string]*structs.TaskState, len(states))
   242  	for task, state := range states {
   243  		copy[task] = state.Copy()
   244  	}
   245  	return copy
   246  }
   247  
   248  // Alloc returns the associated allocation
   249  func (r *AllocRunner) Alloc() *structs.Allocation {
   250  	r.allocLock.Lock()
   251  	alloc := r.alloc.Copy()
   252  
   253  	// The status has explicitly been set.
   254  	if r.allocClientStatus != "" || r.allocClientDescription != "" {
   255  		alloc.ClientStatus = r.allocClientStatus
   256  		alloc.ClientDescription = r.allocClientDescription
   257  
   258  		// Copy over the task states so we don't lose them
   259  		r.taskStatusLock.RLock()
   260  		alloc.TaskStates = copyTaskStates(r.taskStates)
   261  		r.taskStatusLock.RUnlock()
   262  
   263  		r.allocLock.Unlock()
   264  		return alloc
   265  	}
   266  	r.allocLock.Unlock()
   267  
   268  	// Scan the task states to determine the status of the alloc
   269  	var pending, running, dead, failed bool
   270  	r.taskStatusLock.RLock()
   271  	alloc.TaskStates = copyTaskStates(r.taskStates)
   272  	for _, state := range r.taskStates {
   273  		switch state.State {
   274  		case structs.TaskStateRunning:
   275  			running = true
   276  		case structs.TaskStatePending:
   277  			pending = true
   278  		case structs.TaskStateDead:
   279  			if state.Failed {
   280  				failed = true
   281  			} else {
   282  				dead = true
   283  			}
   284  		}
   285  	}
   286  	r.taskStatusLock.RUnlock()
   287  
   288  	// Determine the alloc status
   289  	if failed {
   290  		alloc.ClientStatus = structs.AllocClientStatusFailed
   291  	} else if running {
   292  		alloc.ClientStatus = structs.AllocClientStatusRunning
   293  	} else if pending {
   294  		alloc.ClientStatus = structs.AllocClientStatusPending
   295  	} else if dead {
   296  		alloc.ClientStatus = structs.AllocClientStatusComplete
   297  	}
   298  
   299  	return alloc
   300  }
   301  
   302  // dirtySyncState is used to watch for state being marked dirty to sync
   303  func (r *AllocRunner) dirtySyncState() {
   304  	for {
   305  		select {
   306  		case <-r.dirtyCh:
   307  			r.syncStatus()
   308  		case <-r.destroyCh:
   309  			return
   310  		}
   311  	}
   312  }
   313  
   314  // syncStatus is used to run and sync the status when it changes
   315  func (r *AllocRunner) syncStatus() error {
   316  	// Get a copy of our alloc, update status server side and sync to disk
   317  	alloc := r.Alloc()
   318  	r.updater(alloc)
   319  	return r.saveAllocRunnerState()
   320  }
   321  
   322  // setStatus is used to update the allocation status
   323  func (r *AllocRunner) setStatus(status, desc string) {
   324  	r.allocLock.Lock()
   325  	r.allocClientStatus = status
   326  	r.allocClientDescription = desc
   327  	r.allocLock.Unlock()
   328  	select {
   329  	case r.dirtyCh <- struct{}{}:
   330  	default:
   331  	}
   332  }
   333  
   334  // setTaskState is used to set the status of a task. If state is empty then the
   335  // event is appended but not synced with the server. The event may be omitted
   336  func (r *AllocRunner) setTaskState(taskName, state string, event *structs.TaskEvent) {
   337  	r.taskStatusLock.Lock()
   338  	defer r.taskStatusLock.Unlock()
   339  	taskState, ok := r.taskStates[taskName]
   340  	if !ok {
   341  		taskState = &structs.TaskState{}
   342  		r.taskStates[taskName] = taskState
   343  	}
   344  
   345  	// Set the tasks state.
   346  	if event != nil {
   347  		if event.FailsTask {
   348  			taskState.Failed = true
   349  		}
   350  		r.appendTaskEvent(taskState, event)
   351  	}
   352  
   353  	if state == "" {
   354  		return
   355  	}
   356  
   357  	taskState.State = state
   358  	if state == structs.TaskStateDead {
   359  		// If the task failed, we should kill all the other tasks in the task group.
   360  		if taskState.Failed {
   361  			var destroyingTasks []string
   362  			for task, tr := range r.tasks {
   363  				if task != taskName {
   364  					destroyingTasks = append(destroyingTasks, task)
   365  					tr.Destroy(structs.NewTaskEvent(structs.TaskSiblingFailed).SetFailedSibling(taskName))
   366  				}
   367  			}
   368  			if len(destroyingTasks) > 0 {
   369  				r.logger.Printf("[DEBUG] client: task %q failed, destroying other tasks in task group: %v", taskName, destroyingTasks)
   370  			}
   371  		}
   372  	}
   373  
   374  	select {
   375  	case r.dirtyCh <- struct{}{}:
   376  	default:
   377  	}
   378  }
   379  
   380  // appendTaskEvent updates the task status by appending the new event.
   381  func (r *AllocRunner) appendTaskEvent(state *structs.TaskState, event *structs.TaskEvent) {
   382  	capacity := 10
   383  	if state.Events == nil {
   384  		state.Events = make([]*structs.TaskEvent, 0, capacity)
   385  	}
   386  
   387  	// If we hit capacity, then shift it.
   388  	if len(state.Events) == capacity {
   389  		old := state.Events
   390  		state.Events = make([]*structs.TaskEvent, 0, capacity)
   391  		state.Events = append(state.Events, old[1:]...)
   392  	}
   393  
   394  	state.Events = append(state.Events, event)
   395  }
   396  
   397  // Run is a long running goroutine used to manage an allocation
   398  func (r *AllocRunner) Run() {
   399  	defer close(r.waitCh)
   400  	go r.dirtySyncState()
   401  
   402  	// Find the task group to run in the allocation
   403  	alloc := r.alloc
   404  	tg := alloc.Job.LookupTaskGroup(alloc.TaskGroup)
   405  	if tg == nil {
   406  		r.logger.Printf("[ERR] client: alloc '%s' for missing task group '%s'", alloc.ID, alloc.TaskGroup)
   407  		r.setStatus(structs.AllocClientStatusFailed, fmt.Sprintf("missing task group '%s'", alloc.TaskGroup))
   408  		return
   409  	}
   410  
   411  	// Create the execution context
   412  	r.ctxLock.Lock()
   413  	if r.ctx == nil {
   414  		allocDir := allocdir.NewAllocDir(filepath.Join(r.config.AllocDir, r.alloc.ID))
   415  		if err := allocDir.Build(tg.Tasks); err != nil {
   416  			r.logger.Printf("[WARN] client: failed to build task directories: %v", err)
   417  			r.setStatus(structs.AllocClientStatusFailed, fmt.Sprintf("failed to build task dirs for '%s'", alloc.TaskGroup))
   418  			r.ctxLock.Unlock()
   419  			return
   420  		}
   421  		r.ctx = driver.NewExecContext(allocDir, r.alloc.ID)
   422  		if r.otherAllocDir != nil {
   423  			if err := allocDir.Move(r.otherAllocDir, tg.Tasks); err != nil {
   424  				r.logger.Printf("[ERROR] client: failed to move alloc dir into alloc %q: %v", r.alloc.ID, err)
   425  			}
   426  			if err := r.otherAllocDir.Destroy(); err != nil {
   427  				r.logger.Printf("[ERROR] client: error destroying allocdir %v", r.otherAllocDir.AllocDir, err)
   428  			}
   429  		}
   430  	}
   431  	r.ctxLock.Unlock()
   432  
   433  	// Check if the allocation is in a terminal status. In this case, we don't
   434  	// start any of the task runners and directly wait for the destroy signal to
   435  	// clean up the allocation.
   436  	if alloc.TerminalStatus() {
   437  		r.logger.Printf("[DEBUG] client: alloc %q in terminal status, waiting for destroy", r.alloc.ID)
   438  		r.handleDestroy()
   439  		r.logger.Printf("[DEBUG] client: terminating runner for alloc '%s'", r.alloc.ID)
   440  		return
   441  	}
   442  
   443  	// Start the task runners
   444  	r.logger.Printf("[DEBUG] client: starting task runners for alloc '%s'", r.alloc.ID)
   445  	r.taskLock.Lock()
   446  	for _, task := range tg.Tasks {
   447  		if _, ok := r.restored[task.Name]; ok {
   448  			continue
   449  		}
   450  
   451  		tr := NewTaskRunner(r.logger, r.config, r.setTaskState, r.ctx, r.Alloc(), task.Copy(), r.vaultClient)
   452  		r.tasks[task.Name] = tr
   453  		tr.MarkReceived()
   454  
   455  		go tr.Run()
   456  	}
   457  	r.taskLock.Unlock()
   458  
   459  	// taskDestroyEvent contains an event that caused the destroyment of a task
   460  	// in the allocation.
   461  	var taskDestroyEvent *structs.TaskEvent
   462  
   463  OUTER:
   464  	// Wait for updates
   465  	for {
   466  		select {
   467  		case update := <-r.updateCh:
   468  			// Store the updated allocation.
   469  			r.allocLock.Lock()
   470  			r.alloc = update
   471  			r.allocLock.Unlock()
   472  
   473  			// Check if we're in a terminal status
   474  			if update.TerminalStatus() {
   475  				taskDestroyEvent = structs.NewTaskEvent(structs.TaskKilled)
   476  				break OUTER
   477  			}
   478  
   479  			// Update the task groups
   480  			runners := r.getTaskRunners()
   481  			for _, tr := range runners {
   482  				tr.Update(update)
   483  			}
   484  		case <-r.destroyCh:
   485  			taskDestroyEvent = structs.NewTaskEvent(structs.TaskKilled)
   486  			break OUTER
   487  		}
   488  	}
   489  
   490  	// Kill the task runners
   491  	r.destroyTaskRunners(taskDestroyEvent)
   492  
   493  	// Block until we should destroy the state of the alloc
   494  	r.handleDestroy()
   495  	r.logger.Printf("[DEBUG] client: terminating runner for alloc '%s'", r.alloc.ID)
   496  }
   497  
   498  // SetPreviousAllocDir sets the previous allocation directory of the current
   499  // allocation
   500  func (r *AllocRunner) SetPreviousAllocDir(allocDir *allocdir.AllocDir) {
   501  	r.otherAllocDir = allocDir
   502  }
   503  
   504  // destroyTaskRunners destroys the task runners, waits for them to terminate and
   505  // then saves state.
   506  func (r *AllocRunner) destroyTaskRunners(destroyEvent *structs.TaskEvent) {
   507  	// Destroy each sub-task
   508  	runners := r.getTaskRunners()
   509  	for _, tr := range runners {
   510  		tr.Destroy(destroyEvent)
   511  	}
   512  
   513  	// Wait for termination of the task runners
   514  	for _, tr := range runners {
   515  		<-tr.WaitCh()
   516  	}
   517  
   518  	// Final state sync
   519  	r.syncStatus()
   520  }
   521  
   522  // handleDestroy blocks till the AllocRunner should be destroyed and does the
   523  // necessary cleanup.
   524  func (r *AllocRunner) handleDestroy() {
   525  	select {
   526  	case <-r.destroyCh:
   527  		if err := r.DestroyContext(); err != nil {
   528  			r.logger.Printf("[ERR] client: failed to destroy context for alloc '%s': %v",
   529  				r.alloc.ID, err)
   530  		}
   531  		if err := r.DestroyState(); err != nil {
   532  			r.logger.Printf("[ERR] client: failed to destroy state for alloc '%s': %v",
   533  				r.alloc.ID, err)
   534  		}
   535  	}
   536  }
   537  
   538  // Update is used to update the allocation of the context
   539  func (r *AllocRunner) Update(update *structs.Allocation) {
   540  	select {
   541  	case r.updateCh <- update:
   542  	default:
   543  		r.logger.Printf("[ERR] client: dropping update to alloc '%s'", update.ID)
   544  	}
   545  }
   546  
   547  // StatsReporter returns an interface to query resource usage statistics of an
   548  // allocation
   549  func (r *AllocRunner) StatsReporter() AllocStatsReporter {
   550  	return r
   551  }
   552  
   553  // getTaskRunners is a helper that returns a copy of the task runners list using
   554  // the taskLock.
   555  func (r *AllocRunner) getTaskRunners() []*TaskRunner {
   556  	// Get the task runners
   557  	r.taskLock.RLock()
   558  	defer r.taskLock.RUnlock()
   559  	runners := make([]*TaskRunner, 0, len(r.tasks))
   560  	for _, tr := range r.tasks {
   561  		runners = append(runners, tr)
   562  	}
   563  	return runners
   564  }
   565  
   566  // LatestAllocStats returns the latest allocation stats. If the optional taskFilter is set
   567  // the allocation stats will only include the given task.
   568  func (r *AllocRunner) LatestAllocStats(taskFilter string) (*cstructs.AllocResourceUsage, error) {
   569  	astat := &cstructs.AllocResourceUsage{
   570  		Tasks: make(map[string]*cstructs.TaskResourceUsage),
   571  	}
   572  
   573  	var flat []*cstructs.TaskResourceUsage
   574  	if taskFilter != "" {
   575  		r.taskLock.RLock()
   576  		tr, ok := r.tasks[taskFilter]
   577  		r.taskLock.RUnlock()
   578  		if !ok {
   579  			return nil, fmt.Errorf("allocation %q has no task %q", r.alloc.ID, taskFilter)
   580  		}
   581  		l := tr.LatestResourceUsage()
   582  		if l != nil {
   583  			astat.Tasks[taskFilter] = l
   584  			flat = []*cstructs.TaskResourceUsage{l}
   585  			astat.Timestamp = l.Timestamp
   586  		}
   587  	} else {
   588  		// Get the task runners
   589  		runners := r.getTaskRunners()
   590  		for _, tr := range runners {
   591  			l := tr.LatestResourceUsage()
   592  			if l != nil {
   593  				astat.Tasks[tr.task.Name] = l
   594  				flat = append(flat, l)
   595  				if l.Timestamp > astat.Timestamp {
   596  					astat.Timestamp = l.Timestamp
   597  				}
   598  			}
   599  		}
   600  	}
   601  
   602  	astat.ResourceUsage = sumTaskResourceUsage(flat)
   603  	return astat, nil
   604  }
   605  
   606  // sumTaskResourceUsage takes a set of task resources and sums their resources
   607  func sumTaskResourceUsage(usages []*cstructs.TaskResourceUsage) *cstructs.ResourceUsage {
   608  	summed := &cstructs.ResourceUsage{
   609  		MemoryStats: &cstructs.MemoryStats{},
   610  		CpuStats:    &cstructs.CpuStats{},
   611  	}
   612  	for _, usage := range usages {
   613  		summed.Add(usage.ResourceUsage)
   614  	}
   615  	return summed
   616  }
   617  
   618  // shouldUpdate takes the AllocModifyIndex of an allocation sent from the server and
   619  // checks if the current running allocation is behind and should be updated.
   620  func (r *AllocRunner) shouldUpdate(serverIndex uint64) bool {
   621  	r.allocLock.Lock()
   622  	defer r.allocLock.Unlock()
   623  	return r.alloc.AllocModifyIndex < serverIndex
   624  }
   625  
   626  // Destroy is used to indicate that the allocation context should be destroyed
   627  func (r *AllocRunner) Destroy() {
   628  	r.destroyLock.Lock()
   629  	defer r.destroyLock.Unlock()
   630  
   631  	if r.destroy {
   632  		return
   633  	}
   634  	r.destroy = true
   635  	close(r.destroyCh)
   636  }
   637  
   638  // WaitCh returns a channel to wait for termination
   639  func (r *AllocRunner) WaitCh() <-chan struct{} {
   640  	return r.waitCh
   641  }