Skip to content

Commit

Permalink
Introduce staggered exit when caps change
Browse files Browse the repository at this point in the history
This patch changes the behavior of how the pod is exited when the
capabilities have changed. Instead of all the replicas exiting at
the same time, the exits are staggered. If the pod is the leader
or no leader election is configured, the pod is exited after a
brief delay. Otherwise the pod is exited immediately.

This should allow the non-leaders to come back online before the
leader is exited, enabling one of the restarted pods to take over
as the leader when it exits.
  • Loading branch information
akutz committed Feb 5, 2025
1 parent 035929e commit 45a6163
Show file tree
Hide file tree
Showing 17 changed files with 209 additions and 30 deletions.
4 changes: 3 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ linters-settings:
- alias: pkgctx
pkg: github.com/vmware-tanzu/vm-operator/pkg/context
- alias: pkgerr
pkg: github.com/vmware-tanzu/vm-operator/pkg/pkgerr
pkg: github.com/vmware-tanzu/vm-operator/pkg/errors
- alias: pkgexit
pkg: github.com/vmware-tanzu/vm-operator/pkg/exit
- alias: ctxop
pkg: github.com/vmware-tanzu/vm-operator/pkg/context/operation
- alias: pkgmgr
Expand Down
2 changes: 2 additions & 0 deletions config/local/vmoperator/local_env_var_patch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ spec:
value: "true"
- name: ASYNC_CREATE_ENABLED
value: "true"
- name: EXIT_DELAY
value: "3s"
- name: MEM_STATS_PERIOD
value: "10m"
- name: VSPHERE_NETWORKING
Expand Down
6 changes: 6 additions & 0 deletions config/wcp/vmoperator/manager_env_var_patch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@
name: PRIVILEGED_USERS
value: "<COMMA_SEPARATED_LIST_OF_USERS>"

- op: add
path: /spec/template/spec/containers/0/env/-
value:
name: EXIT_DELAY
value: "3s"

- op: add
path: /spec/template/spec/containers/0/env/-
value:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ import (
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/manager"

"github.com/vmware-tanzu/vm-operator/controllers/infra/capability/exit"
pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config"
"github.com/vmware-tanzu/vm-operator/pkg/config/capabilities"
pkgctx "github.com/vmware-tanzu/vm-operator/pkg/context"
pkgexit "github.com/vmware-tanzu/vm-operator/pkg/exit"
pkgmgr "github.com/vmware-tanzu/vm-operator/pkg/manager"
"github.com/vmware-tanzu/vm-operator/pkg/record"
kubeutil "github.com/vmware-tanzu/vm-operator/pkg/util/kube"
Expand Down Expand Up @@ -53,6 +53,7 @@ func AddToManager(ctx *pkgctx.ControllerManagerContext, mgr manager.Manager) err
cache,
ctrl.Log.WithName("controllers").WithName(controllerName),
record.New(mgr.GetEventRecorderFor(controllerNameLong)),
mgr.Elected(),
)

// This controller is also run on the non-leaders (webhooks) pods too
Expand Down Expand Up @@ -89,13 +90,15 @@ func NewReconciler(
ctx context.Context,
client ctrlclient.Reader,
logger logr.Logger,
recorder record.Recorder) *Reconciler {
recorder record.Recorder,
elected <-chan struct{}) *Reconciler {

return &Reconciler{
Context: ctx,
Client: client,
Logger: logger,
Recorder: recorder,
Elected: elected,
}
}

Expand All @@ -104,6 +107,7 @@ type Reconciler struct {
Client ctrlclient.Reader
Logger logr.Logger
Recorder record.Recorder
Elected <-chan struct{}
}

// +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch
Expand All @@ -120,8 +124,10 @@ func (r *Reconciler) Reconcile(
}

if capabilities.UpdateCapabilitiesFeatures(ctx, obj) {
r.Logger.Info("killing pod due to changed capabilities")
exit.Exit()
pkgexit.Exit(
logr.NewContext(ctx, r.Logger),
"capabilities have changed",
r.Elected)
}

return ctrl.Result{}, nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,21 @@ package capability_test
import (
"sync/atomic"
"testing"
"time"

. "github.com/onsi/ginkgo/v2"

capability "github.com/vmware-tanzu/vm-operator/controllers/infra/capability/configmap"
"github.com/vmware-tanzu/vm-operator/controllers/infra/capability/exit"
pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config"
pkgexit "github.com/vmware-tanzu/vm-operator/pkg/exit"
"github.com/vmware-tanzu/vm-operator/pkg/manager"
"github.com/vmware-tanzu/vm-operator/test/builder"
)

var numExits int32

func init() {
exit.Exit = func() {
pkgexit.ExitFn = func(_ time.Duration) {
atomic.AddInt32(&numExits, 1)
}
}
Expand Down
14 changes: 10 additions & 4 deletions controllers/infra/capability/crd/crd_capability_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ import (
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/predicate"

"github.com/vmware-tanzu/vm-operator/controllers/infra/capability/exit"
capv1 "github.com/vmware-tanzu/vm-operator/external/capabilities/api/v1alpha1"
pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config"
"github.com/vmware-tanzu/vm-operator/pkg/config/capabilities"
pkgctx "github.com/vmware-tanzu/vm-operator/pkg/context"
pkgexit "github.com/vmware-tanzu/vm-operator/pkg/exit"
"github.com/vmware-tanzu/vm-operator/pkg/record"
"github.com/vmware-tanzu/vm-operator/pkg/util/ptr"
)
Expand All @@ -39,6 +39,7 @@ func AddToManager(ctx *pkgctx.ControllerManagerContext, mgr manager.Manager) err
mgr.GetClient(),
ctrl.Log.WithName("controllers").WithName(controllerName),
record.New(mgr.GetEventRecorderFor(controllerNameLong)),
mgr.Elected(),
)

return ctrl.NewControllerManagedBy(mgr).
Expand Down Expand Up @@ -66,13 +67,15 @@ func NewReconciler(
ctx context.Context,
client ctrlclient.Client,
logger logr.Logger,
recorder record.Recorder) *Reconciler {
recorder record.Recorder,
elected <-chan struct{}) *Reconciler {

return &Reconciler{
Context: ctx,
Client: client,
Logger: logger,
Recorder: recorder,
Elected: elected,
}
}

Expand All @@ -81,6 +84,7 @@ type Reconciler struct {
Client ctrlclient.Client
Logger logr.Logger
Recorder record.Recorder
Elected <-chan struct{}
}

// +kubebuilder:rbac:groups=iaas.vmware.com,resources=capabilities,verbs=get;list;watch
Expand All @@ -98,8 +102,10 @@ func (r *Reconciler) Reconcile(
}

if capabilities.UpdateCapabilitiesFeatures(ctx, obj) {
r.Logger.Info("killing pod due to changed capabilities")
exit.Exit()
pkgexit.Exit(
logr.NewContext(ctx, r.Logger),
"capabilities have changed",
r.Elected)
}

return ctrl.Result{}, nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,21 @@ package capability_test
import (
"sync/atomic"
"testing"
"time"

. "github.com/onsi/ginkgo/v2"

capability "github.com/vmware-tanzu/vm-operator/controllers/infra/capability/crd"
"github.com/vmware-tanzu/vm-operator/controllers/infra/capability/exit"
pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config"
pkgexit "github.com/vmware-tanzu/vm-operator/pkg/exit"
"github.com/vmware-tanzu/vm-operator/pkg/manager"
"github.com/vmware-tanzu/vm-operator/test/builder"
)

var numExits int32

func init() {
exit.Exit = func() {
pkgexit.ExitFn = func(_ time.Duration) {
atomic.AddInt32(&numExits, 1)
}
}
Expand Down
17 changes: 0 additions & 17 deletions controllers/infra/capability/exit/exit.go

This file was deleted.

7 changes: 7 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config"
"github.com/vmware-tanzu/vm-operator/pkg/config/capabilities"
pkgctx "github.com/vmware-tanzu/vm-operator/pkg/context"
pkgexit "github.com/vmware-tanzu/vm-operator/pkg/exit"
pkgmgr "github.com/vmware-tanzu/vm-operator/pkg/manager"
pkgmgrinit "github.com/vmware-tanzu/vm-operator/pkg/manager/init"
"github.com/vmware-tanzu/vm-operator/pkg/mem"
Expand Down Expand Up @@ -72,6 +73,8 @@ func main() {
"buildtype", pkg.BuildType,
"commit", pkg.BuildCommit)

initExitFn()

initContext()

initFlags()
Expand Down Expand Up @@ -128,6 +131,10 @@ func initMemStats() {
metrics.Registry.MustRegister)
}

func initExitFn() {
pkgexit.ExitFn = func(_ time.Duration) { os.Exit(1) }
}

func initContext() {
ctx = pkgcfg.WithConfig(defaultConfig)
ctx = cource.WithContext(ctx)
Expand Down
8 changes: 8 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,14 @@ type Config struct {
// such as passwords. Defaults to false.
LogSensitiveData bool

// ExitDelay may be set to a time.Duration value and is the maximum delay
// used when exiting the pod due to an event that requires the pod be
// restarted, ex. the capabilities have changed. The exit will be delayed
// by some duration between 0-ExitDelay.
//
// Defaults to 3s.
ExitDelay time.Duration

// AsyncSignalEnabled may be set to false to disable the vm-watcher service
// used to reconcile VirtualMachine objects if their backend state has
// changed.
Expand Down
1 change: 1 addition & 0 deletions pkg/config/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func Default() Config {
LeaderElectionID: defaultPrefix + "controller-manager-runtime",
MaxCreateVMsOnProvider: 80,
MaxConcurrentReconciles: 1,
ExitDelay: 3 * time.Second,
AsyncSignalEnabled: true,
AsyncCreateEnabled: true,
MemStatsPeriod: 10 * time.Minute,
Expand Down
1 change: 1 addition & 0 deletions pkg/config/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func FromEnv() Config {
setBool(env.VSphereNetworking, &config.VSphereNetworking)
setStringSlice(env.PrivilegedUsers, &config.PrivilegedUsers)
setBool(env.LogSensitiveData, &config.LogSensitiveData)
setDuration(env.ExitDelay, &config.ExitDelay)
setBool(env.AsyncSignalEnabled, &config.AsyncSignalEnabled)
setBool(env.AsyncCreateEnabled, &config.AsyncCreateEnabled)
setDuration(env.MemStatsPeriod, &config.MemStatsPeriod)
Expand Down
3 changes: 3 additions & 0 deletions pkg/config/env/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type VarName uint8
const (
_varNameBegin VarName = iota

ExitDelay
DefaultVMClassControllerName
MaxCreateVMsOnProvider
CreateVMRequeueDelay
Expand Down Expand Up @@ -89,6 +90,8 @@ func All() []VarName {
//nolint:gocyclo
func (n VarName) String() string {
switch n {
case ExitDelay:
return "EXIT_DELAY"
case DefaultVMClassControllerName:
return "DEFAULT_VM_CLASS_CONTROLLER_NAME"
case MaxCreateVMsOnProvider:
Expand Down
2 changes: 2 additions & 0 deletions pkg/config/env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ var _ = Describe(
Expect(os.Setenv("POWERED_ON_VM_HAS_IP_REQUEUE_DELAY", "126h")).To(Succeed())
Expect(os.Setenv("MEM_STATS_PERIOD", "127h")).To(Succeed())
Expect(os.Setenv("SYNC_IMAGE_REQUEUE_DELAY", "128h")).To(Succeed())
Expect(os.Setenv("EXIT_DELAY", "129h")).To(Succeed())
})
It("Should return a default config overridden by the environment", func() {
Expect(config).To(BeComparableTo(pkgcfg.Config{
Expand Down Expand Up @@ -162,6 +163,7 @@ var _ = Describe(
PoweredOnVMHasIPRequeueDelay: 126 * time.Hour,
MemStatsPeriod: 127 * time.Hour,
SyncImageRequeueDelay: 128 * time.Hour,
ExitDelay: 129 * time.Hour,
}))
})
})
Expand Down
53 changes: 53 additions & 0 deletions pkg/exit/exit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) 2024 Broadcom. All Rights Reserved.
// Broadcom Confidential. The term "Broadcom" refers to Broadcom Inc.
// and/or its subsidiaries.

package exit

import (
"context"
"math/rand/v2"
"time"

"github.com/go-logr/logr"

pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config"
)

// ExitFn is used in testing to assert the Exit() function is called.
// The default value is a no-op.
var ExitFn func(exitDelay time.Duration)

func init() {
ExitFn = func(_ time.Duration) {}
}

// Exit will exit the pod immediately if it is not the leader, otherwise if
// the pod is the leader or leader election is not enabled, the pod will exit
// after a brief delay.
func Exit(
ctx context.Context,
reason string,
elected <-chan struct{}) {

var exitDelay time.Duration
select {
case <-elected:
// When the leader or when there is no leader election, delay the
// exit up to some random amount of time.
n := rand.Float64() //nolint:gosec
exitDelay = time.Duration(n * float64(pkgcfg.FromContext(ctx).ExitDelay))
default:
// When not the leader, exit immediately. This should allow one of
// the non-leader pods to come back online to become the leader when
// the leader pod exits.
}
if exitDelay > 0 {
time.Sleep(exitDelay)
}

logr.FromContextOrDiscard(ctx).
Info("Exiting pod", "exitDelay", exitDelay, "reason", reason)

ExitFn(exitDelay)
}
17 changes: 17 additions & 0 deletions pkg/exit/exit_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// © Broadcom. All Rights Reserved.
// The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries.
// SPDX-License-Identifier: Apache-2.0

package exit_test

import (
"testing"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

func TestExit(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Exit Suite")
}
Loading

0 comments on commit 45a6163

Please sign in to comment.