Skip to content

Commit d761f07

Browse files
committed
perf(operator): parallelize secret reads, remove monitorPod goroutines, use informer cache
- Replace sequential secret/config reads with errgroup-based parallel fetches, reducing API round-trips when creating sessions under load - Remove monitorPod goroutine-per-session pattern; controller-runtime reconciler handles pod status via its watch/requeue loop - Eliminate redundant CR re-GET in ReconcilePendingSession; use the object already provided by the controller-runtime informer cache - Switch gauge callbacks in otel_metrics.go to use the informer cache instead of a cluster-wide LIST against the API server every 30s - Replace clearAnnotation GET+UPDATE (2 API calls) with a single JSON merge patch Validated with load-test-sessions.sh: 30 sessions reach Running in ~21s with 20 concurrent reconcilers, zero errors.
1 parent 7d442a7 commit d761f07

5 files changed

Lines changed: 285 additions & 470 deletions

File tree

components/operator/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
go.opentelemetry.io/otel/metric v1.33.0
1111
go.opentelemetry.io/otel/sdk v1.33.0
1212
go.opentelemetry.io/otel/sdk/metric v1.33.0
13+
golang.org/x/sync v0.12.0
1314
k8s.io/api v0.34.0
1415
k8s.io/apimachinery v0.34.0
1516
k8s.io/client-go v0.34.0
@@ -60,7 +61,6 @@ require (
6061
go.yaml.in/yaml/v3 v3.0.4 // indirect
6162
golang.org/x/net v0.38.0 // indirect
6263
golang.org/x/oauth2 v0.27.0 // indirect
63-
golang.org/x/sync v0.12.0 // indirect
6464
golang.org/x/sys v0.31.0 // indirect
6565
golang.org/x/term v0.30.0 // indirect
6666
golang.org/x/text v0.23.0 // indirect

components/operator/internal/controller/otel_metrics.go

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,18 @@ import (
1717
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1818
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
1919
"k8s.io/apimachinery/pkg/runtime/schema"
20+
"sigs.k8s.io/controller-runtime/pkg/client"
2021

2122
"ambient-code-operator/internal/config"
2223
)
2324

2425
var (
2526
meter metric.Meter
2627

28+
// cachedClient uses the controller-runtime informer cache for reads,
29+
// avoiding direct API server hits every 30s for gauge callbacks.
30+
cachedClient client.Client
31+
2732
// Session lifecycle metrics (histograms)
2833
sessionStartupDuration metric.Float64Histogram
2934
sessionTotalDuration metric.Float64Histogram
@@ -48,7 +53,12 @@ var (
4853
// InitMetrics initializes OpenTelemetry metrics.
4954
// Set OTEL_EXPORTER_OTLP_ENDPOINT to configure the collector address.
5055
// Leave unset or empty to disable metrics export (no-op).
51-
func InitMetrics(ctx context.Context) (func(), error) {
56+
// If c is non-nil, gauge callbacks will use the informer cache instead of
57+
// hitting the API server directly (saves a cluster-wide LIST every 30s).
58+
func InitMetrics(ctx context.Context, c ...client.Client) (func(), error) {
59+
if len(c) > 0 && c[0] != nil {
60+
cachedClient = c[0]
61+
}
5262
// Get OTLP endpoint from environment; skip if not configured
5363
endpoint := os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
5464
if endpoint == "" {
@@ -316,33 +326,49 @@ func initGauges() error {
316326
return nil
317327
}
318328

319-
// countSessionsByPhase counts sessions in the given phases, grouped by namespace
329+
// countSessionsByPhase counts sessions in the given phases, grouped by namespace.
330+
// When cachedClient is set (controller-runtime), reads from the informer cache
331+
// instead of making a cluster-wide LIST against the API server every 30s.
320332
func countSessionsByPhase(ctx context.Context, phases ...string) map[string]int64 {
321333
counts := make(map[string]int64)
322334

323-
// Use config.DynamicClient to list sessions
324-
if config.DynamicClient == nil {
325-
return counts
326-
}
327-
328-
gvr := schema.GroupVersionResource{
329-
Group: "vteam.ambient-code",
330-
Version: "v1alpha1",
331-
Resource: "agenticsessions",
332-
}
333-
334-
list, err := config.DynamicClient.Resource(gvr).List(ctx, v1.ListOptions{})
335-
if err != nil {
336-
log.Printf("Failed to list sessions for metrics: %v", err)
337-
return counts
338-
}
339-
340335
phaseSet := make(map[string]bool)
341336
for _, p := range phases {
342337
phaseSet[p] = true
343338
}
344339

345-
for _, item := range list.Items {
340+
sessionList := &unstructured.UnstructuredList{}
341+
sessionList.SetGroupVersionKind(schema.GroupVersionKind{
342+
Group: "vteam.ambient-code",
343+
Version: "v1alpha1",
344+
Kind: "AgenticSessionList",
345+
})
346+
347+
if cachedClient != nil {
348+
// Use informer cache - no API server round-trip
349+
if err := cachedClient.List(ctx, sessionList); err != nil {
350+
log.Printf("Failed to list sessions for metrics (cached): %v", err)
351+
return counts
352+
}
353+
} else {
354+
// Fallback to direct API call if cached client not available
355+
if config.DynamicClient == nil {
356+
return counts
357+
}
358+
gvr := schema.GroupVersionResource{
359+
Group: "vteam.ambient-code",
360+
Version: "v1alpha1",
361+
Resource: "agenticsessions",
362+
}
363+
list, err := config.DynamicClient.Resource(gvr).List(ctx, v1.ListOptions{})
364+
if err != nil {
365+
log.Printf("Failed to list sessions for metrics: %v", err)
366+
return counts
367+
}
368+
sessionList.Items = list.Items
369+
}
370+
371+
for _, item := range sessionList.Items {
346372
ns := item.GetNamespace()
347373
status, found, _ := unstructured.NestedMap(item.Object, "status")
348374
if !found {

components/operator/internal/handlers/helpers.go

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"k8s.io/apimachinery/pkg/api/errors"
1515
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1616
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
17+
k8stypes "k8s.io/apimachinery/pkg/types"
1718
)
1819

1920
const (
@@ -221,31 +222,17 @@ func updateAnnotations(sessionNamespace, name string, annotations map[string]str
221222
return nil
222223
}
223224

224-
// clearAnnotation removes a specific annotation from the AgenticSession CR.
225+
// clearAnnotation removes a specific annotation from the AgenticSession CR
226+
// using a single JSON merge patch (1 API call) instead of GET + full UPDATE (2 API calls).
225227
func clearAnnotation(sessionNamespace, name, annotationKey string) error {
226228
gvr := types.GetAgenticSessionResource()
227229

228-
obj, err := config.DynamicClient.Resource(gvr).Namespace(sessionNamespace).Get(context.TODO(), name, v1.GetOptions{})
229-
if err != nil {
230-
if errors.IsNotFound(err) {
231-
return nil
232-
}
233-
return fmt.Errorf("failed to get AgenticSession %s: %w", name, err)
234-
}
235-
236-
annotations := obj.GetAnnotations()
237-
if annotations == nil {
238-
return nil
239-
}
240-
241-
if _, exists := annotations[annotationKey]; !exists {
242-
return nil
243-
}
244-
245-
delete(annotations, annotationKey)
246-
obj.SetAnnotations(annotations)
230+
// JSON merge patch: setting a key to null removes it
231+
patch := []byte(fmt.Sprintf(`{"metadata":{"annotations":{%q:null}}}`, annotationKey))
247232

248-
_, err = config.DynamicClient.Resource(gvr).Namespace(sessionNamespace).Update(context.TODO(), obj, v1.UpdateOptions{})
233+
_, err := config.DynamicClient.Resource(gvr).Namespace(sessionNamespace).Patch(
234+
context.TODO(), name, k8stypes.MergePatchType, patch, v1.PatchOptions{},
235+
)
249236
if err != nil && !errors.IsNotFound(err) {
250237
return fmt.Errorf("failed to clear annotation %s for %s: %w", annotationKey, name, err)
251238
}

0 commit comments

Comments
 (0)