Skip to content
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

✨Add an option to recover panic for controller reconcile #1627

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions pkg/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ type Options struct {
// CacheSyncTimeout refers to the time limit set to wait for syncing caches.
// Defaults to 2 minutes if not set.
CacheSyncTimeout time.Duration

// RecoverPanic indicates whether the panic caused by reconcile should be recovered.
RecoverPanic bool
}

// Controller implements a Kubernetes API. A Controller manages a work queue fed reconcile.Requests
Expand Down Expand Up @@ -133,5 +136,6 @@ func NewUnmanaged(name string, mgr manager.Manager, options Options) (Controller
SetFields: mgr.SetFields,
Name: name,
Log: options.Log.WithName("controller").WithName(name),
RecoverPanic: options.RecoverPanic,
}, nil
}
17 changes: 15 additions & 2 deletions pkg/internal/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ type Controller struct {

// Log is used to log messages to users during reconciliation, or for example when a watch is started.
Log logr.Logger

// RecoverPanic indicates whether the panic caused by reconcile should be recovered.
RecoverPanic bool
}

// watchDescription contains all the information necessary to start a watch.
Expand All @@ -95,7 +98,17 @@ type watchDescription struct {
}

// Reconcile implements reconcile.Reconciler.
func (c *Controller) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
func (c *Controller) Reconcile(ctx context.Context, req reconcile.Request) (_ reconcile.Result, err error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, what I meant by the other comment was:

  1. Please add an option to Controller to make this configurable (i.E. RecoverPanic bool)
  2. Make the handling here conditional (i.E. if c.RecoverPanic { defer recover() })
  3. Expose the option in pkg/controller.Options

The reason for favoring that over the current approach is that with the current approach we require ppl to read internal packages to understand how they can get panics recovered, which is not good UX.

Sorry for being unclear about this before.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understand, I will fix it. So what do you think of webhook handler? Should its panic default to be recovered or another option field to configure it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should also have an option to set this up and by default panic

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated. It is hard to add the option for webhook. Maybe we can firstly add an option for controller reconcile.

if c.RecoverPanic {
defer func() {
if r := recover(); r != nil {
for _, fn := range utilruntime.PanicHandlers {
fn(r)
}
err = fmt.Errorf("panic: %v [recovered]", r)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add the stack here, otherwise this is almost impossible to debug? From runtime/debug.Stack()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe the utilruntime.PanicHandlers above can print the stack of panic more properly
https://github.com/kubernetes/apimachinery/blob/f916759cb6b8547418dc7708876ecab5c1961448/pkg/util/runtime/runtime.go#L61

}
}()
}
log := c.Log.WithValues("name", req.Name, "namespace", req.Namespace)
ctx = logf.IntoContext(ctx, log)
return c.Do.Reconcile(ctx, req)
Expand Down Expand Up @@ -295,7 +308,7 @@ func (c *Controller) reconcileHandler(ctx context.Context, obj interface{}) {

// RunInformersAndControllers the syncHandler, passing it the Namespace/Name string of the
// resource to be synced.
result, err := c.Do.Reconcile(ctx, req)
result, err := c.Reconcile(ctx, req)
switch {
case err != nil:
c.Queue.AddRateLimited(req)
Expand Down
33 changes: 33 additions & 0 deletions pkg/internal/controller/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,39 @@ var _ = Describe("controller", func() {
Expect(err).NotTo(HaveOccurred())
Expect(result).To(Equal(reconcile.Result{Requeue: true}))
})

It("should not recover panic if RecoverPanic is false by default", func() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

defer func() {
Expect(recover()).ShouldNot(BeNil())
}()
ctrl.Do = reconcile.Func(func(context.Context, reconcile.Request) (reconcile.Result, error) {
var res *reconcile.Result
return *res, nil
})
_, _ = ctrl.Reconcile(ctx,
reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "foo", Name: "bar"}})
})

It("should recover panic if RecoverPanic is true", func() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

defer func() {
Expect(recover()).To(BeNil())
}()
ctrl.RecoverPanic = true
ctrl.Do = reconcile.Func(func(context.Context, reconcile.Request) (reconcile.Result, error) {
var res *reconcile.Result
return *res, nil
})
_, err := ctrl.Reconcile(ctx,
reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "foo", Name: "bar"}})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("[recovered]"))
})
})

Describe("Start", func() {
Expand Down