From 5c181105054ed3f47b928d71bb49d6dea9c82c3a Mon Sep 17 00:00:00 2001 From: Leo Antunes Date: Fri, 26 Jun 2026 15:58:53 +0200 Subject: [PATCH] perf: skip watchdog goroutine and dedup for uncancellable contexts Every call spawned a watchdog goroutine plus a context.WithCancelCause allocation, and wrapped the observer in a dedup layer (a second allocation) to guard against that goroutine racing the deferred observe. The watchdog only exists to record a context cancellation/deadline promptly. When the incoming context can never be canceled (ctx.Done() == nil, e.g. context.Background()), it can never fire usefully, so skip both the goroutine and the context allocation. With no watchdog there is no second observer (normal return and panic go through the same single defer), so the dedup wrapper is unnecessary too and is moved inside the watchdog branch. For uncancellable-context callers this removes a goroutine and drops the per-call allocation count from 2 to 1. Callers passing a cancellable or deadline context are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) --- hoglet.go | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/hoglet.go b/hoglet.go index d2bb3c7..f102b4c 100644 --- a/hoglet.go +++ b/hoglet.go @@ -243,14 +243,25 @@ func Wrap[IN, OUT any](c *Circuit, f WrappableFunc[IN, OUT]) WrappableFunc[IN, O return out, err } - // ensure we dedup the final - potentially wrapped - observer. - obs = dedupObservableCall(obs) - - obsCtx, cancel := context.WithCancelCause(ctx) - defer cancel(errWrappedFunctionDone) - - // TODO: we could skip this if we could ensure the original context has neither cancellation nor deadline - go c.observeCtx(obs, obsCtx) + // The watchdog goroutine exists to record a context cancellation/deadline as a failure promptly, even if the + // wrapped function ignores its context and blocks. If the context can never be canceled (no deadline and no + // cancellation, e.g. [context.Background]), the watchdog can never fire usefully, so we skip it and the + // associated context allocation entirely, relying solely on the deferred observation below. + // + // TODO: allow skipping the watchdog via an option for callers that guarantee their wrapped function respects + // its context, trading prompt cancellation detection for one less goroutine + context allocation per call. + if ctx.Done() != nil { + // Only here can the watchdog race the deferred observe, so dedup to ensure the - potentially wrapped - + // observer is observed exactly once. Without a watchdog the deferred func below is the sole observer + // (normal return and panic go through the same defer and are mutually exclusive), so no dedup is needed. + // This relies on breaker middleware observing synchronously; an async middleware observer must dedup itself. + obs = dedupObservableCall(obs) + + obsCtx, cancel := context.WithCancelCause(ctx) + defer cancel(errWrappedFunctionDone) + + go c.observeCtx(obs, obsCtx) + } defer func() { // ensure we also open the breaker on panics