Skip to content

Fix rep crashes when updating metric tags of running LRPs - #1222

Merged
ameowlia merged 3 commits into
cloudfoundry:developfrom
plamen-bardarov:fix/log-streamer-metric-tags-race
Oct 29, 2026
Merged

ameowlia merged 3 commits into
cloudfoundry:developfrom
plamen-bardarov:fix/log-streamer-metric-tags-race

Conversation

@plamen-bardarov

Copy link
Copy Markdown
Contributor

Summary

Fixes two rep crashes in the metric tag update path for running LRPs. Both were introduced in 83f58c5 (#662).

fatal error: concurrent map read and map write / concurrent map iteration and map write

A container's log streamer shares one tags map between the stdout and stderr destinations, every WithSource child (CELL, HEALTH, PROXY, ...) and the log rate limiter. UpdateTags changed that map in place while holding only one destination's processLock. The other destinations read it under their own locks, and the rate limiter's metrics goroutine reads it with no lock. rep crashes in SendAppLog / WithEnvelopeTags, called from streamDestination.flush.

The tags now live in a copy-on-write tagStore (atomic.Pointer[map[string]string]) shared by all of them. UpdateTags swaps in a new map, and readers get one that is never changed afterwards.

nil pointer dereference in storeNode.Update

logStreamer is only set in Run and gardenContainer only in Create, so updating the metric tags of a container that isn't running yet panics. net/http recovers the panic, but the update request fails. Update now skips both calls when the field is nil. Create and Run pick up the new tags from n.info.LogConfig.

Tests

  • New log_streamer spec using an ingress client that reads the tags map. The counterfeiter fake only stores the map, so the existing race spec can't detect this.
  • New containerstore spec for updating a container that has been created but not yet run.

Both specs fail on develop (data race / panic) and pass with this change. ./scripts/test-in-docker.bash passes for MySQL and Postgres.

Backward Compatibility

Breaking Change? No

No API, config or behaviour changes. Tag updates still reach every log source of the container.

A container's log streamer shares one tags map between the stdout and
stderr destinations, every WithSource child and the log rate limiter.
Each destination guards it with its own processLock and the rate limiter
metrics goroutine uses no lock at all, while UpdateTags mutated the map in
place. Concurrent flushes on another destination therefore raced with
UpdateTags and rep crashed with "fatal error: concurrent map read and map
write" / "concurrent map iteration and map write" in SendAppLog.

Replace the shared map with a copy-on-write tagStore: UpdateTags publishes
a new map atomically and readers use an immutable snapshot.

The regression test uses an ingress client that reads the tags map like
diego-logging-client/go-loggregator do; the counterfeiter fake only stores
the reference, which is why the existing race test did not detect this.

Regression from 83f58c5 ("Support update of metric tags for running LRPs").
…ning

storeNode.logStreamer is only set in Run and gardenContainer only in
Create, so an UpdateLRPInstance with metric tags for an instance that was
claimed but not yet running dereferenced nil ("http: panic serving ...
invalid memory address or nil pointer dereference" at storenode.go).

Skip the log streamer and garden property updates when they do not exist
yet. Create and Run hold the same opLock and build from n.info.LogConfig,
which Update has already changed, so the new tags still take effect.

Regression from 83f58c5 ("Support update of metric tags for running LRPs").

@ameowlia ameowlia left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

My AI has two suggestions for further testing. But other than that it all looks good to me.

[MINOR] — executor/depot/containerstore/containerstore_test.go, storenode.go:759 / No test for Update before Create (gardenContainer nil) — Test Coverage
The new spec covers only "created, not run" (logStreamer nil, gardenContainer set). Update in the Reserved/Initialized state (gardenContainer nil) is never tested, and nothing checks that Create sends the updated log_config property. By reading the code it does: Create copies n.info at :202 under opLock. Fix: add a spec that calls Update before Create, then asserts no panic and that the log_config property sent to garden at Create contains the new tags.

[MINOR] — executor/depot/log_streamer/tag_store_test.go / tagStore isolation paths not tested — Test Coverage
Two paths have no test: Store(nil), and a caller mutating its map after Store. The second is the isolation that defensive copying is meant to give. Fix: add unit specs for Store(nil) and for mutating the map after Store, asserting that Load is unaffected.

…tion

- containerstore: Update in the Reserved and Initialized states does not
  panic, and Create sends the updated tags to garden in the log_config
  property.
- tagStore: Store(nil) results in an empty map, and changing the caller's
  map after Store or storing new tags does not change what was stored or
  already loaded.
@plamen-bardarov

Copy link
Copy Markdown
Contributor Author

Done

@github-project-automation github-project-automation Bot moved this from Inbox to Pending Merge | Prioritized in Application Runtime Platform Working Group Oct 29, 2026
@ameowlia
ameowlia merged commit 33cf497 into cloudfoundry:develop Oct 29, 2026
10 checks passed
@github-project-automation github-project-automation Bot moved this from Pending Merge | Prioritized to Done in Application Runtime Platform Working Group Oct 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

2 participants