Skip to content

Commit c15bf19

Browse files
committed
debounce refresh requests with quietperiod
Signed-off-by: Nicolas De Loof <[email protected]>
1 parent 0b1c867 commit c15bf19

File tree

6 files changed

+163
-4
lines changed

6 files changed

+163
-4
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ require (
8181
github.com/imdario/mergo v0.3.13 // indirect
8282
github.com/inconshreveable/mousetrap v1.0.1 // indirect
8383
github.com/jinzhu/gorm v1.9.11 // indirect
84+
github.com/jonboulle/clockwork v0.3.1-0.20230117163003-a89700cec744
8485
github.com/json-iterator/go v1.1.12 // indirect
8586
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
8687
github.com/klauspost/compress v1.15.9 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,8 @@ github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/
392392
github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548/go.mod h1:hGT6jSUVzF6no3QaDSMLGLEHtHSBSefs+MgcDWnmhmo=
393393
github.com/jmoiron/sqlx v0.0.0-20180124204410-05cef0741ade/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU=
394394
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
395+
github.com/jonboulle/clockwork v0.3.1-0.20230117163003-a89700cec744 h1:fJ+REXDOpsMqA2spt3wAq3HGJJvWnNitGK2KVZTos+8=
396+
github.com/jonboulle/clockwork v0.3.1-0.20230117163003-a89700cec744/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
395397
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
396398
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
397399
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=

pkg/compose/build.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,16 @@ import (
4444

4545
func (s *composeService) Build(ctx context.Context, project *types.Project, options api.BuildOptions) error {
4646
return progress.Run(ctx, func(ctx context.Context) error {
47-
return s.build(ctx, project, options)
47+
_, err := s.build(ctx, project, options)
48+
return err
4849
})
4950
}
5051

51-
func (s *composeService) build(ctx context.Context, project *types.Project, options api.BuildOptions) error {
52+
func (s *composeService) build(ctx context.Context, project *types.Project, options api.BuildOptions) (map[string]string, error) {
5253
args := flatten(options.Args.Resolve(envResolver(project.Environment)))
5354

54-
return InDependencyOrder(ctx, project, func(ctx context.Context, name string) error {
55+
var imageIDs map[string]string
56+
err := InDependencyOrder(ctx, project, func(ctx context.Context, name string) error {
5557
if len(options.Services) > 0 && !utils.Contains(options.Services, name) {
5658
return nil
5759
}
@@ -93,11 +95,12 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
9395
}}
9496
}
9597
opts := map[string]build.Options{imageName: buildOptions}
96-
_, err = s.doBuild(ctx, project, opts, options.Progress)
98+
imageIDs, err = s.doBuild(ctx, project, opts, options.Progress)
9799
return err
98100
}, func(traversal *graphTraversal) {
99101
traversal.maxConcurrency = s.maxConcurrency
100102
})
103+
return imageIDs, err
101104
}
102105

103106
func (s *composeService) ensureImagesExists(ctx context.Context, project *types.Project, quietPull bool) error {

pkg/compose/watch.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,14 @@ import (
1818
"context"
1919
"fmt"
2020
"log"
21+
"strings"
22+
"time"
2123

2224
"github.com/compose-spec/compose-go/types"
2325
"github.com/docker/compose/v2/pkg/api"
26+
"github.com/docker/compose/v2/pkg/utils"
2427
"github.com/fsnotify/fsnotify"
28+
"github.com/jonboulle/clockwork"
2529
"github.com/mitchellh/mapstructure"
2630
"github.com/pkg/errors"
2731
"golang.org/x/sync/errgroup"
@@ -30,10 +34,47 @@ import (
3034
type DevelopmentConfig struct {
3135
}
3236

37+
const quietPeriod = 2 * time.Second
38+
3339
func (s *composeService) Watch(ctx context.Context, project *types.Project, services []string, options api.WatchOptions) error {
3440
fmt.Fprintln(s.stderr(), "not implemented yet")
3541

3642
eg, ctx := errgroup.WithContext(ctx)
43+
needRefresh := make(chan string)
44+
eg.Go(func() error {
45+
clock := clockwork.NewRealClock()
46+
debounce(ctx, clock, quietPeriod, needRefresh, func(services []string) {
47+
fmt.Fprintf(s.stderr(), "Updating %s after changes were detected\n", strings.Join(services, ", "))
48+
imageIds, err := s.build(ctx, project, api.BuildOptions{
49+
Services: services,
50+
})
51+
if err != nil {
52+
fmt.Fprintf(s.stderr(), "Build failed")
53+
}
54+
for i, service := range project.Services {
55+
if id, ok := imageIds[service.Name]; ok {
56+
service.Image = id
57+
}
58+
project.Services[i] = service
59+
}
60+
61+
err = s.Up(ctx, project, api.UpOptions{
62+
Create: api.CreateOptions{
63+
Services: services,
64+
Inherit: true,
65+
},
66+
Start: api.StartOptions{
67+
Services: services,
68+
Project: project,
69+
},
70+
})
71+
if err != nil {
72+
fmt.Fprintf(s.stderr(), "Application failed to start after update")
73+
}
74+
})
75+
return nil
76+
})
77+
3778
err := project.WithServices(services, func(service types.ServiceConfig) error {
3879
var config DevelopmentConfig
3980
if y, ok := service.Extensions["x-develop"]; ok {
@@ -64,6 +105,7 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
64105
return nil
65106
case event := <-watcher.Events:
66107
log.Println("fs event :", event.String())
108+
needRefresh <- service.Name
67109
case err := <-watcher.Errors:
68110
return err
69111
}
@@ -77,3 +119,23 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
77119

78120
return eg.Wait()
79121
}
122+
123+
func debounce(ctx context.Context, clock clockwork.Clock, delay time.Duration, input chan string, fn func(services []string)) {
124+
services := utils.Set[string]{}
125+
t := clock.AfterFunc(delay, func() {
126+
if len(services) > 0 {
127+
refresh := services.Elements()
128+
services.Clear()
129+
fn(refresh)
130+
}
131+
})
132+
for {
133+
select {
134+
case <-ctx.Done():
135+
return
136+
case service := <-input:
137+
t.Reset(delay)
138+
services.Add(service)
139+
}
140+
}
141+
}

pkg/compose/watch_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
3+
Copyright 2020 Docker Compose CLI authors
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
Unless required by applicable law or agreed to in writing, software
9+
distributed under the License is distributed on an "AS IS" BASIS,
10+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
See the License for the specific language governing permissions and
12+
limitations under the License.
13+
*/
14+
15+
package compose
16+
17+
import (
18+
"context"
19+
"testing"
20+
21+
"github.com/jonboulle/clockwork"
22+
"golang.org/x/sync/errgroup"
23+
"gotest.tools/v3/assert"
24+
)
25+
26+
func Test_debounce(t *testing.T) {
27+
ch := make(chan string)
28+
var (
29+
ran int
30+
got []string
31+
)
32+
clock := clockwork.NewFakeClock()
33+
ctx, stop := context.WithCancel(context.TODO())
34+
eg, ctx := errgroup.WithContext(ctx)
35+
eg.Go(func() error {
36+
debounce(ctx, clock, quietPeriod, ch, func(services []string) {
37+
got = append(got, services...)
38+
ran++
39+
stop()
40+
})
41+
return nil
42+
})
43+
for i := 0; i < 100; i++ {
44+
ch <- "test"
45+
}
46+
assert.Equal(t, ran, 0)
47+
clock.Advance(quietPeriod)
48+
err := eg.Wait()
49+
assert.NilError(t, err)
50+
assert.Equal(t, ran, 1)
51+
assert.DeepEqual(t, got, []string{"test"})
52+
}

pkg/utils/set.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
3+
Copyright 2020 Docker Compose CLI authors
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
Unless required by applicable law or agreed to in writing, software
9+
distributed under the License is distributed on an "AS IS" BASIS,
10+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
See the License for the specific language governing permissions and
12+
limitations under the License.
13+
*/
14+
15+
package utils
16+
17+
type Set[T comparable] map[T]struct{}
18+
19+
func (s Set[T]) Add(v T) {
20+
s[v] = struct{}{}
21+
}
22+
23+
func (s Set[T]) Remove(v T) {
24+
delete(s, v)
25+
}
26+
27+
func (s Set[T]) Clear() {
28+
for v := range s {
29+
delete(s, v)
30+
}
31+
}
32+
33+
func (s Set[T]) Elements() []T {
34+
elements := make([]T, 0, len(s))
35+
for v := range s {
36+
elements = append(elements, v)
37+
}
38+
return elements
39+
}

0 commit comments

Comments
 (0)