Skip to content

Commit 7eb966f

Browse files
[confmap] Fix a potential race condition between provider and resolver (#14018)
<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue. Ex. Adding a feature - Explain what this achieves.--> #### Description A provider CAN use the `watcher` callback if it detects any config changes. Moreover, it can also be called from different goroutine. However, resolver's `Shutdown` method closes the `watcher` channel first and then proceeds to close the providers. This results in a race condition/panic: - If the provider detects a new change and attempts to call the `watcher` callback from another goroutine, which sends the event on an already closed channel. This PR reorders the `Shutdown` method to close the providers first and then close the channel. It also documents that it is provider's responsibility to close and wait for any goroutines in its `Shutdown` method <!-- Issue number if applicable --> #### Link to tracking issue Fixes # <!--Describe what testing was performed and which tests were added.--> #### Testing <!--Describe the documentation added.--> #### Documentation <!--Please delete paragraphs that you did not use before submitting.--> --------- Co-authored-by: Alex Boten <[email protected]>
1 parent 266beca commit 7eb966f

File tree

4 files changed

+73
-2
lines changed

4 files changed

+73
-2
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: bug_fix
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver)
7+
component: pkg/confmap
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Fix a potential race condition in confmap by closing the providers first.
11+
12+
# One or more tracking issues or pull requests related to the change
13+
issues: [14018]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext:
19+
20+
# Optional: The change log or logs in which this entry should be included.
21+
# e.g. '[user]' or '[user, api]'
22+
# Include 'user' if the change is relevant to end users.
23+
# Include 'api' if there is a change to a library API.
24+
# Default: '[user]'
25+
change_logs: []

confmap/provider.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ type Provider interface {
8686
// This method must be called when the Collector service ends, either in case of
8787
// success or error. Retrieve cannot be called after Shutdown.
8888
//
89+
// Provider MUST shutdown and wait for any goroutine(s) that were created to call `watcher`, if any.
90+
//
8991
// Should never be called concurrently with itself or with Retrieve.
9092
// If ctx is cancelled should return immediately with an error.
9193
Shutdown(ctx context.Context) error

confmap/resolver.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,14 +243,14 @@ func (mr *Resolver) Watch() <-chan error {
243243
//
244244
// Should never be called concurrently with itself or Get.
245245
func (mr *Resolver) Shutdown(ctx context.Context) error {
246-
close(mr.watcher)
247-
248246
var errs error
249247
errs = multierr.Append(errs, mr.closeIfNeeded(ctx))
250248
for _, p := range mr.providers {
251249
errs = multierr.Append(errs, p.Shutdown(ctx))
252250
}
253251

252+
close(mr.watcher)
253+
254254
return errs
255255
}
256256

confmap/resolver_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,3 +524,47 @@ func newConfFromFile(tb testing.TB, fileName string) map[string]any {
524524

525525
return NewFromStringMap(data).ToStringMap()
526526
}
527+
528+
type provider struct {
529+
wg sync.WaitGroup
530+
}
531+
532+
func newRaceDetectorProvider() ProviderFactory {
533+
return NewProviderFactory(func(_ ProviderSettings) Provider {
534+
return &provider{}
535+
})
536+
}
537+
538+
func (p *provider) Retrieve(_ context.Context, _ string, watcher WatcherFunc) (*Retrieved, error) {
539+
p.wg.Add(1)
540+
go func() {
541+
// mock a config change event and wait for goroutine to return.
542+
defer p.wg.Done()
543+
watcher(&ChangeEvent{})
544+
}()
545+
return NewRetrieved(map[string]any{})
546+
}
547+
548+
func (p *provider) Scheme() string {
549+
return "race"
550+
}
551+
552+
func (p *provider) Shutdown(context.Context) error {
553+
p.wg.Wait()
554+
return nil
555+
}
556+
557+
func TestProviderRaceCondition(t *testing.T) {
558+
resolver, err := NewResolver(ResolverSettings{
559+
URIs: []string{"race:"},
560+
ProviderFactories: []ProviderFactory{
561+
newRaceDetectorProvider(),
562+
},
563+
ConverterFactories: nil,
564+
})
565+
require.NoError(t, err)
566+
c, err := resolver.Resolve(context.Background())
567+
require.NoError(t, err)
568+
require.NotNil(t, c)
569+
require.NoError(t, resolver.Shutdown(context.Background()))
570+
}

0 commit comments

Comments
 (0)