DAG 任务调度的前端实现

背景

最近笔者在看后端操作数仓跑数任务的时候,发现后端有一个很有意思的编排任务的页面,里面实现的功能就是,箭头后面的任务需要等它前面所有任务跑完才会跑当前任务,比如任务F依赖于任务C,D,E,但是任务E依赖于任务B,任务C,D,B依赖于任务A,他们的时间都不是固定的,但是都需前置的任务完成才能跑,下面画一个简易的流程图,假如任务A需要1秒,任务B需要2秒,任务C需要3秒依次到任务I需要9秒

graph LR A --> C C --> F A --> D D --> F A --> B B --> E E --> F G --> H I

所以可以得到下面的时序图

输出顺序是 A B C D G E I F H

coding

那么我们怎么样才能实现这么一个任务队列呢?

节点

首先我们定义一个节点,

ts 复制代码
class TaskNode {
  // 任务
  public task: () => Promise<String>
  // 后面的子节点
  public edges: String[]
  // 父亲节点
  public parents: String[]
  // 名称
  public name: string
  // 状态  初始化的时候是空 pending 任务执行中 success任务执行完毕
  public status: string
  // 任务id
  public id: string
  // 是否被遍历
  public visited: boolean
  constructor(task: () => Promise<String>, name: string, id: string) {
    this.task = task
    this.name = name
    this.id = id
    this.status = ''
    this.edges = []
    this.visited = false
  }
}

再定义一个生产任务的函数

ts 复制代码
/**
 * 
 * @param taskName 任务名称
 * @param timeout 定时器时间
 * @returns 
 */
function genTask(taskName: string, timeout: number): () => Promise<String> {
  return function () {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(taskName)
      }, timeout * 1000)
    })
  }
}

模拟页面上的连线

哪个任务依赖哪个任务是在页面上有两个下拉框选择的,我们就用代码模拟选择的过程

ts 复制代码
const A = new TaskNode(genTask('A', 1), 'A', 'A')
const B = new TaskNode(genTask('B', 2), 'B', 'B')
const C = new TaskNode(genTask('C', 3), 'C', 'C')
const D = new TaskNode(genTask('D', 4), 'D', 'D')
const E = new TaskNode(genTask('E', 5), 'E', 'E')
const F = new TaskNode(genTask('F', 6), 'F', 'F')
const G = new TaskNode(genTask('G', 7), 'G', 'G')
const H = new TaskNode(genTask('H', 8), 'H', 'H')
const I = new TaskNode(genTask('I', 9), 'I', 'I')

A.edges.push(C.id)
A.edges.push(D.id)
A.edges.push(B.id)
C.edges.push(F.id)
D.edges.push(F.id)
B.edges.push(E.id)
E.edges.push(F.id)
G.edges.push(H.id)

判断这个图是否有环

怎么判断给的这个图是不是有环呢?如果有环的话那么这个任务在我们业务系统上要给出一个提醒,有环的话意味着这个任务永远不会结束。

我们是不是可以这样理解,只有节点的父节点都遍历完了,才能算当前节点遍历完了,所以我们需要知道当前节点有哪些父节点,所以我们给TaskNode的parents赋值

ts 复制代码
// 传入一个taskList ,设置每一个TaskNode的parents
  setParents(taskList: TaskNode[]) {
    const parentMaps = new Map()
    taskList.forEach((task) => {
      task.edges.forEach((edge) => {
        // 拿到父节点id数组,再赋值
        const parents = parentMaps.get(edge) || []
        parents.push(task.id)
        parentMaps.set(edge, parents)
      })
    })
    taskList.forEach((task) => {
      task.parents = parentMaps.get(task.id) || []
    })
    return taskList
  }

那么怎么遍历呢

很容易写出来下面的方法 缓存中有task就认为有环

ts 复制代码
  // 图是否有环
  hasCircle() {
    const cache = new Set()
    const taskList = this.taskList
    const taskMap = taskList.reduce((prev, cur) => {
      prev[cur.id] = cur
      return prev
    }, {})
    function loop(taskList) {
      if (taskList.length === 0) {
        return false
      }
      taskList.forEach((task) => {
        // 只要父节点的visited为true
        if (task.parents.every((parent) => taskMap[parent].visited)) {
          // 缓存中有task就认为有环
          if (cache.has(task)) {
            console.log(task)
            throw new Error('已经有环')
          }
          cache.add(task)
          task.visited = true
        }
      })
      loop(taskList.filter((task) => !task.visited))
    }
    try {
      loop(taskList)
    } catch (error) {
      return true
    }
    return false
  }

逻辑上好像没问题?可是有一个问题 真的会有一个message为已经有环的的Error被抛出吗?

按顺序执行任务

如果没有环的话那么接下来就是执行任务了

这一步就比较简单了递归的去执行任务

ts 复制代码
// 启动
  runjob() {
    const taskList = [...this.taskList]
    const taskMap = taskList.reduce((prev, cur) => {
      prev[cur.id] = cur
      return prev
    }, {})
    const heads = taskList.filter((task) => task.parents.length === 0)
    function loop(tasks) {
      tasks.forEach(taskNode => {
        if(['success','pending'].includes(taskNode.status)){
          return
        }
        taskNode.status = 'pending'
        taskNode.task().then(() => {
          taskNode.status = 'success'
          console.log(taskNode.name + "完成任务")
          loop(taskNode.edges.map(id => taskMap[id]))
        })
      })
    }
    loop(heads)
  }

完整代码如下

ts 复制代码
class TaskNode {
  // 任务
  public task: () => Promise<String>
  // 后面的子节点
  public edges: String[]
  // 父亲节点
  public parents: String[]
  // 名称
  public name: string
  // 状态  初始化的时候是空 pending 任务执行中 success任务执行完毕
  public status: string
  // 任务id
  public id: string
  // 是否被遍历
  public visited: boolean
  constructor(task: () => Promise<String>, name: string, id: string) {
    this.task = task
    this.name = name
    this.id = id
    this.status = ''
    this.edges = []
    this.visited = false
  }
}

/**
 * 
 * @param taskName 任务名称
 * @param timeout 定时器时间
 * @returns 
 */
function genTask(taskName: string, timeout: number): () => Promise<String> {
  return function () {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(taskName)
      }, timeout * 1000)
    })
  }
}

const A = new TaskNode(genTask('A', 1), 'A', 'A')
const B = new TaskNode(genTask('B', 2), 'B', 'B')
const C = new TaskNode(genTask('C', 3), 'C', 'C')
const D = new TaskNode(genTask('D', 4), 'D', 'D')
const E = new TaskNode(genTask('E', 5), 'E', 'E')
const F = new TaskNode(genTask('F', 6), 'F', 'F')
const G = new TaskNode(genTask('G', 7), 'G', 'G')
const H = new TaskNode(genTask('H', 8), 'H', 'H')
const I = new TaskNode(genTask('I', 9), 'I', 'I')

A.edges.push(C.id)
A.edges.push(D.id)
A.edges.push(B.id)
C.edges.push(F.id)
D.edges.push(F.id)
B.edges.push(E.id)
E.edges.push(F.id)
G.edges.push(H.id)
// F.edges.push(A.id)
class Dag {
  public taskList: TaskNode[]
  constructor(taskList: TaskNode[]) {
    this.taskList = this.setParents(taskList)
    this.taskList.forEach((task) => {
      task.visited = false
    })
    this.runjob()
  }
  // 传入一个taskList ,设置每一个TaskNode的parents
  setParents(taskList: TaskNode[]) {
    const parentMaps = new Map()
    taskList.forEach((task) => {
      task.edges.forEach((edge) => {
        // 拿到父节点id数组,再赋值
        const parents = parentMaps.get(edge) || []
        parents.push(task.id)
        parentMaps.set(edge, parents)
      })
    })
    taskList.forEach((task) => {
      task.parents = parentMaps.get(task.id) || []
    })
    return taskList
  }
  // 图是否有环
  hasCircle() {
    // const cache = new Set()
    const taskList = this.taskList
    const taskMap = taskList.reduce((prev, cur) => {
      prev[cur.id] = cur
      return prev
    }, {})
    function loop(taskList) {
      if (taskList.length === 0) {
        return false
      }
      taskList.forEach((task) => {
        // 只要父节点的visited为true
        if (task.parents.every((parent) => taskMap[parent].visited)) {
          // 缓存中有task就认为有环
          // if (cache.has(task)) {
          //   console.log(task)
          //   throw new Error('已经有环')
          // }
          // cache.add(task)
          task.visited = true
        }
      })
      loop(taskList.filter((task) => !task.visited))
    }
    try {
      loop(taskList)
    } catch (error) {
      return true
    }
    return false
  }
  // 启动
  runjob() {
    const taskList = [...this.taskList]
    const taskMap = taskList.reduce((prev, cur) => {
      prev[cur.id] = cur
      return prev
    }, {})
    const heads = taskList.filter((task) => task.parents.length === 0)
    function loop(tasks) {
      tasks.forEach(taskNode => {
        if(['success','pending'].includes(taskNode.status)){
          return
        }
        taskNode.status = 'pending'
        taskNode.task().then(() => {
          taskNode.status = 'success'
          console.log(taskNode.name + "完成任务")
          loop(taskNode.edges.map(id => taskMap[id]))
        })
      })
    }
    loop(heads)
  }
}

new Dag([A, B, C, D, E, F, G, H, I])
相关推荐
新晓·故知20 分钟前
<基于递归实现线索二叉树的构造及遍历算法探讨>
数据结构·经验分享·笔记·算法·链表
总裁余(余登武)31 分钟前
算法竞赛(Python)-万变中的不变“随机算法”
开发语言·python·算法
john_hjy37 分钟前
11. 异步编程
运维·服务器·javascript
Eric.Lee202143 分钟前
音频文件重采样 - python 实现
人工智能·python·深度学习·算法·audio·音频重采样
风清扬_jd1 小时前
Chromium 中JavaScript Fetch API接口c++代码实现(二)
javascript·c++·chrome
huapiaoy1 小时前
Redis中数据类型的使用(hash和list)
redis·算法·哈希算法
冷白白1 小时前
【C++】C++对象初探及友元
c语言·开发语言·c++·算法
yanlele1 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范
It'sMyGo1 小时前
Javascript数组研究09_Array.prototype[Symbol.unscopables]
开发语言·javascript·原型模式
鹤上听雷1 小时前
【AGC005D】~K Perm Counting(计数抽象成图)
算法