fix(shared): clean up zero-count entries in ConnectionLimiter to prevent memory leak - #3449
fix(shared): clean up zero-count entries in ConnectionLimiter to prevent memory leak#3449chill-czar wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 119faf42c5
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| l.connections.RemoveCb(key, func(k string, v *atomic.Int64, exists bool) bool { | ||
| return exists && v.Load() == 0 | ||
| }) |
There was a problem hiding this comment.
Keep zero-count cleanup from deleting a newly acquired counter
When the last Release runs concurrently with a new request for the same key, TryAcquire can get the existing zero-valued counter from Upsert but increment it only after this RemoveCb has observed v.Load() == 0; the callback then deletes the map entry even though the new connection successfully acquired the slot. In the proxy/firewall paths that use this limiter, that live connection is no longer tracked, so later releases can undercount a different counter or connection limits can be bypassed for the sandbox lifecycle.
AGENTS.md reference: AGENTS.md:L22-L26
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch! Fixed in commit e4f15b5 using atomic tombstone state (-1):
- In
Release: When a counter decrements from1to0,Releaseattemptscounter.CompareAndSwap(0, -1)to mark the counter instance as tombstoned. IfCAS(0, -1)succeeds,ReleasecallsRemoveCbto removekeyfrom the map if it still references that tombstoned counter instance. If a concurrentTryAcquireacquired the counter beforeCAS(0, -1),CAS(0, -1)fails andReleaseleaves the map entry intact. - In
TryAcquire: If a counter has state-1(current < 0),TryAcquiredetects the tombstoned counter and loops to re-fetch/insert a fresh counter instance viagetCounter.getCounteratomically replaces tombstoned counters insmap.Upsert. - Tests: Added
TestConnectionLimiter_Concurrent/concurrent_acquire_during_release_does_not_orphan_connectionsinlimiter_test.goto verify zero untracked connections under high concurrency.
Closes #3448
Summary
ConnectionLimitermap when all active connections for a key are released.RemoveCbinRelease(key)withinpackages/shared/pkg/connlimit/limiter.go.packages/shared/pkg/connlimit/limiter_test.goasserting zero-count map key cleanup and fresh re-acquisition behavior.Why
ConnectionLimiterinpackages/shared/pkg/connlimittracks per-key concurrent connections in ansmap.Map[*atomic.Int64]. Previously,Release(key)decremented the counter to0viaCompareAndSwap, but never removed the key entry from the underlying concurrent map.For generic HTTP proxy connection limiting and client-IP rate limiting where there is no sandbox termination event to invoke
Remove(key)manually, every historic unique key remained in memory with count0. On long-running edge proxy services (client-proxyandorchestrator/pkg/tcpfirewall), this resulted in steady heap memory accumulation over time.smap.RemoveCbchecksv.Load() == 0undersmap's internal shard lock, guaranteeing zero-count keys are safely removed without race conditions against concurrentTryAcquirecalls.Test Plan
go test -race -v ./packages/shared/pkg/connlimit/...limiter.connections.Count(). assertions-race)go fmt ./...packages/sharedtest suite/cc @jakubno @dobrac @ValentaTomas @arkamar @tvi