Skip to content

registry: stop background goroutines leaked across registry rebuilds - #1

Open
ranjanprasad96 wants to merge 1 commit into
patch-memory-cachefrom
leak_fixes
Open

registry: stop background goroutines leaked across registry rebuilds#1
ranjanprasad96 wants to merge 1 commit into
patch-memory-cachefrom
leak_fixes

Conversation

@ranjanprasad96

@ranjanprasad96 ranjanprasad96 commented Aug 27, 2026

Copy link
Copy Markdown

Problem

anyscaled embeds this library to run a local image registry on every dataplane node, and rebuilds the registry in-process on every hourly STS credential rotation (AnyscaledRegistry.onConfigUpdateregistry.NewRegistry()). Three pieces of background work started by NewRegistry/NewApp are never stopped by App.Shutdown(), so each rebuild leaked one complete dead registry (~50 MB: mux router, S3 clients, config) pinned by orphaned goroutines — ~1.26 GB of heap growth per node over weeks of uptime.

The three leaks (all inherited from upstream; reported there as distribution/distribution repo):

  1. Upload purger — infinite for { purge; sleep } goroutine with no stop mechanism; holds the whole App as its context.
  2. Event broadcasterevents.NewBroadcaster goroutine, never Close()d.
  3. OTeltracing.InitOpenTelemetry runs per NewRegistry call, replacing the global TracerProvider and leaking a BatchSpanProcessor each time.

Fix

  • Make the app's context cancelable; Shutdown() cancels it and the purger loop selects on ctx.Done().
  • Shutdown() closes the event sink (broadcaster goroutine exits).
  • InitOpenTelemetry initializes process-global state at most once (success latches; failures stay retryable).
  • NewRegistry's error path now shuts the app down instead of discarding it.

Verification

  • TestAppShutdownCancelsContext: red without fix (context never canceled, sink stays open), green with.
  • TestRebuildDoesNotLeakGoroutines (new, mirrors anyscaled's rebuild pattern): without fix goroutines grow 5 → 35 over 10 rebuilds (+3/rebuild — exactly the three leaks); with fix, flat.
  • gofmt/vet clean; registry, registry/handlers, tracing suites + race detector pass. (TestGracefulShutdown fails identically on the untouched tree on macOS — pre-existing local flake, expect CI to be green.)

Branch base note

This branch is based on the v3.0.0-anyscale.3 tag (not main) so v3.0.0-anyscale.4 can be cut from it carrying all existing Anyscale patches — the diff therefore also shows the tag's golang-lru commit. Consumed by anyscale/product via its go.mod replace.

NewRegistry/NewApp start three pieces of background work that nothing
ever stops, so they outlive App.Shutdown() and pin the entire App
(router, storage driver, config) in memory. Any process that rebuilds
a registry in-process — as anyscaled does on every credential
rotation — leaks one full registry per rebuild:

- The upload purger goroutine loops forever with no stop mechanism.
  Make the app's context cancelable, cancel it in Shutdown(), and have
  the purger select on ctx.Done() instead of sleeping.
- The docker/go-events Broadcaster goroutine is never closed. Close
  the event sink in Shutdown().
- tracing.InitOpenTelemetry builds a new BatchSpanProcessor and
  replaces the global TracerProvider on every NewRegistry call without
  shutting down the previous one. It configures process-global state,
  so guard it with sync.Once.

Also stop the app's goroutines on the NewRegistry error path that
previously discarded the app without shutting it down.

Upstream issue: distribution#4942

Signed-off-by: ranjanprasad96 <ranjan.prasad@anyscale.com>
@ranjanprasad96
ranjanprasad96 changed the base branch from main to rc3-prep August 27, 2026 22:17
@ranjanprasad96
ranjanprasad96 changed the base branch from rc3-prep to patch-memory-cache August 27, 2026 22:18
Comment thread registry/handlers/app.go
context.Context

// cancel stops background goroutines started by NewApp (e.g. the upload purger).
cancel context.CancelFunc

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

instead of setting the cancel func on the struct, shouldn't the caller that creates the app via NewApp just pass in a ctx that has a cancel, and call cancel when they want to stop it?

@chrisfellowes-anyscale chrisfellowes-anyscale Sep 3, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

oh i see we don't have a run(ctx context.Context) function... ok nevermind this is fine

Comment thread tracing/tracing.go
// subsequent calls are no-ops (a failed attempt may be retried). This also keeps
// repeated registry construction (e.g. on config reload) from leaking a
// BatchSpanProcessor per call.
func InitOpenTelemetry(ctx context.Context) error {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

would it ever make sense to disable the otel setup entirely (ex: via a NewRegistry arg) and rely on the telemetry setup init'd by Anyscaled?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants