github.com/djenriquez/nomad-1@v0.8.1/nomad/core_sched.go (about)

     1  package nomad
     2  
     3  import (
     4  	"fmt"
     5  	"math"
     6  	"time"
     7  
     8  	memdb "github.com/hashicorp/go-memdb"
     9  	"github.com/hashicorp/nomad/nomad/state"
    10  	"github.com/hashicorp/nomad/nomad/structs"
    11  	"github.com/hashicorp/nomad/scheduler"
    12  )
    13  
    14  var (
    15  	// maxIdsPerReap is the maximum number of evals and allocations to reap in a
    16  	// single Raft transaction. This is to ensure that the Raft message does not
    17  	// become too large.
    18  	maxIdsPerReap = (1024 * 256) / 36 // 0.25 MB of ids.
    19  )
    20  
    21  // CoreScheduler is a special "scheduler" that is registered
    22  // as "_core". It is used to run various administrative work
    23  // across the cluster.
    24  type CoreScheduler struct {
    25  	srv  *Server
    26  	snap *state.StateSnapshot
    27  }
    28  
    29  // NewCoreScheduler is used to return a new system scheduler instance
    30  func NewCoreScheduler(srv *Server, snap *state.StateSnapshot) scheduler.Scheduler {
    31  	s := &CoreScheduler{
    32  		srv:  srv,
    33  		snap: snap,
    34  	}
    35  	return s
    36  }
    37  
    38  // Process is used to implement the scheduler.Scheduler interface
    39  func (c *CoreScheduler) Process(eval *structs.Evaluation) error {
    40  	switch eval.JobID {
    41  	case structs.CoreJobEvalGC:
    42  		return c.evalGC(eval)
    43  	case structs.CoreJobNodeGC:
    44  		return c.nodeGC(eval)
    45  	case structs.CoreJobJobGC:
    46  		return c.jobGC(eval)
    47  	case structs.CoreJobDeploymentGC:
    48  		return c.deploymentGC(eval)
    49  	case structs.CoreJobForceGC:
    50  		return c.forceGC(eval)
    51  	default:
    52  		return fmt.Errorf("core scheduler cannot handle job '%s'", eval.JobID)
    53  	}
    54  }
    55  
    56  // forceGC is used to garbage collect all eligible objects.
    57  func (c *CoreScheduler) forceGC(eval *structs.Evaluation) error {
    58  	if err := c.jobGC(eval); err != nil {
    59  		return err
    60  	}
    61  	if err := c.evalGC(eval); err != nil {
    62  		return err
    63  	}
    64  	if err := c.deploymentGC(eval); err != nil {
    65  		return err
    66  	}
    67  
    68  	// Node GC must occur after the others to ensure the allocations are
    69  	// cleared.
    70  	return c.nodeGC(eval)
    71  }
    72  
    73  // jobGC is used to garbage collect eligible jobs.
    74  func (c *CoreScheduler) jobGC(eval *structs.Evaluation) error {
    75  	// Get all the jobs eligible for garbage collection.
    76  	ws := memdb.NewWatchSet()
    77  	iter, err := c.snap.JobsByGC(ws, true)
    78  	if err != nil {
    79  		return err
    80  	}
    81  
    82  	var oldThreshold uint64
    83  	if eval.JobID == structs.CoreJobForceGC {
    84  		// The GC was forced, so set the threshold to its maximum so everything
    85  		// will GC.
    86  		oldThreshold = math.MaxUint64
    87  		c.srv.logger.Println("[DEBUG] sched.core: forced job GC")
    88  	} else {
    89  		// Get the time table to calculate GC cutoffs.
    90  		tt := c.srv.fsm.TimeTable()
    91  		cutoff := time.Now().UTC().Add(-1 * c.srv.config.JobGCThreshold)
    92  		oldThreshold = tt.NearestIndex(cutoff)
    93  		c.srv.logger.Printf("[DEBUG] sched.core: job GC: scanning before index %d (%v)",
    94  			oldThreshold, c.srv.config.JobGCThreshold)
    95  	}
    96  
    97  	// Collect the allocations, evaluations and jobs to GC
    98  	var gcAlloc, gcEval []string
    99  	var gcJob []*structs.Job
   100  
   101  OUTER:
   102  	for i := iter.Next(); i != nil; i = iter.Next() {
   103  		job := i.(*structs.Job)
   104  
   105  		// Ignore new jobs.
   106  		if job.CreateIndex > oldThreshold {
   107  			continue
   108  		}
   109  
   110  		ws := memdb.NewWatchSet()
   111  		evals, err := c.snap.EvalsByJob(ws, job.Namespace, job.ID)
   112  		if err != nil {
   113  			c.srv.logger.Printf("[ERR] sched.core: failed to get evals for job %s: %v", job.ID, err)
   114  			continue
   115  		}
   116  
   117  		allEvalsGC := true
   118  		var jobAlloc, jobEval []string
   119  		for _, eval := range evals {
   120  			gc, allocs, err := c.gcEval(eval, oldThreshold, true)
   121  			if err != nil {
   122  				continue OUTER
   123  			}
   124  
   125  			if gc {
   126  				jobEval = append(jobEval, eval.ID)
   127  				jobAlloc = append(jobAlloc, allocs...)
   128  			} else {
   129  				allEvalsGC = false
   130  				break
   131  			}
   132  		}
   133  
   134  		// Job is eligible for garbage collection
   135  		if allEvalsGC {
   136  			gcJob = append(gcJob, job)
   137  			gcAlloc = append(gcAlloc, jobAlloc...)
   138  			gcEval = append(gcEval, jobEval...)
   139  		}
   140  	}
   141  
   142  	// Fast-path the nothing case
   143  	if len(gcEval) == 0 && len(gcAlloc) == 0 && len(gcJob) == 0 {
   144  		return nil
   145  	}
   146  	c.srv.logger.Printf("[DEBUG] sched.core: job GC: %d jobs, %d evaluations, %d allocs eligible",
   147  		len(gcJob), len(gcEval), len(gcAlloc))
   148  
   149  	// Reap the evals and allocs
   150  	if err := c.evalReap(gcEval, gcAlloc); err != nil {
   151  		return err
   152  	}
   153  
   154  	// Reap the jobs
   155  	return c.jobReap(gcJob, eval.LeaderACL)
   156  }
   157  
   158  // jobReap contacts the leader and issues a reap on the passed jobs
   159  func (c *CoreScheduler) jobReap(jobs []*structs.Job, leaderACL string) error {
   160  	// Call to the leader to issue the reap
   161  	for _, req := range c.partitionJobReap(jobs, leaderACL) {
   162  		var resp structs.JobBatchDeregisterResponse
   163  		if err := c.srv.RPC("Job.BatchDeregister", req, &resp); err != nil {
   164  			c.srv.logger.Printf("[ERR] sched.core: batch job reap failed: %v", err)
   165  			return err
   166  		}
   167  	}
   168  
   169  	return nil
   170  }
   171  
   172  // partitionJobReap returns a list of JobBatchDeregisterRequests to make,
   173  // ensuring a single request does not contain too many jobs. This is necessary
   174  // to ensure that the Raft transaction does not become too large.
   175  func (c *CoreScheduler) partitionJobReap(jobs []*structs.Job, leaderACL string) []*structs.JobBatchDeregisterRequest {
   176  	option := &structs.JobDeregisterOptions{Purge: true}
   177  	var requests []*structs.JobBatchDeregisterRequest
   178  	submittedJobs := 0
   179  	for submittedJobs != len(jobs) {
   180  		req := &structs.JobBatchDeregisterRequest{
   181  			Jobs: make(map[structs.NamespacedID]*structs.JobDeregisterOptions),
   182  			WriteRequest: structs.WriteRequest{
   183  				Region:    c.srv.config.Region,
   184  				AuthToken: leaderACL,
   185  			},
   186  		}
   187  		requests = append(requests, req)
   188  		available := maxIdsPerReap
   189  
   190  		if remaining := len(jobs) - submittedJobs; remaining > 0 {
   191  			if remaining <= available {
   192  				for _, job := range jobs[submittedJobs:] {
   193  					jns := structs.NamespacedID{ID: job.ID, Namespace: job.Namespace}
   194  					req.Jobs[jns] = option
   195  				}
   196  				submittedJobs += remaining
   197  			} else {
   198  				for _, job := range jobs[submittedJobs : submittedJobs+available] {
   199  					jns := structs.NamespacedID{ID: job.ID, Namespace: job.Namespace}
   200  					req.Jobs[jns] = option
   201  				}
   202  				submittedJobs += available
   203  			}
   204  		}
   205  	}
   206  
   207  	return requests
   208  }
   209  
   210  // evalGC is used to garbage collect old evaluations
   211  func (c *CoreScheduler) evalGC(eval *structs.Evaluation) error {
   212  	// Iterate over the evaluations
   213  	ws := memdb.NewWatchSet()
   214  	iter, err := c.snap.Evals(ws)
   215  	if err != nil {
   216  		return err
   217  	}
   218  
   219  	var oldThreshold uint64
   220  	if eval.JobID == structs.CoreJobForceGC {
   221  		// The GC was forced, so set the threshold to its maximum so everything
   222  		// will GC.
   223  		oldThreshold = math.MaxUint64
   224  		c.srv.logger.Println("[DEBUG] sched.core: forced eval GC")
   225  	} else {
   226  		// Compute the old threshold limit for GC using the FSM
   227  		// time table.  This is a rough mapping of a time to the
   228  		// Raft index it belongs to.
   229  		tt := c.srv.fsm.TimeTable()
   230  		cutoff := time.Now().UTC().Add(-1 * c.srv.config.EvalGCThreshold)
   231  		oldThreshold = tt.NearestIndex(cutoff)
   232  		c.srv.logger.Printf("[DEBUG] sched.core: eval GC: scanning before index %d (%v)",
   233  			oldThreshold, c.srv.config.EvalGCThreshold)
   234  	}
   235  
   236  	// Collect the allocations and evaluations to GC
   237  	var gcAlloc, gcEval []string
   238  	for raw := iter.Next(); raw != nil; raw = iter.Next() {
   239  		eval := raw.(*structs.Evaluation)
   240  
   241  		// The Evaluation GC should not handle batch jobs since those need to be
   242  		// garbage collected in one shot
   243  		gc, allocs, err := c.gcEval(eval, oldThreshold, false)
   244  		if err != nil {
   245  			return err
   246  		}
   247  
   248  		if gc {
   249  			gcEval = append(gcEval, eval.ID)
   250  		}
   251  		gcAlloc = append(gcAlloc, allocs...)
   252  	}
   253  
   254  	// Fast-path the nothing case
   255  	if len(gcEval) == 0 && len(gcAlloc) == 0 {
   256  		return nil
   257  	}
   258  	c.srv.logger.Printf("[DEBUG] sched.core: eval GC: %d evaluations, %d allocs eligible",
   259  		len(gcEval), len(gcAlloc))
   260  
   261  	return c.evalReap(gcEval, gcAlloc)
   262  }
   263  
   264  // gcEval returns whether the eval should be garbage collected given a raft
   265  // threshold index. The eval disqualifies for garbage collection if it or its
   266  // allocs are not older than the threshold. If the eval should be garbage
   267  // collected, the associated alloc ids that should also be removed are also
   268  // returned
   269  func (c *CoreScheduler) gcEval(eval *structs.Evaluation, thresholdIndex uint64, allowBatch bool) (
   270  	bool, []string, error) {
   271  	// Ignore non-terminal and new evaluations
   272  	if !eval.TerminalStatus() || eval.ModifyIndex > thresholdIndex {
   273  		return false, nil, nil
   274  	}
   275  
   276  	// Create a watchset
   277  	ws := memdb.NewWatchSet()
   278  
   279  	// Look up the job
   280  	job, err := c.snap.JobByID(ws, eval.Namespace, eval.JobID)
   281  	if err != nil {
   282  		return false, nil, err
   283  	}
   284  
   285  	// If the eval is from a running "batch" job we don't want to garbage
   286  	// collect its allocations. If there is a long running batch job and its
   287  	// terminal allocations get GC'd the scheduler would re-run the
   288  	// allocations.
   289  	if eval.Type == structs.JobTypeBatch {
   290  		// Check if the job is running
   291  
   292  		// Can collect if:
   293  		// Job doesn't exist
   294  		// Job is Stopped and dead
   295  		// allowBatch and the job is dead
   296  		collect := false
   297  		if job == nil {
   298  			collect = true
   299  		} else if job.Status != structs.JobStatusDead {
   300  			collect = false
   301  		} else if job.Stop {
   302  			collect = true
   303  		} else if allowBatch {
   304  			collect = true
   305  		}
   306  
   307  		// We don't want to gc anything related to a job which is not dead
   308  		// If the batch job doesn't exist we can GC it regardless of allowBatch
   309  		if !collect {
   310  			return false, nil, nil
   311  		}
   312  	}
   313  
   314  	// Get the allocations by eval
   315  	allocs, err := c.snap.AllocsByEval(ws, eval.ID)
   316  	if err != nil {
   317  		c.srv.logger.Printf("[ERR] sched.core: failed to get allocs for eval %s: %v",
   318  			eval.ID, err)
   319  		return false, nil, err
   320  	}
   321  
   322  	// Scan the allocations to ensure they are terminal and old
   323  	gcEval := true
   324  	var gcAllocIDs []string
   325  	for _, alloc := range allocs {
   326  		if !allocGCEligible(alloc, job, time.Now(), thresholdIndex) {
   327  			// Can't GC the evaluation since not all of the allocations are
   328  			// terminal
   329  			gcEval = false
   330  		} else {
   331  			// The allocation is eligible to be GC'd
   332  			gcAllocIDs = append(gcAllocIDs, alloc.ID)
   333  		}
   334  	}
   335  
   336  	return gcEval, gcAllocIDs, nil
   337  }
   338  
   339  // evalReap contacts the leader and issues a reap on the passed evals and
   340  // allocs.
   341  func (c *CoreScheduler) evalReap(evals, allocs []string) error {
   342  	// Call to the leader to issue the reap
   343  	for _, req := range c.partitionEvalReap(evals, allocs) {
   344  		var resp structs.GenericResponse
   345  		if err := c.srv.RPC("Eval.Reap", req, &resp); err != nil {
   346  			c.srv.logger.Printf("[ERR] sched.core: eval reap failed: %v", err)
   347  			return err
   348  		}
   349  	}
   350  
   351  	return nil
   352  }
   353  
   354  // partitionEvalReap returns a list of EvalDeleteRequest to make, ensuring a single
   355  // request does not contain too many allocations and evaluations. This is
   356  // necessary to ensure that the Raft transaction does not become too large.
   357  func (c *CoreScheduler) partitionEvalReap(evals, allocs []string) []*structs.EvalDeleteRequest {
   358  	var requests []*structs.EvalDeleteRequest
   359  	submittedEvals, submittedAllocs := 0, 0
   360  	for submittedEvals != len(evals) || submittedAllocs != len(allocs) {
   361  		req := &structs.EvalDeleteRequest{
   362  			WriteRequest: structs.WriteRequest{
   363  				Region: c.srv.config.Region,
   364  			},
   365  		}
   366  		requests = append(requests, req)
   367  		available := maxIdsPerReap
   368  
   369  		// Add the allocs first
   370  		if remaining := len(allocs) - submittedAllocs; remaining > 0 {
   371  			if remaining <= available {
   372  				req.Allocs = allocs[submittedAllocs:]
   373  				available -= remaining
   374  				submittedAllocs += remaining
   375  			} else {
   376  				req.Allocs = allocs[submittedAllocs : submittedAllocs+available]
   377  				submittedAllocs += available
   378  
   379  				// Exhausted space so skip adding evals
   380  				continue
   381  			}
   382  		}
   383  
   384  		// Add the evals
   385  		if remaining := len(evals) - submittedEvals; remaining > 0 {
   386  			if remaining <= available {
   387  				req.Evals = evals[submittedEvals:]
   388  				submittedEvals += remaining
   389  			} else {
   390  				req.Evals = evals[submittedEvals : submittedEvals+available]
   391  				submittedEvals += available
   392  			}
   393  		}
   394  	}
   395  
   396  	return requests
   397  }
   398  
   399  // nodeGC is used to garbage collect old nodes
   400  func (c *CoreScheduler) nodeGC(eval *structs.Evaluation) error {
   401  	// Iterate over the evaluations
   402  	ws := memdb.NewWatchSet()
   403  	iter, err := c.snap.Nodes(ws)
   404  	if err != nil {
   405  		return err
   406  	}
   407  
   408  	var oldThreshold uint64
   409  	if eval.JobID == structs.CoreJobForceGC {
   410  		// The GC was forced, so set the threshold to its maximum so everything
   411  		// will GC.
   412  		oldThreshold = math.MaxUint64
   413  		c.srv.logger.Println("[DEBUG] sched.core: forced node GC")
   414  	} else {
   415  		// Compute the old threshold limit for GC using the FSM
   416  		// time table.  This is a rough mapping of a time to the
   417  		// Raft index it belongs to.
   418  		tt := c.srv.fsm.TimeTable()
   419  		cutoff := time.Now().UTC().Add(-1 * c.srv.config.NodeGCThreshold)
   420  		oldThreshold = tt.NearestIndex(cutoff)
   421  		c.srv.logger.Printf("[DEBUG] sched.core: node GC: scanning before index %d (%v)",
   422  			oldThreshold, c.srv.config.NodeGCThreshold)
   423  	}
   424  
   425  	// Collect the nodes to GC
   426  	var gcNode []string
   427  OUTER:
   428  	for {
   429  		raw := iter.Next()
   430  		if raw == nil {
   431  			break
   432  		}
   433  		node := raw.(*structs.Node)
   434  
   435  		// Ignore non-terminal and new nodes
   436  		if !node.TerminalStatus() || node.ModifyIndex > oldThreshold {
   437  			continue
   438  		}
   439  
   440  		// Get the allocations by node
   441  		ws := memdb.NewWatchSet()
   442  		allocs, err := c.snap.AllocsByNode(ws, node.ID)
   443  		if err != nil {
   444  			c.srv.logger.Printf("[ERR] sched.core: failed to get allocs for node %s: %v",
   445  				eval.ID, err)
   446  			continue
   447  		}
   448  
   449  		// If there are any non-terminal allocations, skip the node. If the node
   450  		// is terminal and the allocations are not, the scheduler may not have
   451  		// run yet to transition the allocs on the node to terminal. We delay
   452  		// GC'ing until this happens.
   453  		for _, alloc := range allocs {
   454  			if !alloc.TerminalStatus() {
   455  				continue OUTER
   456  			}
   457  		}
   458  
   459  		// Node is eligible for garbage collection
   460  		gcNode = append(gcNode, node.ID)
   461  	}
   462  
   463  	// Fast-path the nothing case
   464  	if len(gcNode) == 0 {
   465  		return nil
   466  	}
   467  	c.srv.logger.Printf("[DEBUG] sched.core: node GC: %d nodes eligible", len(gcNode))
   468  
   469  	// Call to the leader to issue the reap
   470  	for _, nodeID := range gcNode {
   471  		req := structs.NodeDeregisterRequest{
   472  			NodeID: nodeID,
   473  			WriteRequest: structs.WriteRequest{
   474  				Region:    c.srv.config.Region,
   475  				AuthToken: eval.LeaderACL,
   476  			},
   477  		}
   478  		var resp structs.NodeUpdateResponse
   479  		if err := c.srv.RPC("Node.Deregister", &req, &resp); err != nil {
   480  			c.srv.logger.Printf("[ERR] sched.core: node '%s' reap failed: %v", nodeID, err)
   481  			return err
   482  		}
   483  	}
   484  	return nil
   485  }
   486  
   487  // deploymentGC is used to garbage collect old deployments
   488  func (c *CoreScheduler) deploymentGC(eval *structs.Evaluation) error {
   489  	// Iterate over the deployments
   490  	ws := memdb.NewWatchSet()
   491  	iter, err := c.snap.Deployments(ws)
   492  	if err != nil {
   493  		return err
   494  	}
   495  
   496  	var oldThreshold uint64
   497  	if eval.JobID == structs.CoreJobForceGC {
   498  		// The GC was forced, so set the threshold to its maximum so everything
   499  		// will GC.
   500  		oldThreshold = math.MaxUint64
   501  		c.srv.logger.Println("[DEBUG] sched.core: forced deployment GC")
   502  	} else {
   503  		// Compute the old threshold limit for GC using the FSM
   504  		// time table.  This is a rough mapping of a time to the
   505  		// Raft index it belongs to.
   506  		tt := c.srv.fsm.TimeTable()
   507  		cutoff := time.Now().UTC().Add(-1 * c.srv.config.DeploymentGCThreshold)
   508  		oldThreshold = tt.NearestIndex(cutoff)
   509  		c.srv.logger.Printf("[DEBUG] sched.core: deployment GC: scanning before index %d (%v)",
   510  			oldThreshold, c.srv.config.DeploymentGCThreshold)
   511  	}
   512  
   513  	// Collect the deployments to GC
   514  	var gcDeployment []string
   515  
   516  OUTER:
   517  	for {
   518  		raw := iter.Next()
   519  		if raw == nil {
   520  			break
   521  		}
   522  		deploy := raw.(*structs.Deployment)
   523  
   524  		// Ignore non-terminal and new deployments
   525  		if deploy.Active() || deploy.ModifyIndex > oldThreshold {
   526  			continue
   527  		}
   528  
   529  		// Ensure there are no allocs referencing this deployment.
   530  		allocs, err := c.snap.AllocsByDeployment(ws, deploy.ID)
   531  		if err != nil {
   532  			c.srv.logger.Printf("[ERR] sched.core: failed to get allocs for deployment %s: %v",
   533  				deploy.ID, err)
   534  			continue
   535  		}
   536  
   537  		// Ensure there is no allocation referencing the deployment.
   538  		for _, alloc := range allocs {
   539  			if !alloc.TerminalStatus() {
   540  				continue OUTER
   541  			}
   542  		}
   543  
   544  		// Deployment is eligible for garbage collection
   545  		gcDeployment = append(gcDeployment, deploy.ID)
   546  	}
   547  
   548  	// Fast-path the nothing case
   549  	if len(gcDeployment) == 0 {
   550  		return nil
   551  	}
   552  	c.srv.logger.Printf("[DEBUG] sched.core: deployment GC: %d deployments eligible", len(gcDeployment))
   553  	return c.deploymentReap(gcDeployment)
   554  }
   555  
   556  // deploymentReap contacts the leader and issues a reap on the passed
   557  // deployments.
   558  func (c *CoreScheduler) deploymentReap(deployments []string) error {
   559  	// Call to the leader to issue the reap
   560  	for _, req := range c.partitionDeploymentReap(deployments) {
   561  		var resp structs.GenericResponse
   562  		if err := c.srv.RPC("Deployment.Reap", req, &resp); err != nil {
   563  			c.srv.logger.Printf("[ERR] sched.core: deployment reap failed: %v", err)
   564  			return err
   565  		}
   566  	}
   567  
   568  	return nil
   569  }
   570  
   571  // partitionDeploymentReap returns a list of DeploymentDeleteRequest to make,
   572  // ensuring a single request does not contain too many deployments. This is
   573  // necessary to ensure that the Raft transaction does not become too large.
   574  func (c *CoreScheduler) partitionDeploymentReap(deployments []string) []*structs.DeploymentDeleteRequest {
   575  	var requests []*structs.DeploymentDeleteRequest
   576  	submittedDeployments := 0
   577  	for submittedDeployments != len(deployments) {
   578  		req := &structs.DeploymentDeleteRequest{
   579  			WriteRequest: structs.WriteRequest{
   580  				Region: c.srv.config.Region,
   581  			},
   582  		}
   583  		requests = append(requests, req)
   584  		available := maxIdsPerReap
   585  
   586  		if remaining := len(deployments) - submittedDeployments; remaining > 0 {
   587  			if remaining <= available {
   588  				req.Deployments = deployments[submittedDeployments:]
   589  				submittedDeployments += remaining
   590  			} else {
   591  				req.Deployments = deployments[submittedDeployments : submittedDeployments+available]
   592  				submittedDeployments += available
   593  			}
   594  		}
   595  	}
   596  
   597  	return requests
   598  }
   599  
   600  // allocGCEligible returns if the allocation is eligible to be garbage collected
   601  // according to its terminal status and its reschedule trackers
   602  func allocGCEligible(a *structs.Allocation, job *structs.Job, gcTime time.Time, thresholdIndex uint64) bool {
   603  	// Not in a terminal status and old enough
   604  	if !a.TerminalStatus() || a.ModifyIndex > thresholdIndex {
   605  		return false
   606  	}
   607  
   608  	// If the job is deleted, stopped or dead all allocs can be removed
   609  	if job == nil || job.Stop || job.Status == structs.JobStatusDead {
   610  		return true
   611  	}
   612  
   613  	// If the alloc hasn't failed then we don't need to consider it for rescheduling
   614  	// Rescheduling needs to copy over information from the previous alloc so that it
   615  	// can enforce the reschedule policy
   616  	if a.ClientStatus != structs.AllocClientStatusFailed {
   617  		return true
   618  	}
   619  
   620  	var reschedulePolicy *structs.ReschedulePolicy
   621  	tg := job.LookupTaskGroup(a.TaskGroup)
   622  
   623  	if tg != nil {
   624  		reschedulePolicy = tg.ReschedulePolicy
   625  	}
   626  	// No reschedule policy or rescheduling is disabled
   627  	if reschedulePolicy == nil || (!reschedulePolicy.Unlimited && reschedulePolicy.Attempts == 0) {
   628  		return true
   629  	}
   630  	// Restart tracking information has been carried forward
   631  	if a.NextAllocation != "" {
   632  		return true
   633  	}
   634  
   635  	// This task has unlimited rescheduling and the alloc has not been replaced, so we can't GC it yet
   636  	if reschedulePolicy.Unlimited {
   637  		return false
   638  	}
   639  
   640  	// No restarts have been attempted yet
   641  	if a.RescheduleTracker == nil || len(a.RescheduleTracker.Events) == 0 {
   642  		return false
   643  	}
   644  
   645  	// Don't GC if most recent reschedule attempt is within time interval
   646  	interval := reschedulePolicy.Interval
   647  	lastIndex := len(a.RescheduleTracker.Events)
   648  	lastRescheduleEvent := a.RescheduleTracker.Events[lastIndex-1]
   649  	timeDiff := gcTime.UTC().UnixNano() - lastRescheduleEvent.RescheduleTime
   650  
   651  	return timeDiff > interval.Nanoseconds()
   652  }