Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 23 additions & 41 deletions internal/core/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"fmt"
"sort"
"sync"
"time"

"RealityChecker/internal/detectors"
Expand Down Expand Up @@ -69,8 +68,8 @@ func (p *Pipeline) Execute(ctx context.Context, domain string) (*types.Detection
EarlyExit: false,
}

// 并发执行检测阶段,提高检测效率
p.executeStagesConcurrently(ctx, pipelineCtx)
// 按依赖顺序执行单个域名的检测阶段;域名之间由批量管理器并发。
p.executeStages(ctx, pipelineCtx)

// 计算总耗时
pipelineCtx.Result.Duration = time.Since(startTime)
Expand All @@ -81,8 +80,8 @@ func (p *Pipeline) Execute(ctx context.Context, domain string) (*types.Detection
return pipelineCtx.Result, nil
}

// executeStagesConcurrently 并发执行检测阶段
func (p *Pipeline) executeStagesConcurrently(ctx context.Context, pipelineCtx *types.PipelineContext) {
// executeStages 按依赖关系执行检测阶段
func (p *Pipeline) executeStages(ctx context.Context, pipelineCtx *types.PipelineContext) {
// 将检测阶段分为两组:阻塞检测和网络检测
var blockingStages []types.DetectionStage
var networkStages []types.DetectionStage
Expand Down Expand Up @@ -123,50 +122,33 @@ func (p *Pipeline) executeStagesConcurrently(ctx context.Context, pipelineCtx *t
return
}

// 并发执行网络检测阶段
// TLS/CDN 与热门网站检测共享结果,必须按优先级顺序执行。
if len(networkStages) > 0 {
p.executeNetworkStagesConcurrently(ctx, pipelineCtx, networkStages)
p.executeNetworkStagesSequentially(ctx, pipelineCtx, networkStages)
}
}

// executeNetworkStagesConcurrently 并发执行网络检测阶段
func (p *Pipeline) executeNetworkStagesConcurrently(ctx context.Context, pipelineCtx *types.PipelineContext, stages []types.DetectionStage) {
// 使用信号量控制网络检测的并发数
networkConcurrency := 4 // 网络检测使用4个并发
semaphore := make(chan struct{}, networkConcurrency)

var wg sync.WaitGroup
for i, stage := range stages {
wg.Add(1)
go func(index int, s types.DetectionStage) {
defer wg.Done()

// 获取信号量
select {
case semaphore <- struct{}{}:
defer func() {
<-semaphore
}()
case <-ctx.Done():
return
}

// 执行检测阶段
func() {
defer func() {
if r := recover(); r != nil {
pipelineCtx.Result.Error = fmt.Errorf("检测阶段 %s panic: %v", s.Name(), r)
}
}()
// executeNetworkStagesSequentially 按优先级执行有数据依赖的网络检测阶段。
func (p *Pipeline) executeNetworkStagesSequentially(ctx context.Context, pipelineCtx *types.PipelineContext, stages []types.DetectionStage) {
for _, stage := range stages {
select {
case <-ctx.Done():
return
default:
}

if err := s.Execute(pipelineCtx); err != nil {
pipelineCtx.Result.Error = err
func() {
defer func() {
if recovered := recover(); recovered != nil {
pipelineCtx.Result.Error = fmt.Errorf("检测阶段 %s panic: %v", stage.Name(), recovered)
}
}()
}(i, stage)
}

wg.Wait()
if err := stage.Execute(pipelineCtx); err != nil {
pipelineCtx.Result.Error = err
}
}()
}
}

// evaluateSuitability 评估适合性
Expand Down
86 changes: 86 additions & 0 deletions internal/core/pipeline_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package core

import (
"context"
"errors"
"reflect"
"strings"
"testing"

"RealityChecker/internal/types"
)

type testStage struct {
name string
execute func(*types.PipelineContext) error
}

func (s testStage) Execute(ctx *types.PipelineContext) error { return s.execute(ctx) }
func (s testStage) CanEarlyExit() bool { return false }
func (s testStage) Priority() int { return 0 }
func (s testStage) Name() string { return s.name }

func TestExecuteNetworkStagesSequentiallyPreservesDependencies(t *testing.T) {
var order []string
pipelineCtx := &types.PipelineContext{
Result: &types.DetectionResult{},
}

stages := []types.DetectionStage{
testStage{name: "redirect", execute: func(ctx *types.PipelineContext) error {
order = append(order, "redirect")
ctx.Result.Network = &types.NetworkResult{FinalDomain: "example.com"}
return nil
}},
testStage{name: "tls", execute: func(ctx *types.PipelineContext) error {
order = append(order, "tls")
if ctx.Result.Network == nil || ctx.Result.Network.FinalDomain != "example.com" {
return errors.New("redirect result is unavailable")
}
ctx.Result.TLS = &types.TLSResult{SupportsTLS13: true}
return nil
}},
testStage{name: "hot-website", execute: func(ctx *types.PipelineContext) error {
order = append(order, "hot-website")
if ctx.Result.TLS == nil || !ctx.Result.TLS.SupportsTLS13 {
return errors.New("TLS result is unavailable")
}
return nil
}},
}

(&Pipeline{}).executeNetworkStagesSequentially(context.Background(), pipelineCtx, stages)

if pipelineCtx.Result.Error != nil {
t.Fatalf("unexpected stage error: %v", pipelineCtx.Result.Error)
}
wantOrder := []string{"redirect", "tls", "hot-website"}
if !reflect.DeepEqual(order, wantOrder) {
t.Fatalf("execution order = %v, want %v", order, wantOrder)
}
}

func TestExecuteNetworkStagesSequentiallyRecoversPanic(t *testing.T) {
continued := false
pipelineCtx := &types.PipelineContext{
Result: &types.DetectionResult{},
}
stages := []types.DetectionStage{
testStage{name: "panic-stage", execute: func(*types.PipelineContext) error {
panic("boom")
}},
testStage{name: "next-stage", execute: func(*types.PipelineContext) error {
continued = true
return nil
}},
}

(&Pipeline{}).executeNetworkStagesSequentially(context.Background(), pipelineCtx, stages)

if !continued {
t.Fatal("pipeline did not continue after recovering a stage panic")
}
if pipelineCtx.Result.Error == nil || !strings.Contains(pipelineCtx.Result.Error.Error(), "panic-stage") {
t.Fatalf("panic error = %v, want stage name", pipelineCtx.Result.Error)
}
}