Skip to content

Commit

Permalink
debounce refresh requests with quietperiod
Browse files Browse the repository at this point in the history
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
  • Loading branch information
ndeloof committed Jan 18, 2023
1 parent 5a2b7b8 commit 4137940
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 0 deletions.
63 changes: 63 additions & 0 deletions pkg/compose/watch.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ import (
"context"
"fmt"
"log"
"strings"
"time"

"github.com/compose-spec/compose-go/types"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/utils"
"github.com/fsnotify/fsnotify"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
Expand All @@ -30,10 +33,20 @@ import (
type DevelopmentConfig struct {
}

const quietPeriod = 2 * time.Second

func (s *composeService) Watch(ctx context.Context, project *types.Project, services []string, options api.WatchOptions) error {
fmt.Fprintln(s.stderr(), "not implemented yet")

eg, ctx := errgroup.WithContext(ctx)
needRefresh := make(chan string)
eg.Go(func() error {
debounce(ctx, newTimer(quietPeriod), needRefresh, func(services []string) {
fmt.Fprintf(s.stderr(), "Updating %s after changes were detected\n", strings.Join(services, ", "))
})
return nil
})

err := project.WithServices(services, func(service types.ServiceConfig) error {
var config DevelopmentConfig
if y, ok := service.Extensions["x-develop"]; ok {
Expand Down Expand Up @@ -64,6 +77,7 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
return nil
case event := <-watcher.Events:
log.Println("fs event :", event.String())
needRefresh <- service.Name
case err := <-watcher.Errors:
return err
}
Expand All @@ -77,3 +91,52 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv

return eg.Wait()
}

func debounce(ctx context.Context, timer timer, input chan string, fn func(services []string)) {
services := utils.Set[string]{}
for {
select {
case service := <-input:
timer.reset()
services.Add(service)
case <-timer.time():
if len(services) > 0 {
refresh := services.Elements()
services.Clear()
fn(refresh)
}
case <-ctx.Done():
return
}
}
}

// timer encapsulate the time.Timer API so we can control actual time in tests
type timer interface {
reset()
time() <-chan time.Time
}

// timeTimer implements time interface using golang's time.Timer
type timeTimer struct {
interval time.Duration
t *time.Timer
}

func newTimer(interval time.Duration) *timeTimer {
return &timeTimer{
interval: interval,
t: time.NewTimer(interval),
}
}

func (t *timeTimer) reset() {
if !t.t.Stop() {
<-t.t.C
}
t.t.Reset(t.interval)
}

func (t *timeTimer) time() <-chan time.Time {
return t.t.C
}
68 changes: 68 additions & 0 deletions pkg/compose/watch_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package compose

import (
"context"
"fmt"
"testing"
"time"

"golang.org/x/sync/errgroup"
"gotest.tools/v3/assert"
)

// testTimer replaces a timer without requirement for controlled timing events
type testTimer struct {
ch chan time.Time
}

func (t testTimer) reset() {
// noop
}

func (t testTimer) time() <-chan time.Time {
return t.ch
}

func Test_debounce(t *testing.T) {
ch := make(chan string)
var (
ran int
got []string
)
ctx, stop := context.WithTimeout(context.TODO(), 500*time.Millisecond)
defer stop()

timeC := make(chan time.Time)
timer := testTimer{ch: timeC}
eg, ctx := errgroup.WithContext(ctx)
eg.Go(func() error {
debounce(ctx, timer, ch, func(services []string) {
got = append(got, services...)
ran++
stop()
})
return nil
})
for i := 0; i < 100; i++ {
ch <- fmt.Sprintf("test-%d", i)
}
timeC <- time.Now()
err := eg.Wait()
assert.NilError(t, err)
assert.Equal(t, ran, 1)
assert.Equal(t, len(got), 100)
}
39 changes: 39 additions & 0 deletions pkg/utils/set.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package utils

type Set[T comparable] map[T]struct{}

func (s Set[T]) Add(v T) {
s[v] = struct{}{}
}

func (s Set[T]) Remove(v T) {
delete(s, v)
}

func (s Set[T]) Clear() {
for v := range s {
delete(s, v)
}
}

func (s Set[T]) Elements() []T {
elements := make([]T, 0, len(s))
for v := range s {
elements = append(elements, v)
}
return elements
}

0 comments on commit 4137940

Please sign in to comment.