2016-05-20 03:53:39 -04:00
/ *
2016-07-18 16:24:57 -04:00
Copyright 2016 The Kubernetes Authors .
2016-05-20 03:53:39 -04:00
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 .
* /
2016-11-01 18:46:23 -04:00
package cronjob
2016-05-20 03:53:39 -04:00
import (
"fmt"
"time"
2021-06-09 10:06:12 -04:00
"github.com/robfig/cron/v3"
2016-05-20 03:53:39 -04:00
2017-06-22 14:24:23 -04:00
batchv1 "k8s.io/api/batch/v1"
2020-10-11 02:49:11 -04:00
corev1 "k8s.io/api/core/v1"
2017-01-11 15:28:46 -05:00
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2021-11-03 12:04:46 -04:00
"k8s.io/apimachinery/pkg/labels"
2017-01-11 09:09:48 -05:00
"k8s.io/apimachinery/pkg/types"
2020-10-11 02:49:11 -04:00
"k8s.io/client-go/tools/record"
2021-11-03 12:04:46 -04:00
"k8s.io/klog/v2"
2016-05-20 03:53:39 -04:00
)
2016-11-01 18:46:23 -04:00
// Utilities for dealing with Jobs and CronJobs and time.
2016-05-20 03:53:39 -04:00
2021-02-24 16:12:26 -05:00
func inActiveList ( cj batchv1 . CronJob , uid types . UID ) bool {
2020-05-09 12:45:21 -04:00
for _ , j := range cj . Status . Active {
2016-05-20 03:53:39 -04:00
if j . UID == uid {
return true
}
}
return false
}
2021-02-24 16:12:26 -05:00
func deleteFromActiveList ( cj * batchv1 . CronJob , uid types . UID ) {
2020-05-09 12:45:21 -04:00
if cj == nil {
2016-05-20 03:53:39 -04:00
return
}
2020-10-11 02:49:11 -04:00
// TODO: @alpatel the memory footprint can may be reduced here by
// cj.Status.Active = append(cj.Status.Active[:indexToRemove], cj.Status.Active[indexToRemove:]...)
newActive := [ ] corev1 . ObjectReference { }
2020-05-09 12:45:21 -04:00
for _ , j := range cj . Status . Active {
2016-05-20 03:53:39 -04:00
if j . UID != uid {
newActive = append ( newActive , j )
}
}
2020-05-09 12:45:21 -04:00
cj . Status . Active = newActive
2016-05-20 03:53:39 -04:00
}
2020-12-06 23:06:47 -05:00
// getNextScheduleTime gets the time of next schedule after last scheduled and before now
// it returns nil if no unmet schedule times.
2020-10-11 02:49:11 -04:00
// If there are too many (>100) unstarted times, it will raise a warning and but still return
// the list of missed times.
2021-02-24 16:12:26 -05:00
func getNextScheduleTime ( cj batchv1 . CronJob , now time . Time , schedule cron . Schedule , recorder record . EventRecorder ) ( * time . Time , error ) {
2021-04-13 01:59:27 -04:00
var (
earliestTime time . Time
)
2020-10-11 02:49:11 -04:00
if cj . Status . LastScheduleTime != nil {
earliestTime = cj . Status . LastScheduleTime . Time
} else {
// If none found, then this is either a recently created cronJob,
// or the active/completed info was somehow lost (contract for status
// in kubernetes says it may need to be recreated), or that we have
// started a job, but have not noticed it yet (distributed systems can
// have arbitrary delays). In any case, use the creation time of the
// CronJob as last known start time.
earliestTime = cj . ObjectMeta . CreationTimestamp . Time
}
if cj . Spec . StartingDeadlineSeconds != nil {
// Controller is not going to schedule anything below this point
schedulingDeadline := now . Add ( - time . Second * time . Duration ( * cj . Spec . StartingDeadlineSeconds ) )
if schedulingDeadline . After ( earliestTime ) {
earliestTime = schedulingDeadline
}
}
if earliestTime . After ( now ) {
2021-02-22 21:40:05 -05:00
return nil , nil
2020-10-11 02:49:11 -04:00
}
2021-02-22 21:40:05 -05:00
t , numberOfMissedSchedules , err := getMostRecentScheduleTime ( earliestTime , now , schedule )
2020-12-06 23:06:47 -05:00
if numberOfMissedSchedules > 100 {
2020-10-11 02:49:11 -04:00
// An object might miss several starts. For example, if
// controller gets wedged on friday at 5:01pm when everyone has
// gone home, and someone comes in on tuesday AM and discovers
// the problem and restarts the controller, then all the hourly
// jobs, more than 80 of them for one hourly cronJob, should
// all start running with no further intervention (if the cronJob
// allows concurrency and late starts).
//
// However, if there is a bug somewhere, or incorrect clock
// on controller's server or apiservers (for setting creationTimestamp)
// then there could be so many missed start times (it could be off
// by decades or more), that it would eat up all the CPU and memory
// of this controller. In that case, we want to not try to list
// all the missed start times.
//
// I've somewhat arbitrarily picked 100, as more than 80,
// but less than "lots".
2021-04-13 01:59:27 -04:00
recorder . Eventf ( & cj , corev1 . EventTypeWarning , "TooManyMissedTimes" , "too many missed start times: %d. Set or decrease .spec.startingDeadlineSeconds or check clock skew" , numberOfMissedSchedules )
klog . InfoS ( "too many missed times" , "cronjob" , klog . KRef ( cj . GetNamespace ( ) , cj . GetName ( ) ) , "missed times" , numberOfMissedSchedules )
2020-10-11 02:49:11 -04:00
}
2021-02-22 21:40:05 -05:00
return t , err
2020-12-06 23:06:47 -05:00
}
// getMostRecentScheduleTime returns the latest schedule time between earliestTime and the count of number of
// schedules in between them
2021-02-22 21:40:05 -05:00
func getMostRecentScheduleTime ( earliestTime time . Time , now time . Time , schedule cron . Schedule ) ( * time . Time , int64 , error ) {
2020-12-06 23:06:47 -05:00
t1 := schedule . Next ( earliestTime )
t2 := schedule . Next ( t1 )
if now . Before ( t1 ) {
2021-02-22 21:40:05 -05:00
return nil , 0 , nil
2020-12-06 23:06:47 -05:00
}
if now . Before ( t2 ) {
2021-02-22 21:40:05 -05:00
return & t1 , 1 , nil
2020-12-06 23:06:47 -05:00
}
2021-02-22 21:40:05 -05:00
// It is possible for cron.ParseStandard("59 23 31 2 *") to return an invalid schedule
// seconds - 59, minute - 23, hour - 31 (?!) dom - 2, and dow is optional, clearly 31 is invalid
// In this case the timeBetweenTwoSchedules will be 0, and we error out the invalid schedule
timeBetweenTwoSchedules := int64 ( t2 . Sub ( t1 ) . Round ( time . Second ) . Seconds ( ) )
if timeBetweenTwoSchedules < 1 {
return nil , 0 , fmt . Errorf ( "time difference between two schedules less than 1 second" )
}
2020-12-06 23:06:47 -05:00
timeElapsed := int64 ( now . Sub ( t1 ) . Seconds ( ) )
numberOfMissedSchedules := ( timeElapsed / timeBetweenTwoSchedules ) + 1
t := time . Unix ( t1 . Unix ( ) + ( ( numberOfMissedSchedules - 1 ) * timeBetweenTwoSchedules ) , 0 ) . UTC ( )
2021-02-22 21:40:05 -05:00
return & t , numberOfMissedSchedules , nil
2020-10-11 02:49:11 -04:00
}
2021-11-03 12:04:46 -04:00
func copyLabels ( template * batchv1 . JobTemplateSpec ) labels . Set {
l := make ( labels . Set )
for k , v := range template . Labels {
l [ k ] = v
2016-05-20 03:53:39 -04:00
}
2021-11-03 12:04:46 -04:00
return l
2016-05-20 03:53:39 -04:00
}
2021-11-03 12:04:46 -04:00
func copyAnnotations ( template * batchv1 . JobTemplateSpec ) labels . Set {
a := make ( labels . Set )
for k , v := range template . Annotations {
a [ k ] = v
}
return a
2016-08-10 21:16:09 -04:00
}
2020-10-11 02:49:11 -04:00
// getJobFromTemplate2 makes a Job from a CronJob. It converts the unix time into minutes from
// epoch time and concatenates that to the job name, because the cronjob_controller v2 has the lowest
// granularity of 1 minute for scheduling job.
2021-02-24 16:12:26 -05:00
func getJobFromTemplate2 ( cj * batchv1 . CronJob , scheduledTime time . Time ) ( * batchv1 . Job , error ) {
2020-10-11 02:49:11 -04:00
labels := copyLabels ( & cj . Spec . JobTemplate )
annotations := copyAnnotations ( & cj . Spec . JobTemplate )
// We want job names for a given nominal start time to have a deterministic name to avoid the same job being created twice
name := getJobName ( cj , scheduledTime )
job := & batchv1 . Job {
ObjectMeta : metav1 . ObjectMeta {
2021-06-06 12:17:34 -04:00
Labels : labels ,
Annotations : annotations ,
Name : name ,
CreationTimestamp : metav1 . Time { Time : scheduledTime } ,
OwnerReferences : [ ] metav1 . OwnerReference { * metav1 . NewControllerRef ( cj , controllerKind ) } ,
2020-10-11 02:49:11 -04:00
} ,
}
cj . Spec . JobTemplate . Spec . DeepCopyInto ( & job . Spec )
return job , nil
}
// getTimeHash returns Unix Epoch Time in minutes
func getTimeHashInMinutes ( scheduledTime time . Time ) int64 {
return scheduledTime . Unix ( ) / 60
}
2017-02-22 06:55:58 -05:00
func getFinishedStatus ( j * batchv1 . Job ) ( bool , batchv1 . JobConditionType ) {
2016-11-18 15:50:17 -05:00
for _ , c := range j . Status . Conditions {
2020-10-11 02:49:11 -04:00
if ( c . Type == batchv1 . JobComplete || c . Type == batchv1 . JobFailed ) && c . Status == corev1 . ConditionTrue {
2017-02-25 06:51:54 -05:00
return true , c . Type
2016-11-18 15:50:17 -05:00
}
}
2017-02-25 06:51:54 -05:00
return false , ""
}
2019-02-26 15:05:45 -05:00
// IsJobFinished returns whether or not a job has completed successfully or failed.
2017-02-22 06:55:58 -05:00
func IsJobFinished ( j * batchv1 . Job ) bool {
2017-02-25 06:51:54 -05:00
isFinished , _ := getFinishedStatus ( j )
return isFinished
}
// byJobStartTime sorts a list of jobs by start timestamp, using their names as a tie breaker.
2017-02-22 06:55:58 -05:00
type byJobStartTime [ ] batchv1 . Job
2017-02-25 06:51:54 -05:00
func ( o byJobStartTime ) Len ( ) int { return len ( o ) }
func ( o byJobStartTime ) Swap ( i , j int ) { o [ i ] , o [ j ] = o [ j ] , o [ i ] }
func ( o byJobStartTime ) Less ( i , j int ) bool {
2019-03-27 09:25:12 -04:00
if o [ i ] . Status . StartTime == nil && o [ j ] . Status . StartTime != nil {
return false
}
if o [ i ] . Status . StartTime != nil && o [ j ] . Status . StartTime == nil {
return true
2017-02-25 06:51:54 -05:00
}
2017-08-04 11:04:14 -04:00
if o [ i ] . Status . StartTime . Equal ( o [ j ] . Status . StartTime ) {
2017-02-25 06:51:54 -05:00
return o [ i ] . Name < o [ j ] . Name
}
2017-08-04 11:04:14 -04:00
return o [ i ] . Status . StartTime . Before ( o [ j ] . Status . StartTime )
2016-11-18 15:50:17 -05:00
}
2020-11-10 22:34:08 -05:00
// byJobStartTimeStar sorts a list of jobs by start timestamp, using their names as a tie breaker.
type byJobStartTimeStar [ ] * batchv1 . Job
func ( o byJobStartTimeStar ) Len ( ) int { return len ( o ) }
func ( o byJobStartTimeStar ) Swap ( i , j int ) { o [ i ] , o [ j ] = o [ j ] , o [ i ] }
func ( o byJobStartTimeStar ) Less ( i , j int ) bool {
if o [ i ] . Status . StartTime == nil && o [ j ] . Status . StartTime != nil {
return false
}
if o [ i ] . Status . StartTime != nil && o [ j ] . Status . StartTime == nil {
return true
}
if o [ i ] . Status . StartTime . Equal ( o [ j ] . Status . StartTime ) {
return o [ i ] . Name < o [ j ] . Name
}
return o [ i ] . Status . StartTime . Before ( o [ j ] . Status . StartTime )
}