如何在controller-runtime的envTest中等待工作队列排空以验证控制器映射函数逻辑
如何在controller-runtime的envTest中等待工作队列排空以验证控制器映射函数逻辑
兄弟,我太懂你这种靠sleep混日子的测试有多闹心了——要么跑快了测挂,要么跑慢了拖时间,完全不稳定。你要的是确保容器的初始reconcile完全处理完,再创建Item来验证你的ItemToContainer映射函数对吧?给你几个靠谱的办法,再也不用依赖sleep了:
方法一:直接监控控制器的工作队列(推荐)
controller-runtime的Controller实例其实可以暴露它的工作队列,我们可以直接检查队列长度,直到队列排空。唯一要做的是在测试中能拿到你的Controller实例:
步骤1:保存Controller实例
在你设置控制器的地方,把创建好的Controller保存到一个测试能访问的变量里(比如全局变量,或者通过测试Setup函数返回):
// 定义一个测试包内可见的变量 var containerCtrl controller.Controller func SetupContainerController(mgr ctrl.Manager, reconciler *ContainerReconciler) error { var err error // 创建控制器时把实例保存下来 containerCtrl, err = ctrl.NewControllerManagedBy(mgr). For(&foov1.Container{}). Watches( &clusterv1.Item{}, handler.EnqueueRequestsFromMapFunc(reconciler.ItemToContainer), ). Complete(reconciler) return err }
步骤2:写一个等待队列排空的工具函数
这个函数会循环检查队列长度,直到队列为空或者超时:
func waitForControllerQueueEmpty(ctx context.Context, ctrl controller.Controller, timeout time.Duration) error { deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { // 尝试将Controller转为暴露Queue的类型(controller-runtime内部实现支持这个) if queueHolder, ok := ctrl.(interface{ Queue() workqueue.RateLimitingInterface }); ok { queue := queueHolder.Queue() if queue.Len() == 0 { // 队列空了之后再等100ms,确保正在处理的最后一个reconcile也完成 time.Sleep(100 * time.Millisecond) // 再检查一次队列,防止新任务被加入 if queue.Len() == 0 { return nil } } } else { return fmt.Errorf("controller does not expose its work queue") } // 每次检查间隔50ms,避免占用太多CPU time.Sleep(50 * time.Millisecond) } return fmt.Errorf("controller queue did not empty within %v timeout", timeout) }
步骤3:在测试中使用
现在你可以替换掉sleep,用这个函数等待队列排空:
// 创建容器 Expect(testEnv.Create(ctx, container)).To(Succeed()) // 等待容器的初始reconcile完全处理完,队列排空 Expect(waitForControllerQueueEmpty(ctx, containerCtrl, 5*time.Second)).To(Succeed()) // 现在再创建Item,确保触发的是ItemToContainer映射的reconcile Expect(testEnv.Create(ctx, item)).To(Succeed())
方法二:利用资源状态字段(需要修改控制器)
如果你的控制器已经在资源的Status字段里记录了最后一次reconcile的时间(比如LastReconciledTime),你可以等待这个字段更新,以此判断reconcile完成:
步骤1:在控制器里更新状态
在你的Reconcile函数最后,更新容器的Status.LastReconciledTime:
func (r *ContainerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { // ... 你的reconcile逻辑 ... // 更新最后reconcile时间 container.Status.LastReconciledTime = metav1.NewTime(time.Now()) if err := r.Status().Update(ctx, &container); err != nil { return ctrl.Result{}, err } return ctrl.Result{}, nil }
步骤2:写等待状态更新的函数
func waitForContainerReconciled(ctx context.Context, client client.Client, container *foov1.Container, timeout time.Duration) error { deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { var latestContainer foov1.Container if err := client.Get(ctx, types.NamespacedName{Name: container.Name, Namespace: container.Namespace}, &latestContainer); err != nil { return err } // 检查最后reconcile时间是否晚于容器创建时间 if latestContainer.Status.LastReconciledTime != nil && latestContainer.Status.LastReconciledTime.After(container.CreationTimestamp.Time) { return nil } time.Sleep(100 * time.Millisecond) } return fmt.Errorf("container %s/%s was not reconciled within %v", container.Namespace, container.Name, timeout) }
步骤3:在测试中使用
Expect(testEnv.Create(ctx, container)).To(Succeed()) // 等待容器完成第一次reconcile Expect(waitForContainerReconciled(ctx, testEnv.Client, container, 5*time.Second)).To(Succeed()) // 再创建Item验证映射函数 Expect(testEnv.Create(ctx, item)).To(Succeed())
注意事项
- 方法一不需要修改控制器代码,是测试友好的方案;方法二需要控制器支持状态更新,但能更准确地判断reconcile是否完成(队列排空不代表reconcile逻辑执行完,只是任务出队了)
- 无论用哪种方法,都要设置合理的超时时间,避免测试无限挂起
- 不要用太短的检查间隔,避免占用过多CPU资源
内容来源于stack exchange
相关产品推荐
相关产品推荐

