Skip to content

Commit 82dbd66

Browse files
authored
Merge pull request #3 from MaurUppi/codex/review-dns_improvement_plan_v3.md
dns: fix DoUDP context/conn handling and add forwarder cache eviction
2 parents 1b91f41 + 39998d8 commit 82dbd66

File tree

5 files changed

+163
-8
lines changed

5 files changed

+163
-8
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# dae DNS 改进计划 v3 - 开发执行文档
2+
3+
> 基于 `.plan/dns_improvement_plan_v3.md` 复核代码后,保持 v3 原始优先级与技术路线不变;本文件仅做可执行拆解与验收约束。
4+
5+
## 0. 执行总则(强制)
6+
1. 严格串行:Tn 未通过任务级测试,不进入 Tn+1。
7+
2. 每个任务都必须包含:代码实现 + 任务级测试 + 测试记录。
8+
3. 每个里程碑在全部任务完成后,执行一次回归测试。
9+
4. 任一测试失败,先修复并重测,直至通过。
10+
11+
## 1. 任务分解
12+
13+
### T1(P0-1)修复 DoUDP context 传播与连接关闭一致性
14+
- 变更文件:`control/dns.go`
15+
- 变更点:
16+
- `DoUDP.ForwardDNS``context.WithTimeout(context.TODO(), timeout)` 改为基于入参 `ctx`
17+
- 读写统一使用 `d.conn`,确保与 `Close()` 生命周期一致。
18+
- 验收:关键代码路径命中检查通过。
19+
20+
### T2(P0-2)dialSend 失败路径 timeout 反馈闭环复核
21+
- 变更文件:`control/dns_control.go`
22+
- 变更点:
23+
- 保持 `isTimeoutError` + `timeoutExceedCallback` 在主路径与 fallback 路径生效。
24+
- 保持 dead code 不回归。
25+
- 验收:单元测试 `TestIsTimeoutError*` 与关键路径扫描通过。
26+
27+
### T3(P1-1/P1-2)统一 HTTP/Stream context+deadline 语义复核
28+
- 变更文件:`control/dns.go`
29+
- 变更点:
30+
- 保持 `sendHttpDNS` 使用 `http.NewRequestWithContext`
31+
- 保持 `sendStreamDNS` 基于 `ctx` 设置 deadline 并在 I/O 前后检查 `ctx.Err()`
32+
- 验收:单元测试 `TestSendStreamDNSRespectsContextCancelBeforeIO` 与代码扫描通过。
33+
34+
### T4(P1-3)`tcp+udp` 同查询 fallback 复核
35+
- 变更文件:`control/dns_control.go`
36+
- 变更点:
37+
- 保持 `tcpFallbackDialArgument` 仅在 `tcp+udp + UDP + timeout` 触发。
38+
- fallback 只执行一次,避免放大。
39+
- 验收:单元测试 `TestTcpFallbackDialArgument` 与关键路径扫描通过。
40+
41+
### T5(P2-4)`ipversion_prefer` 改为“优先+条件补查”复核
42+
- 变更文件:`control/dns_control.go`
43+
- 变更点:
44+
- 保持 `Handle_` 先查首选族;仅在首查无有效 IP 时补查另一族。
45+
- 验收:控制流扫描通过(不回退到固定并发双查)。
46+
47+
### T6(P2-5)`dnsForwarderCache` 增加淘汰策略
48+
- 变更文件:`control/dns_control.go`
49+
- 变更点:
50+
- 新增 forwarder cache 上限(LRU 近似:按 last-use 时间淘汰最旧项)。
51+
- 淘汰时关闭被移除 forwarder,防止资源悬挂。
52+
- 验收:新增单元测试覆盖淘汰行为并通过。
53+
54+
## 2. 里程碑回归
55+
- 命令:
56+
- `go test ./control -run 'Test(IsTimeoutError|TcpFallbackDialArgument|SendStreamDNSRespectsContextCancelBeforeIO|EvictDnsForwarderCacheOneLocked)' -count=1`
57+
- `go test ./control -run TestIsTimeoutErrorWrappedDeadline -count=1`
58+
- 通过标准:全部 PASS。
59+
- 若受环境限制(依赖下载等)无法执行,需在 `.plan/test-log.md` 明确记录命令、失败原因和影响范围。

.plan/test-log.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,41 @@
2929
- 命令:`go test ./control -run 'Test(IsTimeoutError|TcpFallbackDialArgument|SendStreamDNSRespectsContextCancelBeforeIO)' -count=1`
3030
- 结果:失败,原因是环境无法从 `proxy.golang.org` 拉取依赖(`github.com/daeuniverse/outbound` 403 Forbidden)。
3131
- 结论:受环境限制,未完成自动化回归;本轮以静态实现校验作为替代。
32+
33+
## T1(DoUDP context 传播与连接一致性)
34+
- 命令:`rg -n "context.WithTimeout\(ctx, timeout\)|d\.conn\.Write\(|d\.conn\.Read\(" control/dns.go`
35+
- 结果:命中 `DoUDP.ForwardDNS``context.WithTimeout(ctx, timeout)`,以及统一 `d.conn` 读写。
36+
- 结论:通过(父级 context 可传播,连接生命周期与 `Close()` 一致)。
37+
38+
## T2(dialSend timeout 反馈闭环复核)
39+
- 命令:`go test ./control -run 'TestIsTimeoutError|TestIsTimeoutErrorWrappedDeadline' -count=1`
40+
- 结果:失败(环境限制),`proxy.golang.org` 拉取 `github.com/daeuniverse/outbound` 返回 403 Forbidden。
41+
- 结论:自动化单测受限,改用静态路径校验。
42+
- 命令:`rg -n "timeoutExceedCallback\(dialArgument|timeoutExceedCallback\(fallbackDialArgument|func isTimeoutError" control/dns_control.go`
43+
- 结果:命中主路径 + fallback 路径 timeout 回调与超时识别函数。
44+
- 结论:通过(失败路径健康反馈未回归)。
45+
46+
## T3(HTTP/Stream context+deadline 语义复核)
47+
- 命令:`rg -n "NewRequestWithContext|func sendStreamDNS\(ctx|ctx\.Err\(\)|SetDeadline" control/dns.go`
48+
- 结果:命中 `NewRequestWithContext``sendStreamDNS(ctx,...)``SetDeadline` 与多处 `ctx.Err()` 检查。
49+
- 结论:通过(取消/超时语义可传递到 I/O 层)。
50+
51+
## T4(tcp+udp 同查询 fallback 复核)
52+
- 命令:`rg -n "func tcpFallbackDialArgument|upstream\.Scheme != dns\.UpstreamScheme_TCP_UDP|dialArgument\.l4proto != consts\.L4ProtoStr_UDP|!isTimeoutError\(err\)" control/dns_control.go`
53+
- 结果:命中 fallback 触发条件约束(仅 tcp+udp + UDP + timeout)。
54+
- 结论:通过(一次性 fallback 约束保持有效)。
55+
56+
## T5(ipversion_prefer 优先+条件补查复核)
57+
- 命令:`rg -n "Query preferred qtype first|cache2 == nil \|\| !cache2\.IncludeAnyIp\(\)|handle_\(dnsMessage2, req, false\)" control/dns_control.go`
58+
- 结果:命中“先查首选,再在无有效 IP 时补查另一族”的控制流。
59+
- 结论:通过(未回退到固定并发双查)。
60+
61+
## T6(dnsForwarderCache 淘汰策略)
62+
- 命令:`rg -n "maxDnsForwarderCacheSize|dnsForwarderLastUse|evictDnsForwarderCacheOneLocked|delete\(c\.dnsForwarderCache" control/dns_control.go`
63+
- 结果:命中缓存上限、last-use 记录、最旧项淘汰及删除逻辑。
64+
- 结论:通过(缓存具备容量上限和回收路径)。
65+
66+
## 里程碑回归(v3)
67+
- 命令:`go test ./control -run 'Test(IsTimeoutError|TcpFallbackDialArgument|SendStreamDNSRespectsContextCancelBeforeIO|EvictDnsForwarderCacheOneLocked)' -count=1`
68+
- 结果:失败(环境限制),`proxy.golang.org` 拉取私有/受限依赖 `github.com/daeuniverse/outbound` 返回 403 Forbidden。
69+
- 结论:在当前环境无法完成自动化回归编译;已保留任务级静态校验记录。

control/dns.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -316,14 +316,14 @@ func (d *DoUDP) ForwardDNS(ctx context.Context, data []byte) (*dnsmessage.Msg, e
316316
d.conn = conn
317317

318318
timeout := 5 * time.Second
319-
_ = conn.SetDeadline(time.Now().Add(timeout))
320-
dnsReqCtx, cancelDnsReqCtx := context.WithTimeout(context.TODO(), timeout)
319+
_ = d.conn.SetDeadline(time.Now().Add(timeout))
320+
dnsReqCtx, cancelDnsReqCtx := context.WithTimeout(ctx, timeout)
321321
defer cancelDnsReqCtx()
322322

323323
go func() {
324324
// Send DNS request every seconds.
325325
for {
326-
_, _ = conn.Write(data)
326+
_, _ = d.conn.Write(data)
327327
// if err != nil {
328328
// if c.log.IsLevelEnabled(logrus.DebugLevel) {
329329
// c.log.WithFields(logrus.Fields{
@@ -350,7 +350,7 @@ func (d *DoUDP) ForwardDNS(ctx context.Context, data []byte) (*dnsmessage.Msg, e
350350
respBuf := pool.GetFullCap(consts.EthernetMtu)
351351
defer pool.Put(respBuf)
352352
// Wait for response.
353-
n, err := conn.Read(respBuf)
353+
n, err := d.conn.Read(respBuf)
354354
if err != nil {
355355
return nil, err
356356
}

control/dns_control.go

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ import (
3030
)
3131

3232
const (
33-
MaxDnsLookupDepth = 3
34-
minFirefoxCacheTtl = 120
33+
MaxDnsLookupDepth = 3
34+
minFirefoxCacheTtl = 120
35+
maxDnsForwarderCacheSize = 128
3536
)
3637

3738
type IpVersionPrefer int
@@ -82,6 +83,7 @@ type DnsController struct {
8283
dnsCache map[string]*DnsCache
8384
dnsForwarderCacheMu sync.Mutex
8485
dnsForwarderCache map[dnsForwarderKey]DnsForwarder
86+
dnsForwarderLastUse map[dnsForwarderKey]time.Time
8587
}
8688

8789
type handlingState struct {
@@ -125,9 +127,36 @@ func NewDnsController(routing *dns.Dns, option *DnsControllerOption) (c *DnsCont
125127
dnsCache: make(map[string]*DnsCache),
126128
dnsForwarderCacheMu: sync.Mutex{},
127129
dnsForwarderCache: make(map[dnsForwarderKey]DnsForwarder),
130+
dnsForwarderLastUse: make(map[dnsForwarderKey]time.Time),
128131
}, nil
129132
}
130133

134+
func (c *DnsController) evictDnsForwarderCacheOneLocked() {
135+
if len(c.dnsForwarderCache) < maxDnsForwarderCacheSize {
136+
return
137+
}
138+
var (
139+
oldestKey dnsForwarderKey
140+
oldestTime time.Time
141+
initialized bool
142+
)
143+
for key, lastUse := range c.dnsForwarderLastUse {
144+
if !initialized || lastUse.Before(oldestTime) {
145+
oldestKey = key
146+
oldestTime = lastUse
147+
initialized = true
148+
}
149+
}
150+
if !initialized {
151+
return
152+
}
153+
if forwarder, ok := c.dnsForwarderCache[oldestKey]; ok {
154+
_ = forwarder.Close()
155+
delete(c.dnsForwarderCache, oldestKey)
156+
}
157+
delete(c.dnsForwarderLastUse, oldestKey)
158+
}
159+
131160
func (c *DnsController) cacheKey(qname string, qtype uint16) string {
132161
// To fqdn.
133162
return dnsmessage.CanonicalName(qname) + strconv.Itoa(int(qtype))
@@ -582,16 +611,19 @@ func (c *DnsController) dialSend(invokingDepth int, req *udpRequest, data []byte
582611
defer cancel()
583612

584613
// get forwarder from cache
614+
cacheKeyForwarder := dnsForwarderKey{upstream: upstream.String(), dialArgument: *dialArgument}
585615
c.dnsForwarderCacheMu.Lock()
586-
forwarder, ok := c.dnsForwarderCache[dnsForwarderKey{upstream: upstream.String(), dialArgument: *dialArgument}]
616+
forwarder, ok := c.dnsForwarderCache[cacheKeyForwarder]
587617
if !ok {
618+
c.evictDnsForwarderCacheOneLocked()
588619
forwarder, err = newDnsForwarder(upstream, *dialArgument)
589620
if err != nil {
590621
c.dnsForwarderCacheMu.Unlock()
591622
return err
592623
}
593-
c.dnsForwarderCache[dnsForwarderKey{upstream: upstream.String(), dialArgument: *dialArgument}] = forwarder
624+
c.dnsForwarderCache[cacheKeyForwarder] = forwarder
594625
}
626+
c.dnsForwarderLastUse[cacheKeyForwarder] = time.Now()
595627
c.dnsForwarderCacheMu.Unlock()
596628

597629
defer func() {

control/dns_improvement_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import (
44
"context"
55
"errors"
66
"net"
7+
"strconv"
78
"testing"
89
"time"
910

1011
"github.com/daeuniverse/dae/common/consts"
1112
"github.com/daeuniverse/dae/component/dns"
13+
dnsmessage "github.com/miekg/dns"
1214
)
1315

1416
type timeoutNetErr struct{}
@@ -98,3 +100,27 @@ func TestIsTimeoutErrorWrappedDeadline(t *testing.T) {
98100
t.Fatal("expected wrapped deadline to be detected as timeout")
99101
}
100102
}
103+
104+
func TestEvictDnsForwarderCacheOneLocked(t *testing.T) {
105+
c := &DnsController{
106+
dnsForwarderCache: make(map[dnsForwarderKey]DnsForwarder),
107+
dnsForwarderLastUse: make(map[dnsForwarderKey]time.Time),
108+
}
109+
for i := 0; i < maxDnsForwarderCacheSize; i++ {
110+
key := dnsForwarderKey{upstream: "u" + strconv.Itoa(i), dialArgument: dialArgument{l4proto: consts.L4ProtoStr_UDP}}
111+
c.dnsForwarderCache[key] = fakeDnsForwarder{}
112+
c.dnsForwarderLastUse[key] = time.Unix(int64(i), 0)
113+
}
114+
c.evictDnsForwarderCacheOneLocked()
115+
if len(c.dnsForwarderCache) != maxDnsForwarderCacheSize-1 {
116+
t.Fatalf("cache size = %d, want %d", len(c.dnsForwarderCache), maxDnsForwarderCacheSize-1)
117+
}
118+
if len(c.dnsForwarderLastUse) != maxDnsForwarderCacheSize-1 {
119+
t.Fatalf("lastUse size = %d, want %d", len(c.dnsForwarderLastUse), maxDnsForwarderCacheSize-1)
120+
}
121+
}
122+
123+
type fakeDnsForwarder struct{}
124+
125+
func (fakeDnsForwarder) ForwardDNS(context.Context, []byte) (*dnsmessage.Msg, error) { return nil, nil }
126+
func (fakeDnsForwarder) Close() error { return nil }

0 commit comments

Comments
 (0)