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])
相关推荐
J总裁的小芒果6 分钟前
THREE.js 入门(六) 纹理、uv坐标
开发语言·javascript·uv
m0_548514777 分钟前
前端Pako.js 压缩解压库 与 Java 的 zlib 压缩与解压 的互通实现
java·前端·javascript
浮游本尊15 分钟前
Nginx配置:如何在一个域名下运行两个网站
前端·javascript
新中地GIS开发老师23 分钟前
《Vue进阶教程》(12)ref的实现详细教程
前端·javascript·vue.js·arcgis·前端框架·地理信息科学·地信
巫师不要去魔法部乱说1 小时前
PyCharm专项训练4 最小生成树算法
算法·pycharm
Cachel wood1 小时前
Django REST framework (DRF)中的api_view和APIView权限控制
javascript·vue.js·后端·python·ui·django·前端框架
IT猿手1 小时前
最新高性能多目标优化算法:多目标麋鹿优化算法(MOEHO)求解GLSMOP1-GLSMOP9及工程应用---盘式制动器设计,提供完整MATLAB代码
开发语言·算法·机器学习·matlab·强化学习
阿七想学习1 小时前
数据结构《排序》
java·数据结构·学习·算法·排序算法
王老师青少年编程2 小时前
gesp(二级)(12)洛谷:B3955:[GESP202403 二级] 小杨的日字矩阵
c++·算法·矩阵·gesp·csp·信奥赛
放逐者-保持本心,方可放逐2 小时前
SSE 流式场景应用 及 方案总结
javascript·axios·fetch·eventsource