Skip to content

Commit bfd06eb

Browse files
committed
chore: rebuild SetupContextForConn with context.AfterFunc
1 parent e8af058 commit bfd06eb

File tree

5 files changed

+166
-18
lines changed

5 files changed

+166
-18
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package contextutils
2+
3+
import (
4+
"context"
5+
"sync"
6+
)
7+
8+
func afterFunc(ctx context.Context, f func()) (stop func() bool) {
9+
stopc := make(chan struct{})
10+
once := sync.Once{} // either starts running f or stops f from running
11+
if ctx.Done() != nil {
12+
go func() {
13+
select {
14+
case <-ctx.Done():
15+
once.Do(func() {
16+
go f()
17+
})
18+
case <-stopc:
19+
}
20+
}()
21+
}
22+
23+
return func() bool {
24+
stopped := false
25+
once.Do(func() {
26+
stopped = true
27+
close(stopc)
28+
})
29+
return stopped
30+
}
31+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//go:build !go1.21
2+
3+
package contextutils
4+
5+
import (
6+
"context"
7+
)
8+
9+
func AfterFunc(ctx context.Context, f func()) (stop func() bool) {
10+
return afterFunc(ctx, f)
11+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//go:build go1.21
2+
3+
package contextutils
4+
5+
import "context"
6+
7+
func AfterFunc(ctx context.Context, f func()) (stop func() bool) {
8+
return context.AfterFunc(ctx, f)
9+
}

common/contextutils/afterfunc_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package contextutils
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
)
8+
9+
const (
10+
shortDuration = 1 * time.Millisecond // a reasonable duration to block in a test
11+
veryLongDuration = 1000 * time.Hour // an arbitrary upper bound on the test's running time
12+
)
13+
14+
func TestAfterFuncCalledAfterCancel(t *testing.T) {
15+
ctx, cancel := context.WithCancel(context.Background())
16+
donec := make(chan struct{})
17+
stop := afterFunc(ctx, func() {
18+
close(donec)
19+
})
20+
select {
21+
case <-donec:
22+
t.Fatalf("AfterFunc called before context is done")
23+
case <-time.After(shortDuration):
24+
}
25+
cancel()
26+
select {
27+
case <-donec:
28+
case <-time.After(veryLongDuration):
29+
t.Fatalf("AfterFunc not called after context is canceled")
30+
}
31+
if stop() {
32+
t.Fatalf("stop() = true, want false")
33+
}
34+
}
35+
36+
func TestAfterFuncCalledAfterTimeout(t *testing.T) {
37+
ctx, cancel := context.WithTimeout(context.Background(), shortDuration)
38+
defer cancel()
39+
donec := make(chan struct{})
40+
afterFunc(ctx, func() {
41+
close(donec)
42+
})
43+
select {
44+
case <-donec:
45+
case <-time.After(veryLongDuration):
46+
t.Fatalf("AfterFunc not called after context is canceled")
47+
}
48+
}
49+
50+
func TestAfterFuncCalledImmediately(t *testing.T) {
51+
ctx, cancel := context.WithCancel(context.Background())
52+
cancel()
53+
donec := make(chan struct{})
54+
afterFunc(ctx, func() {
55+
close(donec)
56+
})
57+
select {
58+
case <-donec:
59+
case <-time.After(veryLongDuration):
60+
t.Fatalf("AfterFunc not called for already-canceled context")
61+
}
62+
}
63+
64+
func TestAfterFuncNotCalledAfterStop(t *testing.T) {
65+
ctx, cancel := context.WithCancel(context.Background())
66+
donec := make(chan struct{})
67+
stop := afterFunc(ctx, func() {
68+
close(donec)
69+
})
70+
if !stop() {
71+
t.Fatalf("stop() = false, want true")
72+
}
73+
cancel()
74+
select {
75+
case <-donec:
76+
t.Fatalf("AfterFunc called for already-canceled context")
77+
case <-time.After(shortDuration):
78+
}
79+
if stop() {
80+
t.Fatalf("stop() = true, want false")
81+
}
82+
}
83+
84+
// This test verifies that canceling a context does not block waiting for AfterFuncs to finish.
85+
func TestAfterFuncCalledAsynchronously(t *testing.T) {
86+
ctx, cancel := context.WithCancel(context.Background())
87+
donec := make(chan struct{})
88+
stop := afterFunc(ctx, func() {
89+
// The channel send blocks until donec is read from.
90+
donec <- struct{}{}
91+
})
92+
defer stop()
93+
cancel()
94+
// After cancel returns, read from donec and unblock the AfterFunc.
95+
select {
96+
case <-donec:
97+
case <-time.After(veryLongDuration):
98+
t.Fatalf("AfterFunc not called after context is canceled")
99+
}
100+
}

common/net/context.go

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,26 @@ package net
33
import (
44
"context"
55
"net"
6+
7+
"github.com/metacubex/mihomo/common/contextutils"
68
)
79

810
// SetupContextForConn is a helper function that starts connection I/O interrupter goroutine.
911
func SetupContextForConn(ctx context.Context, conn net.Conn) (done func(*error)) {
10-
var (
11-
quit = make(chan struct{})
12-
interrupt = make(chan error, 1)
13-
)
14-
go func() {
15-
select {
16-
case <-quit:
17-
interrupt <- nil
18-
case <-ctx.Done():
19-
// Close the connection, discarding the error
20-
_ = conn.Close()
21-
interrupt <- ctx.Err()
22-
}
23-
}()
12+
stopc := make(chan struct{})
13+
stop := contextutils.AfterFunc(ctx, func() {
14+
// Close the connection, discarding the error
15+
_ = conn.Close()
16+
close(stopc)
17+
})
2418
return func(inputErr *error) {
25-
close(quit)
26-
if ctxErr := <-interrupt; ctxErr != nil && inputErr != nil {
27-
// Return context error to user.
28-
inputErr = &ctxErr
19+
if !stop() {
20+
// The AfterFunc was started, wait for it to complete.
21+
<-stopc
22+
if ctxErr := ctx.Err(); ctxErr != nil && inputErr != nil {
23+
// Return context error to user.
24+
inputErr = &ctxErr
25+
}
2926
}
3027
}
3128
}

0 commit comments

Comments
 (0)