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 }