github.com/govau/cf-common@v0.0.7/jobs/singleton.go (about)

     1  package jobs
     2  
     3  import (
     4  	"errors"
     5  	"log"
     6  	"time"
     7  
     8  	que "github.com/bgentry/que-go"
     9  	"github.com/jackc/pgx"
    10  )
    11  
    12  var (
    13  	ErrImmediateReschedule = errors.New("commit tx, and reschedule ASAP")
    14  	ErrDoNotReschedule     = errors.New("no need to reschedule, we are done")
    15  )
    16  
    17  // JobFunc should do a thing. Return either:
    18  // nil => wrapper will schedule the next cron (if a cron), then commit the tx.
    19  // ErrImmediateReschedule => wrapper will commit the tx, then try it again immediately.
    20  // ErrDidNotReschedule => wrapper will rollback the tx, and if a cron, will not reschedule or retry.
    21  // any other error => wrapper rollback the tx, and allow que to reschedule
    22  type JobFunc func(qc *que.Client, logger *log.Logger, job *que.Job, tx *pgx.Tx) error
    23  
    24  type JobConfig struct {
    25  	// Set by this library on a clone
    26  	qc     *que.Client
    27  	logger *log.Logger
    28  
    29  	F JobFunc
    30  
    31  	// Debug logging
    32  	VerboseLogging bool
    33  
    34  	// One will only be called at once
    35  	Singleton bool
    36  
    37  	// Will be rescheduled upon success
    38  	Duration time.Duration
    39  }
    40  
    41  func (scw *JobConfig) CloneWith(qc *que.Client, logger *log.Logger) *JobConfig {
    42  	return &JobConfig{
    43  		qc:             qc,
    44  		logger:         logger,
    45  		F:              scw.F,
    46  		Singleton:      scw.Singleton,
    47  		VerboseLogging: scw.VerboseLogging,
    48  		Duration:       scw.Duration,
    49  	}
    50  }
    51  
    52  // Return should continue, error. Never returns True if an error returns
    53  func (scw *JobConfig) ensureNooneElseRunning(job *que.Job, tx *pgx.Tx, key string) (bool, error) {
    54  	var lastCompleted time.Time
    55  	var nextScheduled time.Time
    56  	err := tx.QueryRow("SELECT last_completed, next_scheduled FROM cron_metadata WHERE id = $1 FOR UPDATE", key).Scan(&lastCompleted, &nextScheduled)
    57  	if err != nil {
    58  		if err == pgx.ErrNoRows {
    59  			_, err = tx.Exec("INSERT INTO cron_metadata (id) VALUES ($1)", key)
    60  			if err != nil {
    61  				return false, err
    62  			}
    63  			return false, ErrImmediateReschedule
    64  		}
    65  		return false, err
    66  	}
    67  
    68  	if time.Now().Before(nextScheduled) {
    69  		var futureJobs int
    70  		// make sure we don't regard ourself as a future job. Sometimes clock skew makes us think we can't run yet.
    71  		err = tx.QueryRow("SELECT count(*) FROM que_jobs WHERE job_class = $1 AND args::jsonb = $2::jsonb AND run_at >= $3 AND job_id != $4", job.Type, job.Args, nextScheduled, job.ID).Scan(&futureJobs)
    72  		if err != nil {
    73  			return false, err
    74  		}
    75  
    76  		if futureJobs > 0 {
    77  			return false, nil
    78  		}
    79  
    80  		return false, scw.qc.EnqueueInTx(&que.Job{
    81  			Type:  job.Type,
    82  			Args:  job.Args,
    83  			RunAt: nextScheduled,
    84  		}, tx)
    85  	}
    86  
    87  	// Continue
    88  	return true, nil
    89  }
    90  
    91  func (scw *JobConfig) scheduleJobLater(job *que.Job, tx *pgx.Tx, key string) error {
    92  	n := time.Now()
    93  	next := n.Add(scw.Duration)
    94  
    95  	_, err := tx.Exec("UPDATE cron_metadata SET last_completed = $1, next_scheduled = $2 WHERE id = $3", n, next, key)
    96  	if err != nil {
    97  		tx.Rollback()
    98  		return err
    99  	}
   100  
   101  	err = scw.qc.EnqueueInTx(&que.Job{
   102  		Type:  job.Type,
   103  		Args:  job.Args,
   104  		RunAt: next,
   105  	}, tx)
   106  	if err != nil {
   107  		return err
   108  	}
   109  
   110  	return nil
   111  }
   112  
   113  func (scw *JobConfig) Run(job *que.Job) error {
   114  	for {
   115  		err := scw.tryRun(job)
   116  		switch err {
   117  		case nil:
   118  			return nil
   119  		case ErrImmediateReschedule:
   120  			scw.logger.Printf("RESCHEDULE REQUESTED, RESTARTING... %s%s (%d)\n", job.Type, job.Args, job.ID)
   121  			continue
   122  		case ErrDoNotReschedule:
   123  			scw.logger.Printf("CRON JOB FINISHED AND HAS REQUESTED NOT TO BE RESCHEDULED %s%s (%d)\n", job.Type, job.Args, job.ID)
   124  			return nil
   125  		default:
   126  			scw.logger.Printf("FAILED WITH ERROR, RELY ON QUE TO RESCHEDULE %s%s (%d): %s\n", job.Type, job.Args, job.ID, err)
   127  			return err
   128  		}
   129  	}
   130  }
   131  
   132  // This job manages the tx, no one else should commit or rollback
   133  func (scw *JobConfig) tryRun(job *que.Job) error {
   134  	if scw.VerboseLogging {
   135  		scw.logger.Printf("START %s%s (%d)\n", job.Type, job.Args, job.ID)
   136  		defer scw.logger.Printf("STOP %s%s (%d)\n", job.Type, job.Args, job.ID)
   137  	}
   138  
   139  	tx, err := job.Conn().Begin()
   140  	if err != nil {
   141  		return err
   142  	}
   143  	defer tx.Rollback()
   144  
   145  	key := job.Type + string(job.Args)
   146  	if scw.Singleton {
   147  		carryOn, err := scw.ensureNooneElseRunning(job, tx, key)
   148  		if !carryOn {
   149  			// We are not carrying on, check the error codes.
   150  			switch err {
   151  			case nil:
   152  				return tx.Commit()
   153  			case ErrImmediateReschedule:
   154  				// We commit, but propagate the error code
   155  				err = tx.Commit()
   156  				if err != nil {
   157  					return err
   158  				}
   159  				return ErrImmediateReschedule
   160  			default:
   161  				return err
   162  			}
   163  		}
   164  	}
   165  
   166  	err = scw.F(scw.qc, scw.logger, job, tx)
   167  	switch err {
   168  	case nil:
   169  		// continue, we commit later
   170  	case ErrImmediateReschedule:
   171  		err = tx.Commit()
   172  		if err != nil {
   173  			return err
   174  		}
   175  		return ErrImmediateReschedule
   176  	default:
   177  		return err
   178  	}
   179  
   180  	if scw.Singleton && scw.Duration != 0 {
   181  		err = scw.scheduleJobLater(job, tx, key)
   182  		if err != nil {
   183  			return err
   184  		}
   185  	}
   186  
   187  	return tx.Commit()
   188  }