add_docker_metadata: fix data races in lazy cgroup cache init#51688
add_docker_metadata: fix data races in lazy cgroup cache init#51688orestisfl wants to merge 1 commit into
Conversation
lazyCgroupCacheInit did an unguarded nil-check-and-write of d.cgroups while concurrent Run calls read it, racing on the pointer and able to start multiple janitor goroutines; the path is on by default via match_pids and reachable whenever one instance runs events from multiple goroutines, for example as a beat-level global processor. Guard the init with sync.Once and publish the cache through atomic.Pointer. common.Cache.Get held only the read lock while updating the element's last access time on access-expiry caches; take the exclusive lock. Both races were confirmed with -race before the fix; the new concurrent-Run test passes 600+ stress runs after it.
🤖 GitHub commentsJust comment with:
|
|
Pinging @elastic/elastic-agent-data-plane (Team:Elastic-Agent-Data-Plane) |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Enterprise Run ID: 📒 Files selected for processing (4)
👮 Files not reviewed due to content moderation or server errors (4)
📝 Walkthrough
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR fixes two data races that can occur when add_docker_metadata is used as a beat-level (shared) processor and Run() is invoked concurrently, by making cgroup-cache initialization and cache reads/writes safe under concurrency.
Changes:
- Make the PID→container-ID cgroup cache lazily initialized in a concurrency-safe way using
sync.Once+atomic.Pointer. - Fix a race in
libbeat/common.Cache.Get()by taking an exclusive lock whenGet()may update an element’s last-access time. - Add a regression test that runs concurrent
Run()calls to validate the fix under-race, and add a changelog fragment.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
libbeat/processors/add_docker_metadata/add_docker_metadata.go |
Concurrency-safe lazy init and access for the cgroup cache used by PID matching. |
libbeat/processors/add_docker_metadata/add_docker_metadata_test.go |
New concurrent regression test covering shared-processor Run() calls. |
libbeat/common/cache.go |
Use exclusive locking in Cache.Get() to prevent races when updating last-access time. |
changelog/fragments/1783070200-fix-add-docker-metadata-cgroup-cache-races.yaml |
Changelog entry for the race fix. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if d.cgroups != nil { | ||
| d.cgroups.StopJanitor() | ||
| if cgroups := d.cgroups.Load(); cgroups != nil { | ||
| cgroups.StopJanitor() |
There was a problem hiding this comment.
I think we have a problem if Close is ever called before cgroupCache. We would get a Startjanitor goroutine leak. I had Claude code up a test with a potential fix commented out. Let me know what you think.
It is possible this never happens but we have had enough bugs with Start/Stop ordering and races I think it is at least worth looking at.
khushijain21
left a comment
There was a problem hiding this comment.
aside from Lee's comment, this looks good to me
| func (c *Cache) Get(k Key) Value { | ||
| c.RLock() | ||
| defer c.RUnlock() | ||
| // Exclusive lock because get can update the element's last access time. |
There was a problem hiding this comment.
[nit]
It's just that while reading it sounded bizzare at first
| // Exclusive lock because get can update the element's last access time. | |
| // Exclusive lock because Get() can update the element's last access time. |
Proposed commit message
Checklist
I have made corresponding changes to the documentationI have made corresponding change to the default configuration filesstresstest.shscript to run them under stress conditions and race detector to verify their stability../changelog/fragmentsusing the changelog tool.Disruptive User Impact
None.
How to test this PR locally
Regression test
Run the new test with the stress test script:
./script/stresstest.sh --race ./libbeat/processors/add_docker_metadata '^TestMatchPIDsConcurrent$' -p 32End-to-end reproduction
A running Docker daemon is required, otherwise
Runreturns early before reaching the cgroup cache./tmp/dmrepro/filebeat.ymlwith 24filestreaminputs (each a distinctid), anadd_fieldsthat setsprocess.pidandadd_docker_metadataas a global processor:Config
On
mainthe run aborts within a second with aWARNING: DATA RACE(exit code 66) on the shared processor's cgroup cache: