-
Notifications
You must be signed in to change notification settings - Fork 47
Fix: wrap status update with RetryOnConflict to prevent linkedSkills loss #423
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ba9cc60
fdd22e1
1432569
85616c9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -209,6 +209,47 @@ var _ = Describe("AgentRuntime Controller", func() { | |
| Expect(k8sClient.Get(ctx, nn, updatedRT)).To(Succeed()) | ||
| Expect(updatedRT.Status.LinkedSkills).To(ConsistOf("summarizer", "translator")) | ||
| }) | ||
|
|
||
| It("should persist linkedSkills even after a concurrent status modification", func() { | ||
| dep := newDeployment("skills-conflict-deploy", namespace) | ||
| dep.Annotations = map[string]string{ | ||
| AnnotationSkills: `["skill-a","skill-b"]`, | ||
| } | ||
| Expect(k8sClient.Create(ctx, dep)).To(Succeed()) | ||
| defer func() { _ = k8sClient.Delete(ctx, dep) }() | ||
|
|
||
| rt := newAgentRuntime("skills-conflict-rt", namespace, "skills-conflict-deploy", agentv1alpha1.RuntimeTypeAgent) | ||
| Expect(k8sClient.Create(ctx, rt)).To(Succeed()) | ||
| defer func() { _ = k8sClient.Delete(ctx, rt) }() | ||
|
|
||
| r := newReconciler() | ||
| r.GetFeatureGates = func() *webhookconfig.FeatureGates { | ||
| return &webhookconfig.FeatureGates{SkillDiscovery: true} | ||
| } | ||
| nn := types.NamespacedName{Name: "skills-conflict-rt", Namespace: namespace} | ||
|
|
||
| // First reconcile to set up finalizer | ||
| _, _ = r.Reconcile(ctx, reconcile.Request{NamespacedName: nn}) | ||
|
|
||
| // Simulate a concurrent modification by updating the object's status | ||
| // from another controller (this bumps resourceVersion) | ||
| current := &agentv1alpha1.AgentRuntime{} | ||
| Expect(k8sClient.Get(ctx, nn, current)).To(Succeed()) | ||
| current.Status.ConfiguredPods = 99 | ||
| Expect(k8sClient.Status().Update(ctx, current)).To(Succeed()) | ||
|
|
||
| // Now reconcile — the status update inside Reconcile will initially | ||
| // hit a conflict (stale resourceVersion), but retry should succeed | ||
| _, err := r.Reconcile(ctx, reconcile.Request{NamespacedName: nn}) | ||
| Expect(err).NotTo(HaveOccurred()) | ||
|
|
||
| updatedRT := &agentv1alpha1.AgentRuntime{} | ||
| Expect(k8sClient.Get(ctx, nn, updatedRT)).To(Succeed()) | ||
| Expect(updatedRT.Status.LinkedSkills).To(ConsistOf("skill-a", "skill-b")) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion — This new test only asserts |
||
|
|
||
| configCond := meta.FindStatusCondition(updatedRT.Status.Conditions, ConditionTypeConfigResolved) | ||
| Expect(configCond).NotTo(BeNil(), "ConfigResolved condition must survive the retry re-fetch") | ||
| }) | ||
| }) | ||
|
|
||
| Context("When setting status", func() { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
must-fix — This
r.Get(ctx, req.NamespacedName, rt)overwrites the entire in-memoryrt(includingrt.Status.Conditions) with server state. ButConfigResolved(set at step 5) andIstioMeshEnrolled(step 4.6) are set in-memory and not yet persisted when we reach step 8 — the re-fetch discards them, and the closure only re-appliesReady,SkillsDiscovered,ConfiguredPods,LinkedSkills. So those conditions are silently dropped on every reconcile.This is exactly what breaks the two
agentruntime_config_test.gospecs (lines 348 & 386 —ConfigResolvedcomes backnil).Suggest snapshotting the desired status before the loop and restoring it after the fresh
Get: