Skip to content

Commit c04087e

Browse files
authored
fix: properly clean up finished jobs and return failure when max-age exceeded (#1128)
* Skip jobs from hibernating due to culling config * Update timeouts on job reconcile * Extract failure message from job, skip reconciling finished jobs * Not setting ttlSecondsAfterFinished for jobs the job is deleted with the amalthea session; it is part of the `needsDeletion` function determining when to remove a session. The job sets its idleSince time when completed.
1 parent 1d239a2 commit c04087e

5 files changed

Lines changed: 50 additions & 14 deletions

File tree

api/v1alpha1/amaltheasession_children.go

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -180,13 +180,6 @@ func (cr *AmaltheaSession) Job(cfg config.AmaltheaSessionConfiguration) (batchv1
180180
activeTTL = ptr.To(int64(cr.Spec.Culling.MaxAge.Seconds()))
181181
}
182182

183-
// use MaxHibernatedDuration (amount of time until a hibernated session is deleted) to set the time
184-
// after which the job is removed after it has completed
185-
var ttlFinish *int32
186-
if cr.Spec.Culling.MaxHibernatedDuration.Seconds() > 0 {
187-
ttlFinish = ptr.To(int32(cr.Spec.Culling.MaxHibernatedDuration.Seconds()))
188-
}
189-
190183
job := batchv1.Job{
191184
ObjectMeta: metav1.ObjectMeta{
192185
Name: cr.JobName(),
@@ -195,12 +188,11 @@ func (cr *AmaltheaSession) Job(cfg config.AmaltheaSessionConfiguration) (batchv1
195188
Annotations: cr.Spec.Template.Metadata.Annotations,
196189
},
197190
Spec: batchv1.JobSpec{
198-
Parallelism: ptr.To(int32(1)),
199-
Completions: ptr.To(int32(1)),
200-
BackoffLimit: ptr.To(int32(0)),
201-
ActiveDeadlineSeconds: activeTTL,
202-
TTLSecondsAfterFinished: ttlFinish,
203-
Suspend: &cr.Spec.Hibernated,
191+
Parallelism: ptr.To(int32(1)),
192+
Completions: ptr.To(int32(1)),
193+
BackoffLimit: ptr.To(int32(0)),
194+
ActiveDeadlineSeconds: activeTTL,
195+
Suspend: &cr.Spec.Hibernated,
204196
Template: v1.PodTemplateSpec{
205197
ObjectMeta: metav1.ObjectMeta{
206198
Labels: cr.childLabels(),

internal/controller/children.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,11 @@ func (c ChildResource[T]) Reconcile(ctx context.Context, clnt client.Client, cr
301301
if !ok {
302302
return fmt.Errorf("could not cast when reconciling")
303303
}
304+
// finished jobs are not updated
305+
if jobIsFinished(current.Status) {
306+
log.Info("Job is terminated, not reconciling", "job", current.Name)
307+
return nil
308+
}
304309
if current.CreationTimestamp.IsZero() {
305310
log.Info("Creating a Job", "job", desired.Spec)
306311
current.Spec = desired.Spec
@@ -334,6 +339,8 @@ func (c ChildResource[T]) Reconcile(ctx context.Context, clnt client.Client, cr
334339
current.Spec.Template.Spec.NodeSelector = desired.Spec.Template.Spec.NodeSelector
335340
current.Spec.Template.Spec.PriorityClassName = desired.Spec.Template.Spec.PriorityClassName
336341
current.Spec.Suspend = desired.Spec.Suspend
342+
current.Spec.ActiveDeadlineSeconds = desired.Spec.ActiveDeadlineSeconds
343+
current.Spec.TTLSecondsAfterFinished = desired.Spec.TTLSecondsAfterFinished
337344
switch strategy := cr.Spec.ReconcileStrategy; strategy {
338345
case amaltheadevv1alpha1.Never:
339346
return nil
@@ -538,6 +545,10 @@ func (c ChildResourceUpdates) failureMessage(pod *v1.Pod) string {
538545
if msg != "" {
539546
return msg
540547
}
548+
msg = jobFailureReason(c.Job.Manifest)
549+
if msg != "" {
550+
return msg
551+
}
541552

542553
return ""
543554
}

internal/controller/children_conditions.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"slices"
66

7+
batchv1 "k8s.io/api/batch/v1"
78
v1 "k8s.io/api/core/v1"
89
networkingv1 "k8s.io/api/networking/v1"
910
)
@@ -26,6 +27,22 @@ func handleBadWaitingState(status v1.ContainerStatus) string {
2627
return fmt.Sprintf("the container %s is failing because %s", status.Name, waitingState.Reason)
2728
}
2829

30+
func jobFailureReason(job *batchv1.Job) string {
31+
if job == nil {
32+
return ""
33+
}
34+
35+
if job.Status.Failed > 0 {
36+
for _, cond := range job.Status.Conditions {
37+
if (cond.Type == batchv1.JobFailed || cond.Type == batchv1.JobFailureTarget) && cond.Status == v1.ConditionTrue {
38+
return cond.Reason + ": " + cond.Message
39+
}
40+
}
41+
return "The job failed."
42+
}
43+
return ""
44+
}
45+
2946
func podFailureReason(pod *v1.Pod) string {
3047
if pod == nil {
3148
return ""

internal/controller/cull.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ import (
3030
)
3131

3232
func updateHibernationState(ctx context.Context, r *AmaltheaSessionReconciler, amaltheasession *amaltheadevv1alpha1.AmaltheaSession) error {
33+
if amaltheasession.Spec.SessionType == amaltheadevv1alpha1.SessionTypeNonInteractive {
34+
// non-interactive sessions (jobs) never get auto-scaled down by amalthea;
35+
// this is managed by k8s via the activeDeadlineSeconds setting for jobs
36+
return nil
37+
}
38+
3339
status := amaltheasession.Status
3440
log := log.FromContext(ctx)
3541
if !amaltheasession.Spec.Hibernated {
@@ -40,7 +46,7 @@ func updateHibernationState(ctx context.Context, r *AmaltheaSessionReconciler, a
4046
if err != nil {
4147
return err
4248
}
43-
log.Info("statefulSet scaled down or job suspended")
49+
log.Info("statefulSet scaled down")
4450
}
4551
}
4652
return nil

internal/controller/status.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"time"
77

88
amaltheadevv1alpha1 "github.com/SwissDataScienceCenter/amalthea/api/v1alpha1"
9+
batchv1 "k8s.io/api/batch/v1"
910
v1 "k8s.io/api/core/v1"
1011
resource "k8s.io/apimachinery/pkg/api/resource"
1112
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -99,6 +100,15 @@ func podIsCompleted(pod *v1.Pod) bool {
99100
return phaseCompleted && totalCnt == completedCnt
100101
}
101102

103+
func jobIsFinished(job batchv1.JobStatus) bool {
104+
for _, c := range job.Conditions {
105+
if (c.Type == batchv1.JobComplete || c.Type == batchv1.JobFailed) && c.Status == v1.ConditionTrue {
106+
return true
107+
}
108+
}
109+
return false
110+
}
111+
102112
func metrics(ctx context.Context, clnt metricsv1beta1.PodMetricsesGetter, cr *amaltheadevv1alpha1.AmaltheaSession) (v1.ResourceList, error) {
103113
podName := fmt.Sprintf("%s-0", cr.Name)
104114
podMetricses, err := clnt.PodMetricses(cr.Namespace).List(

0 commit comments

Comments
 (0)