Skip to content

Commit 1a782df

Browse files
authored
Add helper to create a sub-registry or return the existing one if present (#313)
When creating a child registry, code often checks if the registry already exists, since creating a duplicate entry will cause a panic. There are two issues with this: - Most registry creation has extra identical boilerplate to check if a registry exists before creating it - This boilerplate is not concurrency-safe, because the registry lock is not held continuously; it's possible for two goroutines to both check the same registry name, both observe that it does not exist yet, and both try to create it. This is rare (I don't know of confirmed occurrences) but if it does happen the entire process will panic. This PR adds a helper that handles both these issues, replacing the usual checks with a one-line call that properly holds the lock between checking for existing registries and creating new ones.
1 parent 26c72b3 commit 1a782df

File tree

2 files changed

+63
-10
lines changed

2 files changed

+63
-10
lines changed

monitoring/registry.go

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -106,22 +106,52 @@ func (r *Registry) Get(name string) Var {
106106

107107
// GetRegistry tries to find a sub-registry by name.
108108
func (r *Registry) GetRegistry(name string) *Registry {
109-
e, err := r.find(name)
110-
if err != nil {
111-
return nil
109+
if v := r.Get(name); v != nil {
110+
if reg, ok := v.(*Registry); ok {
111+
return reg
112+
}
112113
}
114+
return nil
115+
}
113116

114-
v := e.Var
115-
if v == nil {
116-
return nil
117+
// Creates new recursive registries under the given sequence of names,
118+
// returning the final leaf node.
119+
// r.mu must be held by the caller.
120+
func (r *Registry) newRegistryChainWithLock(names []string, opts *options) *Registry {
121+
cur := r
122+
for _, name := range names {
123+
sub := &Registry{
124+
name: fullName(cur, name),
125+
opts: opts,
126+
entries: map[string]entry{},
127+
}
128+
cur.entries[name] = entry{sub, sub.opts.mode}
129+
cur = sub
117130
}
131+
return cur
132+
}
118133

119-
reg, ok := v.(*Registry)
120-
if !ok {
121-
return nil
134+
func (r *Registry) GetOrCreateRegistry(name string, opts ...Option) *Registry {
135+
names := strings.Split(name, ".")
136+
return r.getOrCreateRegistry(names, opts...)
137+
}
138+
139+
func (r *Registry) getOrCreateRegistry(names []string, opts ...Option) *Registry {
140+
if len(names) == 0 {
141+
return r
122142
}
143+
name := names[0]
144+
r.mu.Lock()
145+
defer r.mu.Unlock()
123146

124-
return reg
147+
if entry, found := r.entries[name]; found {
148+
reg, ok := entry.Var.(*Registry)
149+
if !ok {
150+
return nil
151+
}
152+
return reg.getOrCreateRegistry(names[1:], opts...)
153+
}
154+
return r.newRegistryChainWithLock(names, applyOpts(r.opts, opts))
125155
}
126156

127157
// Remove removes a variable or a sub-registry by name

monitoring/registry_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,26 @@ func TestRegistryIter(t *testing.T) {
155155

156156
assert.Equal(t, vars, collected)
157157
}
158+
159+
func TestGetOrCreateRegistry(t *testing.T) {
160+
root := &Registry{
161+
name: "root",
162+
entries: map[string]entry{},
163+
opts: &defaultOptions,
164+
}
165+
166+
require.Nil(t, root.GetRegistry("a.b.c"), "GetRegistry on empty registry always returns nil")
167+
168+
c := root.GetOrCreateRegistry("a.b.c")
169+
require.NotNil(t, c, "GetOrCreateRegistry must be successful on an empty registry")
170+
assert.Equal(t, c, root.GetRegistry("a.b.c"), "GetRegistry after GetOrCreateRegistry should return the same value")
171+
assert.Equal(t, "root.a.b.c", c.name, "Registries created with GetOrCreateRegistry should contain the parent name followed by the path to the registry")
172+
173+
y := c.GetOrCreateRegistry("z.y")
174+
require.NotNil(t, y, "GetOrCreateRegistry must be successful on an empty registry")
175+
assert.Equal(t, y, c.GetOrCreateRegistry("z.y"), "GetOrCreateRegistry on the same input should return the same result")
176+
assert.Equal(t, c.GetOrCreateRegistry("z.y"), root.GetOrCreateRegistry("a.b.c.z.y"), "GetOrCreateRegistry with equivalent paths from different starting points should return the same result")
177+
178+
c.Add("scalar", &Int{}, Full)
179+
assert.Nil(t, root.GetOrCreateRegistry("a.b.c.scalar.w.x"), "GetOrCreateRegistry should return nil if part of the path is a non-registry type")
180+
}

0 commit comments

Comments
 (0)