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

Commit a9fa9ac

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 a9fa9ac

File tree

4 files changed

+46
-27
lines changed

4 files changed

+46
-27
lines changed

project/interface.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ type APIProject interface {
3939
AddConfig(name string, config *config.ServiceConfig) error
4040
Load(bytes []byte) error
4141
Containers(ctx context.Context, filter Filter, services ...string) ([]string, error)
42+
Close() error
4243

4344
GetServiceConfig(service string) (*config.ServiceConfig, bool)
4445
}

project/listener.go

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,24 +32,26 @@ var (
3232
}
3333
)
3434

35-
type defaultListener struct {
36-
project *Project
37-
listenChan chan events.Event
38-
upCount int
35+
// DefaultListerner is a listener with the default logger
36+
type DefaultListener struct {
37+
C chan events.Event
38+
39+
project *Project
40+
upCount int
3941
}
4042

4143
// NewDefaultListener create a default listener for the specified project.
42-
func NewDefaultListener(p *Project) chan<- events.Event {
43-
l := defaultListener{
44-
listenChan: make(chan events.Event),
45-
project: p,
44+
func NewDefaultListener(p *Project) *DefaultListener {
45+
l := &DefaultListener{
46+
C: make(chan events.Event),
47+
project: p,
4648
}
47-
go l.start()
48-
return l.listenChan
49+
go l.Start()
50+
return l
4951
}
5052

51-
func (d *defaultListener) start() {
52-
for event := range d.listenChan {
53+
func (d *DefaultListener) Start() {
54+
for event := range d.C {
5355
buffer := bytes.NewBuffer(nil)
5456
if event.Data != nil {
5557
for k, v := range event.Data {
@@ -79,3 +81,7 @@ func (d *defaultListener) start() {
7981
}
8082
}
8183
}
84+
85+
func (d *DefaultListener) Close() {
86+
close(d.C)
87+
}

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.C}
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.C {
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)