2018-10-18 06:00:18 +02:00
|
|
|
// Copyright Project Harbor Authors
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
2018-03-20 11:03:04 +01:00
|
|
|
|
|
|
|
package pool
|
|
|
|
|
|
|
|
import (
|
2018-10-18 06:00:18 +02:00
|
|
|
"errors"
|
2018-03-29 13:50:44 +02:00
|
|
|
"fmt"
|
2018-10-18 06:00:18 +02:00
|
|
|
"runtime"
|
2018-03-20 11:03:04 +01:00
|
|
|
"time"
|
|
|
|
|
2018-10-18 06:00:18 +02:00
|
|
|
"github.com/goharbor/harbor/src/jobservice/job/impl"
|
|
|
|
|
2018-03-20 11:03:04 +01:00
|
|
|
"github.com/gocraft/work"
|
2018-08-23 09:02:20 +02:00
|
|
|
"github.com/goharbor/harbor/src/jobservice/env"
|
|
|
|
"github.com/goharbor/harbor/src/jobservice/errs"
|
|
|
|
"github.com/goharbor/harbor/src/jobservice/job"
|
|
|
|
"github.com/goharbor/harbor/src/jobservice/logger"
|
2018-10-18 06:00:18 +02:00
|
|
|
"github.com/goharbor/harbor/src/jobservice/models"
|
2018-08-23 09:02:20 +02:00
|
|
|
"github.com/goharbor/harbor/src/jobservice/opm"
|
2018-10-18 06:00:18 +02:00
|
|
|
"github.com/goharbor/harbor/src/jobservice/utils"
|
2018-03-20 11:03:04 +01:00
|
|
|
)
|
|
|
|
|
2018-09-05 10:16:31 +02:00
|
|
|
// RedisJob is a job wrapper to wrap the job.Interface to the style which can be recognized by the redis pool.
|
2018-03-20 11:03:04 +01:00
|
|
|
type RedisJob struct {
|
2018-09-05 10:16:31 +02:00
|
|
|
job interface{} // the real job implementation
|
|
|
|
context *env.Context // context
|
|
|
|
statsManager opm.JobStatsManager // job stats manager
|
2018-03-20 11:03:04 +01:00
|
|
|
}
|
|
|
|
|
2018-09-05 10:16:31 +02:00
|
|
|
// NewRedisJob is constructor of RedisJob
|
2018-03-20 11:03:04 +01:00
|
|
|
func NewRedisJob(j interface{}, ctx *env.Context, statsManager opm.JobStatsManager) *RedisJob {
|
|
|
|
return &RedisJob{
|
|
|
|
job: j,
|
|
|
|
context: ctx,
|
|
|
|
statsManager: statsManager,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-05 10:16:31 +02:00
|
|
|
// Run the job
|
2018-03-20 11:03:04 +01:00
|
|
|
func (rj *RedisJob) Run(j *work.Job) error {
|
2018-03-22 11:29:39 +01:00
|
|
|
var (
|
|
|
|
cancelled = false
|
|
|
|
buildContextFailed = false
|
|
|
|
runningJob job.Interface
|
|
|
|
err error
|
|
|
|
execContext env.JobContext
|
|
|
|
)
|
|
|
|
|
2018-03-20 11:03:04 +01:00
|
|
|
defer func() {
|
2018-03-22 11:29:39 +01:00
|
|
|
if err == nil {
|
2018-04-08 04:38:47 +02:00
|
|
|
logger.Infof("Job '%s:%s' exit with success", j.Name, j.ID)
|
2018-09-05 10:16:31 +02:00
|
|
|
return // nothing need to do
|
2018-03-22 11:29:39 +01:00
|
|
|
}
|
|
|
|
|
2018-09-05 10:16:31 +02:00
|
|
|
// log error
|
2018-04-08 04:38:47 +02:00
|
|
|
logger.Errorf("Job '%s:%s' exit with error: %s\n", j.Name, j.ID, err)
|
|
|
|
|
2018-03-22 11:29:39 +01:00
|
|
|
if buildContextFailed || rj.shouldDisableRetry(runningJob, j, cancelled) {
|
2018-09-05 10:16:31 +02:00
|
|
|
j.Fails = 10000000000 // Make it big enough to avoid retrying
|
2018-03-20 11:03:04 +01:00
|
|
|
now := time.Now().Unix()
|
|
|
|
go func() {
|
2018-09-05 10:16:31 +02:00
|
|
|
timer := time.NewTimer(2 * time.Second) // make sure the failed job is already put into the dead queue
|
2018-03-20 11:03:04 +01:00
|
|
|
defer timer.Stop()
|
|
|
|
|
|
|
|
<-timer.C
|
|
|
|
|
|
|
|
rj.statsManager.DieAt(j.ID, now)
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2018-03-29 13:50:44 +02:00
|
|
|
defer func() {
|
|
|
|
if r := recover(); r != nil {
|
|
|
|
err = fmt.Errorf("Runtime error: %s", r)
|
2018-10-18 06:00:18 +02:00
|
|
|
|
|
|
|
// Log the stack
|
|
|
|
buf := make([]byte, 1<<16)
|
|
|
|
size := runtime.Stack(buf, false)
|
|
|
|
logger.Errorf("Runtime error happened when executing job %s:%s: %s", j.Name, j.ID, buf[0:size])
|
|
|
|
|
2018-09-05 10:16:31 +02:00
|
|
|
// record runtime error status
|
2018-04-08 04:38:47 +02:00
|
|
|
rj.jobFailed(j.ID)
|
2018-03-29 13:50:44 +02:00
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2018-09-05 10:16:31 +02:00
|
|
|
// Wrap job
|
2018-04-08 05:29:43 +02:00
|
|
|
runningJob = Wrap(rj.job)
|
|
|
|
|
|
|
|
execContext, err = rj.buildContext(j)
|
|
|
|
if err != nil {
|
|
|
|
buildContextFailed = true
|
2018-09-05 10:16:31 +02:00
|
|
|
goto FAILED // no need to retry
|
2018-04-08 05:29:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
defer func() {
|
2018-09-05 10:16:31 +02:00
|
|
|
// Close open io stream first
|
2018-04-08 05:29:43 +02:00
|
|
|
if closer, ok := execContext.GetLogger().(logger.Closer); ok {
|
|
|
|
closer.Close()
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2018-09-05 10:16:31 +02:00
|
|
|
// Start to run
|
2018-03-20 11:03:04 +01:00
|
|
|
rj.jobRunning(j.ID)
|
2018-10-18 06:00:18 +02:00
|
|
|
|
2018-09-05 10:16:31 +02:00
|
|
|
// Inject data
|
2018-03-20 11:03:04 +01:00
|
|
|
err = runningJob.Run(execContext, j.Args)
|
|
|
|
|
2018-09-05 10:16:31 +02:00
|
|
|
// update the proper status
|
2018-03-20 11:03:04 +01:00
|
|
|
if err == nil {
|
|
|
|
rj.jobSucceed(j.ID)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if errs.IsJobStoppedError(err) {
|
|
|
|
rj.jobStopped(j.ID)
|
|
|
|
return nil // no need to put it into the dead queue for resume
|
|
|
|
}
|
|
|
|
|
|
|
|
if errs.IsJobCancelledError(err) {
|
|
|
|
rj.jobCancelled(j.ID)
|
|
|
|
cancelled = true
|
2018-09-05 10:16:31 +02:00
|
|
|
return err // need to resume
|
2018-03-20 11:03:04 +01:00
|
|
|
}
|
|
|
|
|
2018-03-22 11:29:39 +01:00
|
|
|
FAILED:
|
2018-03-20 11:03:04 +01:00
|
|
|
rj.jobFailed(j.ID)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (rj *RedisJob) jobRunning(jobID string) {
|
|
|
|
rj.statsManager.SetJobStatus(jobID, job.JobStatusRunning)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (rj *RedisJob) jobFailed(jobID string) {
|
|
|
|
rj.statsManager.SetJobStatus(jobID, job.JobStatusError)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (rj *RedisJob) jobStopped(jobID string) {
|
|
|
|
rj.statsManager.SetJobStatus(jobID, job.JobStatusStopped)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (rj *RedisJob) jobCancelled(jobID string) {
|
|
|
|
rj.statsManager.SetJobStatus(jobID, job.JobStatusCancelled)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (rj *RedisJob) jobSucceed(jobID string) {
|
|
|
|
rj.statsManager.SetJobStatus(jobID, job.JobStatusSuccess)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (rj *RedisJob) buildContext(j *work.Job) (env.JobContext, error) {
|
2018-09-05 10:16:31 +02:00
|
|
|
// Build job execution context
|
2018-03-20 11:03:04 +01:00
|
|
|
jData := env.JobData{
|
|
|
|
ID: j.ID,
|
|
|
|
Name: j.Name,
|
|
|
|
Args: j.Args,
|
|
|
|
ExtraData: make(map[string]interface{}),
|
|
|
|
}
|
|
|
|
|
|
|
|
checkOPCmdFuncFactory := func(jobID string) job.CheckOPCmdFunc {
|
|
|
|
return func() (string, bool) {
|
|
|
|
cmd, err := rj.statsManager.CtlCommand(jobID)
|
|
|
|
if err != nil {
|
|
|
|
return "", false
|
|
|
|
}
|
|
|
|
return cmd, true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
jData.ExtraData["opCommandFunc"] = checkOPCmdFuncFactory(j.ID)
|
|
|
|
|
|
|
|
checkInFuncFactory := func(jobID string) job.CheckInFunc {
|
|
|
|
return func(message string) {
|
|
|
|
rj.statsManager.CheckIn(jobID, message)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
jData.ExtraData["checkInFunc"] = checkInFuncFactory(j.ID)
|
|
|
|
|
2018-10-18 06:00:18 +02:00
|
|
|
launchJobFuncFactory := func(jobID string) job.LaunchJobFunc {
|
|
|
|
funcIntf := rj.context.SystemContext.Value(utils.CtlKeyOfLaunchJobFunc)
|
|
|
|
return func(jobReq models.JobRequest) (models.JobStats, error) {
|
|
|
|
launchJobFunc, ok := funcIntf.(job.LaunchJobFunc)
|
|
|
|
if !ok {
|
|
|
|
return models.JobStats{}, errors.New("no launch job func provided")
|
|
|
|
}
|
|
|
|
|
|
|
|
jobName := ""
|
|
|
|
if jobReq.Job != nil {
|
|
|
|
jobName = jobReq.Job.Name
|
|
|
|
}
|
|
|
|
if j.Name == jobName {
|
|
|
|
return models.JobStats{}, errors.New("infinite job creating loop may exist")
|
|
|
|
}
|
|
|
|
|
|
|
|
res, err := launchJobFunc(jobReq)
|
|
|
|
if err != nil {
|
|
|
|
return models.JobStats{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := rj.statsManager.Update(jobID, "multiple_executions", true); err != nil {
|
|
|
|
logger.Error(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := rj.statsManager.Update(res.Stats.JobID, "upstream_job_id", jobID); err != nil {
|
|
|
|
logger.Error(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
rj.statsManager.AttachExecution(jobID, res.Stats.JobID)
|
|
|
|
|
|
|
|
logger.Infof("Launch sub job %s:%s for upstream job %s", res.Stats.JobName, res.Stats.JobID, jobID)
|
|
|
|
return res, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
jData.ExtraData["launchJobFunc"] = launchJobFuncFactory(j.ID)
|
|
|
|
|
|
|
|
// Use default context
|
|
|
|
if rj.context.JobContext == nil {
|
|
|
|
rj.context.JobContext = impl.NewDefaultContext(rj.context.SystemContext)
|
|
|
|
}
|
|
|
|
|
2018-03-20 11:03:04 +01:00
|
|
|
return rj.context.JobContext.Build(jData)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (rj *RedisJob) shouldDisableRetry(j job.Interface, wj *work.Job, cancelled bool) bool {
|
|
|
|
maxFails := j.MaxFails()
|
|
|
|
if maxFails == 0 {
|
2018-09-05 10:16:31 +02:00
|
|
|
maxFails = 4 // Consistent with backend worker pool
|
2018-03-20 11:03:04 +01:00
|
|
|
}
|
|
|
|
fails := wj.Fails
|
2018-09-05 10:16:31 +02:00
|
|
|
fails++ // as the fail is not returned to backend pool yet
|
2018-03-20 11:03:04 +01:00
|
|
|
|
|
|
|
if cancelled && fails < int64(maxFails) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
if !cancelled && fails < int64(maxFails) && !j.ShouldRetry() {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|