Skip to content
This repository was archived by the owner on Jul 18, 2025. It is now read-only.

Commit 1889c7c

Browse files
committed
Add Close() method to Project to release resources
Problem Observed Goroutine leaks because there is no way to "close" projects Problem Detail NewProject() creates a defaultListener in it. The listener starts a new goroutine in NewDefaultListener(). We currently don't have a method to stop this goroutine. Suggested Resolution This commit adds a new method Close() to release resources tied to a Project. Signed-off-by: Iwasaki Yudai <[email protected]>
1 parent 8e4221d commit 1889c7c

File tree

3 files changed

+34
-18
lines changed

3 files changed

+34
-18
lines changed

project/listener.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,13 @@ type defaultListener struct {
3939
}
4040

4141
// NewDefaultListener create a default listener for the specified project.
42-
func NewDefaultListener(p *Project) chan<- events.Event {
43-
l := defaultListener{
42+
func NewDefaultListener(p *Project) *defaultListener {
43+
l := &defaultListener{
4444
listenChan: make(chan events.Event),
4545
project: p,
4646
}
4747
go l.start()
48-
return l.listenChan
48+
return l
4949
}
5050

5151
func (d *defaultListener) start() {
@@ -79,3 +79,7 @@ func (d *defaultListener) start() {
7979
}
8080
}
8181
}
82+
83+
func (d *defaultListener) close() {
84+
close(d.listenChan)
85+
}

project/project.go

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,15 @@ type Project struct {
3535
ReloadCallback func() error
3636
ParseOptions *config.ParseOptions
3737

38-
runtime RuntimeProject
39-
networks Networks
40-
volumes Volumes
41-
configVersion string
42-
context *Context
43-
reload []string
44-
upCount int
45-
listeners []chan<- events.Event
46-
hasListeners bool
38+
runtime RuntimeProject
39+
networks Networks
40+
volumes Volumes
41+
configVersion string
42+
context *Context
43+
reload []string
44+
upCount int
45+
listeners []chan<- events.Event
46+
defaultListener *defaultListener
4747
}
4848

4949
// NewProject creates a new project with the specified context.
@@ -93,11 +93,18 @@ func NewProject(context *Context, runtime RuntimeProject, parseOptions *config.P
9393

9494
context.Project = p
9595

96-
p.listeners = []chan<- events.Event{NewDefaultListener(p)}
96+
p.defaultListener = NewDefaultListener(p)
97+
p.listeners = []chan<- events.Event{p.defaultListener.listenChan}
9798

9899
return p
99100
}
100101

102+
// Close releases resources attached to the project
103+
func (p *Project) Close() error {
104+
p.defaultListener.close()
105+
return nil
106+
}
107+
101108
// Parse populates project information based on its context. It sets up the name,
102109
// the composefile and the composebytes (the composefile content).
103110
func (p *Project) Parse() error {
@@ -511,11 +518,7 @@ func (p *Project) traverse(start bool, selected map[string]bool, wrappers map[st
511518
// AddListener adds the specified listener to the project.
512519
// This implements implicitly events.Emitter.
513520
func (p *Project) AddListener(c chan<- events.Event) {
514-
if !p.hasListeners {
515-
for _, l := range p.listeners {
516-
close(l)
517-
}
518-
p.hasListeners = true
521+
if len(p.listeners) == 1 && p.listeners[0] == p.defaultListener.listenChan {
519522
p.listeners = []chan<- events.Event{c}
520523
} else {
521524
p.listeners = append(p.listeners, c)

project/project_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ func TestTwoCall(t *testing.T) {
6464
p := NewProject(&Context{
6565
ServiceFactory: factory,
6666
}, nil, nil)
67+
defer p.Close()
6768
p.ServiceConfigs = config.NewServiceConfigs()
6869
p.ServiceConfigs.Add("foo", &config.ServiceConfig{})
6970

@@ -83,6 +84,7 @@ func TestTwoCall(t *testing.T) {
8384
func TestGetServiceConfig(t *testing.T) {
8485

8586
p := NewProject(&Context{}, nil, nil)
87+
defer p.Close()
8688
p.ServiceConfigs = config.NewServiceConfigs()
8789
fooService := &config.ServiceConfig{}
8890
p.ServiceConfigs.Add("foo", fooService)
@@ -112,6 +114,7 @@ func TestParseWithBadContent(t *testing.T) {
112114
[]byte("garbage"),
113115
},
114116
}, nil, nil)
117+
defer p.Close()
115118

116119
err := p.Parse()
117120
if err == nil {
@@ -129,6 +132,7 @@ func TestParseWithGoodContent(t *testing.T) {
129132
[]byte("not-garbage:\n image: foo"),
130133
},
131134
}, nil, nil)
135+
defer p.Close()
132136

133137
err := p.Parse()
134138
if err != nil {
@@ -142,6 +146,7 @@ func TestParseWithDefaultEnvironmentLookup(t *testing.T) {
142146
[]byte("not-garbage:\n image: foo:${version}"),
143147
},
144148
}, nil, nil)
149+
defer p.Close()
145150

146151
err := p.Parse()
147152
if err != nil {
@@ -165,6 +170,7 @@ func TestEnvironmentResolve(t *testing.T) {
165170
ServiceFactory: factory,
166171
EnvironmentLookup: &TestEnvironmentLookup{},
167172
}, nil, nil)
173+
defer p.Close()
168174
p.ServiceConfigs = config.NewServiceConfigs()
169175
p.ServiceConfigs.Add("foo", &config.ServiceConfig{
170176
Environment: yaml.MaporEqualSlice([]string{
@@ -209,6 +215,7 @@ func TestParseWithMultipleComposeFiles(t *testing.T) {
209215
p := NewProject(&Context{
210216
ComposeBytes: [][]byte{configOne, configTwo},
211217
}, nil, nil)
218+
defer p.Close()
212219

213220
err := p.Parse()
214221

@@ -222,6 +229,7 @@ func TestParseWithMultipleComposeFiles(t *testing.T) {
222229
p = NewProject(&Context{
223230
ComposeBytes: [][]byte{configTwo, configOne},
224231
}, nil, nil)
232+
defer p.Close()
225233

226234
err = p.Parse()
227235

@@ -235,6 +243,7 @@ func TestParseWithMultipleComposeFiles(t *testing.T) {
235243
p = NewProject(&Context{
236244
ComposeBytes: [][]byte{configOne, configTwo, configThree},
237245
}, nil, nil)
246+
defer p.Close()
238247

239248
err = p.Parse()
240249

0 commit comments

Comments
 (0)