Skip to content

Commit a5e93e3

Browse files
committed
watchset: Settle time instead of timeout for Wait
Wait() should block until either context cancelled or channel(s) close. This way we can block until there is actually interesting work rather than just waiting for a timeout and returning empty set of closed channels. Signed-off-by: Jussi Maki <[email protected]>
1 parent d451c48 commit a5e93e3

File tree

2 files changed

+37
-22
lines changed

2 files changed

+37
-22
lines changed

watchset.go

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ package statedb
55

66
import (
77
"context"
8-
"fmt"
98
"maps"
109
"slices"
1110
"sync"
@@ -75,13 +74,12 @@ func (ws *WatchSet) Merge(other *WatchSet) {
7574
}
7675
}
7776

78-
// Wait for channels in the watch set to close until context is cancelled or timeout reached.
77+
// Wait for channels in the watch set to close or the context is cancelled.
78+
// After the first closed channel is seen Wait will wait [settleTime] for
79+
// more closed channels.
7980
// Returns the closed channels and removes them from the set.
80-
func (ws *WatchSet) Wait(ctx context.Context, timeout time.Duration) ([]<-chan struct{}, error) {
81-
if timeout <= 0 {
82-
return nil, fmt.Errorf("bad timeout %d, must be >0", timeout)
83-
}
84-
innerCtx, cancel := context.WithTimeout(ctx, timeout)
81+
func (ws *WatchSet) Wait(ctx context.Context, settleTime time.Duration) ([]<-chan struct{}, error) {
82+
innerCtx, cancel := context.WithCancel(ctx)
8583
defer cancel()
8684

8785
ws.mu.Lock()
@@ -91,7 +89,7 @@ func (ws *WatchSet) Wait(ctx context.Context, timeout time.Duration) ([]<-chan s
9189

9290
// No channels to watch? Just watch the context.
9391
if len(ws.chans) == 0 {
94-
<-innerCtx.Done()
92+
<-ctx.Done()
9593
return nil, ctx.Err()
9694
}
9795

@@ -101,30 +99,44 @@ func (ws *WatchSet) Wait(ctx context.Context, timeout time.Duration) ([]<-chan s
10199
chunkSize := 16
102100
roundedSize := len(chans) + (chunkSize - len(chans)%chunkSize)
103101
chans = slices.Grow(chans, roundedSize)[:roundedSize]
104-
105-
if len(ws.chans) <= chunkSize {
106-
watch16(closedChannels, innerCtx.Done(), chans)
107-
return closedChannels.chans, ctx.Err()
108-
}
102+
haveResult := make(chan struct{}, 1)
109103

110104
var wg sync.WaitGroup
111-
for chunk := range slices.Chunk(chans, chunkSize) {
105+
chunks := slices.Chunk(chans, chunkSize)
106+
for chunk := range chunks {
112107
wg.Add(1)
113108
go func() {
114109
defer wg.Done()
115-
watch16(closedChannels, innerCtx.Done(), chunk)
110+
watch16(haveResult, closedChannels, innerCtx.Done(), chunk)
116111
}()
117112
}
113+
114+
// Wait for the first closed channel to be seen. If [settleTime] is set,
115+
// then wait a bit longer for more.
116+
select {
117+
case <-haveResult:
118+
if settleTime > 0 {
119+
select {
120+
case <-time.After(settleTime):
121+
case <-ctx.Done():
122+
}
123+
}
124+
case <-ctx.Done():
125+
}
126+
127+
// Stop waiting for more channels to close
128+
cancel()
118129
wg.Wait()
119130

131+
// Remove the closed channels from the watch set.
120132
for _, ch := range closedChannels.chans {
121133
delete(ws.chans, ch)
122134
}
123135

124136
return closedChannels.chans, ctx.Err()
125137
}
126138

127-
func watch16(closedChannels *closedChannelsSlice, stop <-chan struct{}, chans []<-chan struct{}) {
139+
func watch16(haveClosed chan struct{}, closedChannels *closedChannelsSlice, stop <-chan struct{}, chans []<-chan struct{}) {
128140
for {
129141
closedIndex := -1
130142
select {
@@ -165,6 +177,13 @@ func watch16(closedChannels *closedChannelsSlice, stop <-chan struct{}, chans []
165177
}
166178
closedChannels.append(chans[closedIndex])
167179
chans[closedIndex] = nil
180+
if haveClosed != nil {
181+
select {
182+
case haveClosed <- struct{}{}:
183+
haveClosed = nil
184+
default:
185+
}
186+
}
168187
}
169188
}
170189

watchset_test.go

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ func TestWatchSet(t *testing.T) {
2020

2121
// Empty watch set, cancelled context.
2222
ctx, cancel := context.WithCancel(context.Background())
23-
go cancel()
23+
cancel()
2424
ch, err := ws.Wait(ctx, time.Second)
2525
require.ErrorIs(t, err, context.Canceled)
2626
require.Nil(t, ch)
@@ -31,7 +31,7 @@ func TestWatchSet(t *testing.T) {
3131
ch3 := make(chan struct{})
3232
ws.Add(ch1, ch2, ch3)
3333
ctx, cancel = context.WithCancel(context.Background())
34-
go cancel()
34+
cancel()
3535
ch, err = ws.Wait(ctx, time.Second)
3636
require.ErrorIs(t, err, context.Canceled)
3737
require.Nil(t, ch)
@@ -96,10 +96,6 @@ func TestWatchSetInQueries(t *testing.T) {
9696
_, _, watch2, _ := table.GetWatch(txn, idIndex.Query(2))
9797
_, _, watch3, _ := table.GetWatch(txn, idIndex.Query(3))
9898

99-
closed, err = ws.Wait(context.Background(), time.Millisecond)
100-
require.NoError(t, err)
101-
require.Empty(t, closed)
102-
10399
wtxn = db.WriteTxn(table)
104100
table.Insert(wtxn, testObject{ID: 1, Tags: part.NewSet("foo")})
105101
wtxn.Commit()

0 commit comments

Comments
 (0)