Skip to content

Commit 5521e1e

Browse files
committed
Add StatusTracker for intermediate status updates
Provides immediate status visibility during long-running reconciliation operations by flushing updates at key checkpoints. Updates only when status changes and retries on conflict with exponential backoff.
1 parent 1188a7b commit 5521e1e

2 files changed

Lines changed: 125 additions & 7 deletions

File tree

internal/controller/function_controller.go

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,15 +91,17 @@ func (r *FunctionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
9191
}
9292

9393
function := original.DeepCopy()
94+
95+
// Create tracker and add to context
96+
statusTracker := NewStatusTracker(r.Client, function)
97+
ctx = WithStatusTracker(ctx, statusTracker)
98+
9499
reconcileErr := r.reconcile(ctx, function)
95-
function.CalculateReadyCondition()
96100

97-
// update status if required
98-
if !equality.Semantic.DeepEqual(original.Status, function.Status) {
99-
if err := r.Status().Update(ctx, function); err != nil {
100-
logger.Error(err, "Unable to update Function status")
101-
return ctrl.Result{}, err
102-
}
101+
// Final flush at the end (handles ready condition calculation)
102+
if err := statusTracker.Flush(ctx, function); err != nil {
103+
logger.Error(err, "Unable to update Function status")
104+
return ctrl.Result{}, err
103105
}
104106

105107
if reconcileErr != nil {
@@ -122,12 +124,18 @@ func (r *FunctionReconciler) reconcile(ctx context.Context, function *v1alpha1.F
122124
defer repo.Cleanup()
123125

124126
r.updateFunctionStatusGit(function, repo)
127+
if err := FlushStatus(ctx, function); err != nil {
128+
return fmt.Errorf("failed to update status: %w", err)
129+
}
125130

126131
if err := r.ensureDeployment(ctx, function, repo, metadata); err != nil {
127132
return fmt.Errorf("deploying function failed: %w", err)
128133
}
129134

130135
r.updateFunctionStatus(function, metadata)
136+
if err := FlushStatus(ctx, function); err != nil {
137+
return fmt.Errorf("failed to update status: %w", err)
138+
}
131139

132140
return nil
133141
}
@@ -199,6 +207,12 @@ func (r *FunctionReconciler) handleMiddlewareUpdate(ctx context.Context, functio
199207
if !isOnLatestMiddleware {
200208
logger.Info("Function is not on latest middleware. Will redeploy")
201209
function.MarkMiddlewareNotUpToDate("MiddlewareOutdated", "Middleware is outdated, redeploying")
210+
211+
// Checkpoint 2: Flush status before long deploy operation
212+
if err := FlushStatus(ctx, function); err != nil {
213+
logger.Error(err, "Failed to update status before redeployment")
214+
}
215+
202216
if err := r.deploy(ctx, function, repo); err != nil {
203217
function.MarkDeployNotReady("DeployFailed", "Redeployment failed: %s", err.Error())
204218
return fmt.Errorf("failed to redeploy function: %w", err)
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
Copyright 2025.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controller
18+
19+
import (
20+
"context"
21+
"fmt"
22+
23+
"github.com/functions-dev/func-operator/api/v1alpha1"
24+
"k8s.io/apimachinery/pkg/api/equality"
25+
"k8s.io/apimachinery/pkg/types"
26+
"k8s.io/client-go/util/retry"
27+
"sigs.k8s.io/controller-runtime/pkg/client"
28+
)
29+
30+
// StatusTracker manages incremental status updates during reconciliation
31+
type StatusTracker struct {
32+
client client.Client
33+
original *v1alpha1.Function
34+
}
35+
36+
// NewStatusTracker creates a new status tracker with a snapshot of the current function state
37+
func NewStatusTracker(client client.Client, function *v1alpha1.Function) *StatusTracker {
38+
return &StatusTracker{
39+
client: client,
40+
original: function.DeepCopy(),
41+
}
42+
}
43+
44+
// Flush updates the function status if it has changed since the last flush
45+
func (t *StatusTracker) Flush(ctx context.Context, current *v1alpha1.Function) error {
46+
// Always calculate ready condition before comparing
47+
current.CalculateReadyCondition()
48+
49+
// Compare and update if changed
50+
if !equality.Semantic.DeepEqual(t.original.Status, current.Status) {
51+
// Retry on conflict with exponential backoff
52+
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
53+
// Get the latest version to ensure we have the most recent resourceVersion
54+
latest := &v1alpha1.Function{}
55+
if err := t.client.Get(ctx, types.NamespacedName{
56+
Name: current.Name,
57+
Namespace: current.Namespace,
58+
}, latest); err != nil {
59+
return err
60+
}
61+
62+
// Apply our status changes to the latest version
63+
latest.Status = current.Status
64+
65+
// Attempt the update
66+
return t.client.Status().Update(ctx, latest)
67+
})
68+
69+
if err != nil {
70+
return fmt.Errorf("failed to update status: %w", err)
71+
}
72+
73+
// Update our snapshot to the new state
74+
t.original = current.DeepCopy()
75+
}
76+
77+
return nil
78+
}
79+
80+
// statusTrackerKey is the context key for the status tracker
81+
type statusTrackerKey struct{}
82+
83+
// WithStatusTracker adds a status tracker to the context
84+
func WithStatusTracker(ctx context.Context, tracker *StatusTracker) context.Context {
85+
return context.WithValue(ctx, statusTrackerKey{}, tracker)
86+
}
87+
88+
// GetStatusTracker retrieves the tracker from context
89+
func GetStatusTracker(ctx context.Context) *StatusTracker {
90+
tracker, ok := ctx.Value(statusTrackerKey{}).(*StatusTracker)
91+
if !ok {
92+
return nil
93+
}
94+
return tracker
95+
}
96+
97+
// FlushStatus is a convenience helper that gets tracker from context and flushes
98+
func FlushStatus(ctx context.Context, function *v1alpha1.Function) error {
99+
tracker := GetStatusTracker(ctx)
100+
if tracker == nil {
101+
return nil // gracefully handle missing tracker
102+
}
103+
return tracker.Flush(ctx, function)
104+
}

0 commit comments

Comments
 (0)