Skip to content

Commit e83835d

Browse files
jyxjjjcodexj2rong4cnPIKACHUIM
authored
fix(buffer): wait for active I/O before closing (#2825)
* fix(buffer): wait for active I/O before closing - Track in-flight PipeBuffer read and write operations - Wait for active I/O before releasing backing storage - Add concurrent close regression coverage for reads and writes Co-authored-by: Codex <267193182+codex@users.noreply.github.com> Signed-off-by: jyxjjj <16695261+jyxjjj@users.noreply.github.com> * ensure waitgroup handling --------- Signed-off-by: jyxjjj <16695261+jyxjjj@users.noreply.github.com> Co-authored-by: Codex <267193182+codex@users.noreply.github.com> Co-authored-by: j2rong4cn <j2rong@qq.com> Co-authored-by: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com>
1 parent 791a27e commit e83835d

2 files changed

Lines changed: 139 additions & 1 deletion

File tree

pkg/buffer/pipe.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type PipeBuffer struct {
1313
offR int
1414
offW int
1515
rw sync.Mutex
16+
ioWg sync.WaitGroup
1617
block Block
1718

1819
readSignal chan struct{}
@@ -73,6 +74,8 @@ func (br *PipeBuffer) Read(p []byte) (int, error) {
7374

7475
off := br.offR
7576
block := br.block
77+
br.ioWg.Add(1)
78+
defer br.ioWg.Done()
7679
br.rw.Unlock()
7780

7881
n, err := block.ReadAt(p[:min(len(p), canRead)], int64(off))
@@ -109,6 +112,8 @@ func (br *PipeBuffer) Write(p []byte) (int, error) {
109112

110113
off := br.offW
111114
block := br.block
115+
br.ioWg.Add(1)
116+
defer br.ioWg.Done()
112117
br.rw.Unlock()
113118

114119
n, err := block.WriteAt(p[:min(canWrite, len(p))], int64(off))
@@ -131,6 +136,7 @@ func (br *PipeBuffer) Write(p []byte) (int, error) {
131136
}
132137

133138
func (br *PipeBuffer) Reset(limit int) error {
139+
br.ioWg.Wait()
134140
br.rw.Lock()
135141
defer br.rw.Unlock()
136142
if br.block == nil {
@@ -147,11 +153,12 @@ func (br *PipeBuffer) Reset(limit int) error {
147153

148154
func (br *PipeBuffer) Close() error {
149155
br.rw.Lock()
150-
defer br.rw.Unlock()
151156
if br.block != nil {
152157
br.block = nil
153158
br.readPending = false
154159
close(br.readSignal)
155160
}
161+
br.rw.Unlock()
162+
br.ioWg.Wait()
156163
return nil
157164
}

pkg/buffer/pipe_test.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package buffer
2+
3+
import (
4+
"context"
5+
"errors"
6+
"io"
7+
"testing"
8+
"time"
9+
)
10+
11+
type blockingBlock struct {
12+
data []byte
13+
blockOn string
14+
started chan struct{}
15+
release chan struct{}
16+
}
17+
18+
func newBlockingBlock(blockOn string) *blockingBlock {
19+
return &blockingBlock{
20+
data: make([]byte, 1),
21+
blockOn: blockOn,
22+
started: make(chan struct{}),
23+
release: make(chan struct{}),
24+
}
25+
}
26+
27+
func (b *blockingBlock) Size() int64 {
28+
return int64(len(b.data))
29+
}
30+
31+
func (b *blockingBlock) ReadAt(p []byte, off int64) (int, error) {
32+
if b.blockOn == "read" {
33+
close(b.started)
34+
<-b.release
35+
}
36+
n := copy(p, b.data[off:])
37+
if n < len(p) {
38+
return n, io.EOF
39+
}
40+
return n, nil
41+
}
42+
43+
func (b *blockingBlock) WriteAt(p []byte, off int64) (int, error) {
44+
if b.blockOn == "write" {
45+
close(b.started)
46+
<-b.release
47+
}
48+
n := copy(b.data[off:], p)
49+
if n < len(p) {
50+
return n, io.ErrShortWrite
51+
}
52+
return n, nil
53+
}
54+
55+
func TestPipeBufferCloseWaitsForActiveIO(t *testing.T) {
56+
for _, operation := range []string{"read", "write"} {
57+
t.Run(operation, func(t *testing.T) {
58+
block := newBlockingBlock(operation)
59+
buf := NewPipeBuffer(context.Background(), block)
60+
if operation == "read" {
61+
if _, err := buf.Write([]byte{1}); err != nil {
62+
t.Fatalf("prepare read: %v", err)
63+
}
64+
}
65+
66+
ioDone := make(chan error, 1)
67+
go func() {
68+
var err error
69+
if operation == "read" {
70+
_, err = buf.Read(make([]byte, 1))
71+
} else {
72+
_, err = buf.Write([]byte{1})
73+
}
74+
ioDone <- err
75+
}()
76+
77+
select {
78+
case <-block.started:
79+
case <-time.After(time.Second):
80+
t.Fatal("I/O did not start")
81+
}
82+
83+
closeDone := make(chan error, 1)
84+
go func() {
85+
closeDone <- buf.Close()
86+
}()
87+
88+
deadline := time.Now().Add(time.Second)
89+
for {
90+
buf.rw.Lock()
91+
closed := buf.block == nil
92+
buf.rw.Unlock()
93+
if closed {
94+
break
95+
}
96+
if time.Now().After(deadline) {
97+
t.Fatal("buffer did not enter the closed state")
98+
}
99+
time.Sleep(time.Millisecond)
100+
}
101+
102+
select {
103+
case err := <-closeDone:
104+
t.Fatalf("Close returned before active %s completed: %v", operation, err)
105+
default:
106+
}
107+
108+
close(block.release)
109+
select {
110+
case err := <-ioDone:
111+
if err != nil {
112+
t.Fatalf("active %s failed: %v", operation, err)
113+
}
114+
case <-time.After(time.Second):
115+
t.Fatalf("active %s did not complete", operation)
116+
}
117+
select {
118+
case err := <-closeDone:
119+
if err != nil {
120+
t.Fatalf("Close failed: %v", err)
121+
}
122+
case <-time.After(time.Second):
123+
t.Fatal("Close did not wait for active I/O")
124+
}
125+
126+
if _, err := buf.Write([]byte{1}); !errors.Is(err, io.ErrClosedPipe) {
127+
t.Fatalf("write after Close error = %v, want %v", err, io.ErrClosedPipe)
128+
}
129+
})
130+
}
131+
}

0 commit comments

Comments
 (0)