DNS resolver LRU cache used []string slice for access ordering: - moveToFrontLocked: O(n) linear scan + slice重组 per cache update - storeCache held exclusive write lock during entire O(n) operation, blocking all concurrent reads Replace with container/list + map[string]*list.Element: - storeCache: O(1) MoveToFront via list.Element pointer - evictLRULocked: O(1) Back() + Remove() - DeleteCacheEntry: O(1) instead of O(n) scan This matches the LRU pattern already used by FileCache and FileInfoCache. Write lock duration reduced from O(n) to O(1), significantly reducing read contention under mixed workloads.
568 lines
13 KiB
Go
568 lines
13 KiB
Go
// Package resolver 提供 Mock DNS 测试。
|
||
//
|
||
// 该文件测试 DNS 解析器的核心功能:
|
||
// - queryDNS 成功/超时/失败
|
||
// - queryWithResolver 自定义服务器
|
||
// - refreshLoop 启动/停止
|
||
// - 缓存和 LRU 淘汰
|
||
//
|
||
// 作者:xfy
|
||
package resolver
|
||
|
||
import (
|
||
"context"
|
||
"sync"
|
||
"sync/atomic"
|
||
"testing"
|
||
"time"
|
||
|
||
"rua.plus/lolly/internal/config"
|
||
)
|
||
|
||
// TestMockDNSResolverDisabled 测试禁用状态。
|
||
func TestMockDNSResolverDisabled(t *testing.T) {
|
||
cfg := &config.ResolverConfig{
|
||
Enabled: false,
|
||
}
|
||
|
||
resolver := New(cfg)
|
||
if resolver == nil {
|
||
t.Fatal("New() returned nil")
|
||
}
|
||
|
||
// 禁用状态下应返回错误
|
||
_, err := resolver.LookupHost(context.Background(), "example.com")
|
||
if err == nil {
|
||
t.Error("Expected error for disabled resolver")
|
||
}
|
||
}
|
||
|
||
// TestMockDNSResolverEnabled 测试启用状态。
|
||
func TestMockDNSResolverEnabled(t *testing.T) {
|
||
cfg := &config.ResolverConfig{
|
||
Enabled: true,
|
||
Valid: 30 * time.Second,
|
||
Timeout: 5 * time.Second,
|
||
CacheSize: 100,
|
||
}
|
||
|
||
resolver := New(cfg)
|
||
if resolver == nil {
|
||
t.Fatal("New() returned nil")
|
||
}
|
||
|
||
// 验证类型
|
||
if _, ok := resolver.(*DNSResolver); !ok {
|
||
t.Error("Expected DNSResolver type")
|
||
}
|
||
}
|
||
|
||
// TestMockDNSResolverDefaultValues 测试默认值设置。
|
||
func TestMockDNSResolverDefaultValues(t *testing.T) {
|
||
cfg := &config.ResolverConfig{
|
||
Enabled: true,
|
||
// 其他字段为零值
|
||
}
|
||
|
||
resolver := New(cfg).(*DNSResolver)
|
||
|
||
// 验证默认值
|
||
if resolver.config.Valid != 30*time.Second {
|
||
t.Errorf("Expected default Valid 30s, got %v", resolver.config.Valid)
|
||
}
|
||
if resolver.config.Timeout != 5*time.Second {
|
||
t.Errorf("Expected default Timeout 5s, got %v", resolver.config.Timeout)
|
||
}
|
||
if !resolver.config.IPv4 {
|
||
t.Error("Expected IPv4 to be enabled by default")
|
||
}
|
||
}
|
||
|
||
// TestMockDNSLookupHostIPAddress 测试直接 IP 地址。
|
||
func TestMockDNSLookupHostIPAddress(t *testing.T) {
|
||
cfg := &config.ResolverConfig{
|
||
Enabled: true,
|
||
}
|
||
|
||
resolver := New(cfg).(*DNSResolver)
|
||
|
||
// 直接 IP 地址应直接返回
|
||
ips, err := resolver.LookupHost(context.Background(), "127.0.0.1")
|
||
if err != nil {
|
||
t.Fatalf("LookupHost() error: %v", err)
|
||
}
|
||
if len(ips) != 1 || ips[0] != "127.0.0.1" {
|
||
t.Errorf("Expected ['127.0.0.1'], got %v", ips)
|
||
}
|
||
|
||
// IPv6 地址
|
||
ips, err = resolver.LookupHost(context.Background(), "::1")
|
||
if err != nil {
|
||
t.Fatalf("LookupHost() error for IPv6: %v", err)
|
||
}
|
||
if len(ips) != 1 || ips[0] != "::1" {
|
||
t.Errorf("Expected ['::1'], got %v", ips)
|
||
}
|
||
}
|
||
|
||
// TestMockDNSLookupHostWithCache 测试带缓存的解析。
|
||
func TestMockDNSLookupHostWithCache(t *testing.T) {
|
||
cfg := &config.ResolverConfig{
|
||
Enabled: true,
|
||
Valid: 30 * time.Second,
|
||
CacheSize: 10,
|
||
}
|
||
|
||
resolver := New(cfg).(*DNSResolver)
|
||
|
||
// 第一次查询(缓存未命中)
|
||
ips1, err := resolver.LookupHostWithCache(context.Background(), "localhost")
|
||
if err != nil {
|
||
t.Fatalf("First LookupHostWithCache() error: %v", err)
|
||
}
|
||
|
||
// 第二次查询(缓存命中)
|
||
ips2, err := resolver.LookupHostWithCache(context.Background(), "localhost")
|
||
if err != nil {
|
||
t.Fatalf("Second LookupHostWithCache() error: %v", err)
|
||
}
|
||
|
||
// 结果应一致
|
||
if len(ips1) != len(ips2) {
|
||
t.Errorf("Cache result mismatch: %v vs %v", ips1, ips2)
|
||
}
|
||
|
||
// 验证缓存命中
|
||
stats := resolver.Stats()
|
||
if stats.CacheHits < 1 {
|
||
t.Error("Expected at least one cache hit")
|
||
}
|
||
}
|
||
|
||
// TestMockDNSCacheExpiration 测试缓存过期。
|
||
func TestMockDNSCacheExpiration(t *testing.T) {
|
||
cfg := &config.ResolverConfig{
|
||
Enabled: true,
|
||
Valid: 100 * time.Millisecond, // 短 TTL
|
||
CacheSize: 10,
|
||
}
|
||
|
||
resolver := New(cfg).(*DNSResolver)
|
||
|
||
// 第一次查询
|
||
_, err := resolver.LookupHostWithCache(context.Background(), "localhost")
|
||
if err != nil {
|
||
t.Fatalf("First lookup error: %v", err)
|
||
}
|
||
|
||
// 等待缓存过期
|
||
time.Sleep(150 * time.Millisecond)
|
||
|
||
// 第二次查询应重新解析
|
||
_, err = resolver.LookupHostWithCache(context.Background(), "localhost")
|
||
if err != nil {
|
||
t.Fatalf("Second lookup error: %v", err)
|
||
}
|
||
|
||
// 验证有两次缓存未命中
|
||
stats := resolver.Stats()
|
||
if stats.CacheMisses < 2 {
|
||
t.Errorf("Expected at least 2 cache misses, got %d", stats.CacheMisses)
|
||
}
|
||
}
|
||
|
||
// TestMockDNSLRUEviction 测试 LRU 淘汰。
|
||
func TestMockDNSLRUEviction(t *testing.T) {
|
||
cfg := &config.ResolverConfig{
|
||
Enabled: true,
|
||
Valid: 30 * time.Second,
|
||
CacheSize: 3, // 只缓存 3 个
|
||
}
|
||
|
||
resolver := New(cfg).(*DNSResolver)
|
||
|
||
// 添加 3 个条目
|
||
hosts := []string{"host1.local", "host2.local", "host3.local"}
|
||
for _, host := range hosts {
|
||
resolver.storeCache(host, &DNSCacheEntry{
|
||
IPs: []string{"127.0.0.1"},
|
||
ExpiresAt: time.Now().Add(time.Hour),
|
||
})
|
||
}
|
||
|
||
// 验证缓存大小
|
||
resolver.mu.RLock()
|
||
cacheLen := len(resolver.cache)
|
||
resolver.mu.RUnlock()
|
||
if cacheLen != 3 {
|
||
t.Errorf("Expected 3 cache entries, got %d", cacheLen)
|
||
}
|
||
|
||
// 添加第 4 个条目,应淘汰最旧的
|
||
resolver.storeCache("host4.local", &DNSCacheEntry{
|
||
IPs: []string{"127.0.0.1"},
|
||
ExpiresAt: time.Now().Add(time.Hour),
|
||
})
|
||
|
||
resolver.mu.RLock()
|
||
cacheLen = len(resolver.cache)
|
||
_, exists := resolver.cache["host1.local"]
|
||
resolver.mu.RUnlock()
|
||
|
||
if cacheLen > 3 {
|
||
t.Errorf("Cache should not exceed 3 entries, got %d", cacheLen)
|
||
}
|
||
if exists {
|
||
t.Error("Oldest entry should have been evicted")
|
||
}
|
||
}
|
||
|
||
// TestMockDNSRefreshLoop 测试后台刷新循环。
|
||
func TestMockDNSRefreshLoop(t *testing.T) {
|
||
cfg := &config.ResolverConfig{
|
||
Enabled: true,
|
||
Valid: 200 * time.Millisecond,
|
||
Timeout: 1 * time.Second,
|
||
CacheSize: 10,
|
||
}
|
||
|
||
resolver := New(cfg).(*DNSResolver)
|
||
|
||
// 启动刷新循环
|
||
if err := resolver.Start(); err != nil {
|
||
t.Fatalf("Start() error: %v", err)
|
||
}
|
||
|
||
// 验证已启动
|
||
if !resolver.started.Load() {
|
||
t.Error("Resolver should be started")
|
||
}
|
||
|
||
// 添加一个需要刷新的主机
|
||
resolver.mu.Lock()
|
||
resolver.refreshHosts["localhost"] = struct{}{}
|
||
resolver.mu.Unlock()
|
||
|
||
// 等待刷新
|
||
time.Sleep(300 * time.Millisecond)
|
||
|
||
// 停止
|
||
if err := resolver.Stop(); err != nil {
|
||
t.Fatalf("Stop() error: %v", err)
|
||
}
|
||
|
||
// 验证已停止
|
||
if resolver.started.Load() {
|
||
t.Error("Resolver should be stopped")
|
||
}
|
||
}
|
||
|
||
// TestMockDNSStartIdempotent 测试重复启动。
|
||
func TestMockDNSStartIdempotent(t *testing.T) {
|
||
cfg := &config.ResolverConfig{
|
||
Enabled: true,
|
||
}
|
||
|
||
resolver := New(cfg).(*DNSResolver)
|
||
|
||
// 第一次启动
|
||
if err := resolver.Start(); err != nil {
|
||
t.Fatalf("First Start() error: %v", err)
|
||
}
|
||
|
||
// 第二次启动应无操作
|
||
if err := resolver.Start(); err != nil {
|
||
t.Fatalf("Second Start() error: %v", err)
|
||
}
|
||
|
||
// 清理
|
||
_ = resolver.Stop()
|
||
}
|
||
|
||
// TestMockDNSStopIdempotent 测试重复停止。
|
||
func TestMockDNSStopIdempotent(t *testing.T) {
|
||
cfg := &config.ResolverConfig{
|
||
Enabled: true,
|
||
}
|
||
|
||
resolver := New(cfg).(*DNSResolver)
|
||
|
||
// 未启动时停止
|
||
if err := resolver.Stop(); err != nil {
|
||
t.Fatalf("Stop() on non-started resolver error: %v", err)
|
||
}
|
||
|
||
// 启动
|
||
_ = resolver.Start()
|
||
|
||
// 第一次停止
|
||
if err := resolver.Stop(); err != nil {
|
||
t.Fatalf("First Stop() error: %v", err)
|
||
}
|
||
|
||
// 第二次停止应无操作
|
||
if err := resolver.Stop(); err != nil {
|
||
t.Fatalf("Second Stop() error: %v", err)
|
||
}
|
||
}
|
||
|
||
// TestMockDNSStatsFunc 测试统计信息。
|
||
func TestMockDNSStatsFunc(t *testing.T) {
|
||
cfg := &config.ResolverConfig{
|
||
Enabled: true,
|
||
CacheSize: 10,
|
||
}
|
||
|
||
resolver := New(cfg).(*DNSResolver)
|
||
|
||
// 初始统计
|
||
stats := resolver.Stats()
|
||
if stats.CacheHits != 0 || stats.CacheMisses != 0 {
|
||
t.Error("Initial stats should be zero")
|
||
}
|
||
|
||
// 执行查询
|
||
_, _ = resolver.LookupHostWithCache(context.Background(), "localhost")
|
||
_, _ = resolver.LookupHostWithCache(context.Background(), "localhost")
|
||
|
||
stats = resolver.Stats()
|
||
if stats.CacheMisses < 1 {
|
||
t.Error("Expected at least one cache miss")
|
||
}
|
||
if stats.CacheHits < 1 {
|
||
t.Error("Expected at least one cache hit")
|
||
}
|
||
}
|
||
|
||
// TestMockDNSQueryDNSTimeout 测试 DNS 查询超时。
|
||
func TestMockDNSQueryDNSTimeout(t *testing.T) {
|
||
cfg := &config.ResolverConfig{
|
||
Enabled: true,
|
||
Timeout: 100 * time.Millisecond,
|
||
Addresses: []string{"127.0.0.1:53535"}, // 不存在的 DNS 服务器
|
||
CacheSize: 10,
|
||
}
|
||
|
||
resolver := New(cfg).(*DNSResolver)
|
||
|
||
// 使用短超时 context
|
||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||
defer cancel()
|
||
|
||
start := time.Now()
|
||
_, err := resolver.LookupHost(ctx, "example.com")
|
||
elapsed := time.Since(start)
|
||
|
||
// 应该超时
|
||
if err == nil {
|
||
t.Error("Expected timeout error")
|
||
}
|
||
if elapsed > 200*time.Millisecond {
|
||
t.Errorf("Query took too long: %v", elapsed)
|
||
}
|
||
}
|
||
|
||
// TestMockDNSQueryWithResolverSystemDefault 测试使用系统默认 DNS。
|
||
func TestMockDNSQueryWithResolverSystemDefault(t *testing.T) {
|
||
cfg := &config.ResolverConfig{
|
||
Enabled: true,
|
||
IPv4: true,
|
||
CacheSize: 10,
|
||
}
|
||
|
||
resolver := New(cfg).(*DNSResolver)
|
||
|
||
// 不指定 DNS 服务器,使用系统默认
|
||
ips, err := resolver.queryWithResolver(context.Background(), "localhost", "")
|
||
if err != nil {
|
||
t.Fatalf("queryWithResolver() error: %v", err)
|
||
}
|
||
if len(ips) == 0 {
|
||
t.Error("Expected at least one IP for localhost")
|
||
}
|
||
}
|
||
|
||
// TestMockDNSConcurrentAccess 测试并发访问。
|
||
func TestMockDNSConcurrentAccess(t *testing.T) {
|
||
cfg := &config.ResolverConfig{
|
||
Enabled: true,
|
||
CacheSize: 100,
|
||
}
|
||
|
||
resolver := New(cfg).(*DNSResolver)
|
||
|
||
var wg sync.WaitGroup
|
||
concurrency := 10
|
||
iterations := 100
|
||
|
||
for range concurrency {
|
||
wg.Go(func() {
|
||
for range iterations {
|
||
_, _ = resolver.LookupHostWithCache(context.Background(), "localhost")
|
||
}
|
||
})
|
||
}
|
||
|
||
wg.Wait()
|
||
|
||
stats := resolver.Stats()
|
||
totalOps := stats.CacheHits + stats.CacheMisses
|
||
if totalOps < int64(concurrency*iterations/2) {
|
||
t.Errorf("Expected more operations, got hits=%d, misses=%d", stats.CacheHits, stats.CacheMisses)
|
||
}
|
||
}
|
||
|
||
// TestMockDNSMoveToFrontLocked 测试 LRU 移动到前端。
|
||
func TestMockDNSMoveToFrontLocked(t *testing.T) {
|
||
cfg := &config.ResolverConfig{
|
||
Enabled: true,
|
||
CacheSize: 10,
|
||
}
|
||
|
||
resolver := New(cfg).(*DNSResolver)
|
||
|
||
resolver.cache = map[string]*DNSCacheEntry{
|
||
"a": {},
|
||
"b": {},
|
||
"c": {},
|
||
}
|
||
elemA := resolver.lruList.PushFront("a")
|
||
elemB := resolver.lruList.PushFront("b")
|
||
elemC := resolver.lruList.PushFront("c")
|
||
resolver.lruIndex["a"] = elemA
|
||
resolver.lruIndex["b"] = elemB
|
||
resolver.lruIndex["c"] = elemC
|
||
|
||
resolver.lruList.MoveToFront(resolver.lruIndex["a"])
|
||
|
||
order := []string{}
|
||
for e := resolver.lruList.Back(); e != nil; e = e.Prev() {
|
||
order = append(order, e.Value.(string))
|
||
}
|
||
expected := []string{"b", "c", "a"}
|
||
for i, v := range expected {
|
||
if order[i] != v {
|
||
t.Errorf("LRU order[%d] = %s, want %s", i, order[i], v)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestMockDNSNoopResolver 测试空实现解析器。
|
||
func TestMockDNSNoopResolver(t *testing.T) {
|
||
var r Resolver = &noopResolver{}
|
||
|
||
// LookupHost 应返回错误
|
||
_, err := r.LookupHost(context.Background(), "example.com")
|
||
if err == nil {
|
||
t.Error("Expected error from noopResolver.LookupHost")
|
||
}
|
||
|
||
// LookupHostWithCache 应返回错误
|
||
_, err = r.LookupHostWithCache(context.Background(), "example.com")
|
||
if err == nil {
|
||
t.Error("Expected error from noopResolver.LookupHostWithCache")
|
||
}
|
||
|
||
// Refresh 应返回 nil
|
||
if err := r.Refresh("example.com"); err != nil {
|
||
t.Errorf("noopResolver.Refresh() error: %v", err)
|
||
}
|
||
|
||
// Start 应返回 nil
|
||
if err := r.Start(); err != nil {
|
||
t.Errorf("noopResolver.Start() error: %v", err)
|
||
}
|
||
|
||
// Stop 应返回 nil
|
||
if err := r.Stop(); err != nil {
|
||
t.Errorf("noopResolver.Stop() error: %v", err)
|
||
}
|
||
|
||
// Stats 应返回空
|
||
stats := r.Stats()
|
||
if stats.CacheHits != 0 || stats.CacheMisses != 0 {
|
||
t.Error("noopResolver.Stats() should return zero values")
|
||
}
|
||
}
|
||
|
||
// TestMockDNSCacheEntryConcurrency 测试缓存条目并发访问。
|
||
func TestMockDNSCacheEntryConcurrency(t *testing.T) {
|
||
entry := &DNSCacheEntry{
|
||
IPs: []string{"127.0.0.1", "127.0.0.2"},
|
||
ExpiresAt: time.Now().Add(time.Hour),
|
||
}
|
||
|
||
var wg sync.WaitGroup
|
||
var readCount atomic.Int64
|
||
|
||
// 并发读取
|
||
for range 100 {
|
||
wg.Go(func() {
|
||
entry.mu.RLock()
|
||
_ = entry.IPs
|
||
_ = entry.ExpiresAt
|
||
entry.mu.RUnlock()
|
||
readCount.Add(1)
|
||
})
|
||
}
|
||
|
||
wg.Wait()
|
||
|
||
if readCount.Load() != 100 {
|
||
t.Errorf("Expected 100 reads, got %d", readCount.Load())
|
||
}
|
||
}
|
||
|
||
// TestMockDNSIPv6QueryFallback 测试 IPv6 查询失败降级。
|
||
func TestMockDNSIPv6QueryFallback(t *testing.T) {
|
||
cfg := &config.ResolverConfig{
|
||
Enabled: true,
|
||
IPv4: true,
|
||
IPv6: true,
|
||
CacheSize: 10,
|
||
}
|
||
|
||
resolver := New(cfg).(*DNSResolver)
|
||
|
||
// 查询 localhost(通常有 IPv4)
|
||
ips, err := resolver.LookupHost(context.Background(), "localhost")
|
||
if err != nil {
|
||
t.Fatalf("LookupHost() error: %v", err)
|
||
}
|
||
|
||
// 应该至少有 IPv4 地址
|
||
if len(ips) == 0 {
|
||
t.Error("Expected at least one IP address")
|
||
}
|
||
}
|
||
|
||
// TestMockDNSDoRefresh 测试刷新操作。
|
||
func TestMockDNSDoRefresh(t *testing.T) {
|
||
cfg := &config.ResolverConfig{
|
||
Enabled: true,
|
||
Valid: 30 * time.Second,
|
||
Timeout: 1 * time.Second,
|
||
CacheSize: 10,
|
||
}
|
||
|
||
resolver := New(cfg).(*DNSResolver)
|
||
|
||
// 添加需要刷新的主机
|
||
resolver.mu.Lock()
|
||
resolver.refreshHosts["localhost"] = struct{}{}
|
||
resolver.refreshHosts["127.0.0.1"] = struct{}{} // IP 地址会被跳过
|
||
resolver.mu.Unlock()
|
||
|
||
// 执行刷新
|
||
resolver.doRefresh()
|
||
|
||
// 验证刷新列表仍然存在
|
||
resolver.mu.RLock()
|
||
_, exists := resolver.refreshHosts["localhost"]
|
||
resolver.mu.RUnlock()
|
||
|
||
if !exists {
|
||
t.Error("Refresh hosts should still contain localhost")
|
||
}
|
||
}
|