3. 节点定义与属性:Node 结构体详解

在 GoDAG 调度系统中,节点(Node) 是构成有向无环图(DAG)的最小执行单元。每个节点封装了调度引擎所需的所有信息:唯一标识、执行逻辑、依赖关系以及运行时元数据。本章将深入解析 Node 结构体的设计,重点讲解节点 ID、名称、元数据(Metadata)的存储与读取机制。

3.1 Node 结构体核心定义

以下代码展示了 GoDAG 中 Node 结构体的标准定义。该结构体采用“字段 + 方法”的封装模式,确保属性安全访问的同时,提供灵活的扩展能力。

// Node 定义 DAG 中的基本执行单元
type Node struct {
    // 核心标识字段(不可变)
    ID   string // 全局唯一节点 ID,由 DAG 构建时生成
    Name string // 节点可读名称,用于日志与监控

    // 元数据存储(可变,线程安全)
    metadata sync.Map // 存储任意类型的键值对

    // 内部状态(不对外暴露)
    status     NodeStatus   // 当前执行状态
    dependents []*Node      // 下游依赖节点列表
    depCount   int32        // 尚未就绪的上游依赖计数
    runFunc    func(ctx context.Context) error // 节点执行函数
}

// NodeStatus 节点生命周期状态枚举
type NodeStatus int32

const (
    StatusPending   NodeStatus = iota // 等待调度
    StatusReady                       // 依赖已满足,可执行
    StatusRunning                     // 正在执行
    StatusSuccess                     // 执行成功
    StatusFailed                      // 执行失败
    StatusSkipped                     // 被跳过(条件不满足)
)
设计要点: 使用 sync.Map 存储元数据而非普通 map,是为了支持并发场景下多个 goroutine 同时读写节点属性(如监控探针、动态参数注入),避免加锁带来的性能开销。

3.2 节点 ID 与名称:唯一性与可读性

属性 类型 约束 用途
ID string 全局唯一,不可变,构建时生成 DAG 内部寻址、依赖关系解析、状态追踪
Name string 同一 DAG 内建议唯一,允许重复 日志输出、监控面板、人工排查

ID 生成策略: 推荐使用 UUID v4 或 Snowflake 算法,确保分布式环境下不冲突。Name 则通常由用户定义,如 "数据清洗"、"模型训练",便于阅读。

// NewNode 创建节点实例,自动生成 UUID 作为 ID
func NewNode(name string, fn func(ctx context.Context) error) *Node {
    return &Node{
        ID:        uuid.New().String(),
        Name:      name,
        metadata:  sync.Map{},
        status:    StatusPending,
        runFunc:   fn,
    }
}

3.3 元数据(Metadata)的存储与读取

元数据是节点属性的“扩展槽”,用于存储调度引擎或用户自定义的附加信息,例如:超时时间、重试次数、环境变量、标签、输入输出参数等。GoDAG 通过 sync.Map 提供线程安全的读写接口。

3.3.1 核心操作方法

// SetMetadata 存储键值对,key 必须为非空字符串
func (n *Node) SetMetadata(key string, value interface{}) {
    if key == "" {
        return // 拒绝空键
    }
    n.metadata.Store(key, value)
}

// GetMetadata 读取指定键的值,若不存在返回 nil
func (n *Node) GetMetadata(key string) interface{} {
    val, _ := n.metadata.Load(key)
    return val
}

// GetMetadataAs 泛型读取,自动类型断言,避免调用方重复转换
func GetMetadataAs[T any](n *Node, key string) (T, bool) {
    val := n.GetMetadata(key)
    if val == nil {
        var zero T
        return zero, false
    }
    result, ok := val.(T)
    return result, ok
}

// DeleteMetadata 删除指定键
func (n *Node) DeleteMetadata(key string) {
    n.metadata.Delete(key)
}

// ListMetadataKeys 列出所有元数据键名(用于调试或监控)
func (n *Node) ListMetadataKeys() []string {
    var keys []string
    n.metadata.Range(func(key, value interface{}) bool {
        keys = append(keys, key.(string))
        return true
    })
    return keys
}
最佳实践: 使用 GetMetadataAs[T] 泛型函数可以避免每次读取后手动类型断言,减少代码重复。例如:timeout, ok := GetMetadataAs[time.Duration](node, "timeout")。

3.3.2 常见元数据键约定

GoDAG 调度引擎预定义了一些标准元数据键,用户也可自由扩展:

键名 值类型 说明
"timeout" time.Duration 节点执行超时时间,超时后自动标记失败
"retry" int 失败重试次数,默认 0 表示不重试
"tags" []string 标签列表,用于分组调度或条件路由
"env" map[string]string 环境变量,注入到执行上下文中
"description" string 节点功能描述,用于文档生成

3.4 节点属性结构图

下图展示了 Node 结构体的完整属性布局及其在 DAG 中的角色:

Node 结构体属性与元数据关系图 Node 结构体属性与元数据关系 Node 实例 不可变属性(构建时确定) ID: uuid (全局唯一) Name: 可读名称 可变属性(运行时更新) status: NodeStatus metadata: sync.Map metadata 典型键值对 "timeout" → 30s "retry" → 3 "tags" → ["critical","gpu"] "env" → {"MODE":"prod"} "description" → "清洗原始数据" ID 和 Name 在节点创建后不可修改;metadata 支持并发读写,适合动态注入配置

3.5 元数据读取性能对比

为了帮助开发者选择最合适的读取方式,下表对比了三种元数据访问模式的适用场景:

读取方式 代码示例 类型安全 性能 推荐场景
直接 GetMetadata val := node.GetMetadata("timeout") 否(需手动断言) 高 快速原型、类型已知的临时读取
泛型 GetMetadataAs[T] t, ok := GetMetadataAs[time.Duration](node, "timeout") 是 中(含类型断言开销) 生产代码,推荐使用
Range 遍历 node.metadata.Range(func(k, v any) bool { ... }) 否 低(全量遍历) 调试、监控、导出所有元数据

3.6 节点定义完整示例

以下示例展示了如何创建一个带有元数据的节点,并在执行函数中读取配置:

// 创建节点
cleanNode := NewNode("数据清洗", func(ctx context.Context) error {
    // 从元数据读取超时配置
    timeout, ok := GetMetadataAs[time.Duration](ctx.Value("node").(*Node), "timeout")
    if ok {
        // 应用超时逻辑
    }
    
    // 读取环境变量
    env, _ := GetMetadataAs[map[string]string](ctx.Value("node").(*Node), "env")
    fmt.Println("运行模式:", env["MODE"])
    
    // 执行实际业务逻辑
    return cleanData()
})

// 设置元数据
cleanNode.SetMetadata("timeout", 30*time.Second)
cleanNode.SetMetadata("retry", 3)
cleanNode.SetMetadata("env", map[string]string{"MODE": "prod"})
cleanNode.SetMetadata("tags", []string{"critical", "data-pipeline"})
注意: 元数据的 key 建议使用常量定义,避免字符串硬编码。例如:const MetaKeyTimeout = "timeout"。同时,避免在元数据中存储大量数据(如超过 1MB),否则会影响 sync.Map 的遍历性能。

3.7 本章小结

  • Node 结构体 由不可变字段(ID、Name)和可变字段(status、metadata)组成,兼顾安全性与灵活性。
  • ID 保证全局唯一性,Name 提供可读性,两者共同构成节点的身份标识。
  • Metadata 基于 sync.Map 实现,支持并发安全的键值存储,是节点扩展属性的核心机制。
  • 推荐使用 泛型读取方法 GetMetadataAs[T] 提升代码的类型安全性和可维护性。