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])
相关推荐
软件派1 小时前
基于YOLO算法的目标检测系统实现指南
算法·yolo·目标检测
flying robot1 小时前
js在浏览器执行原理
开发语言·javascript·ecmascript
熊猫钓鱼>_>3 小时前
建筑IT数字化突围:建筑设计企业的生存法则重塑
前端·javascript·easyui
代码小将3 小时前
Leetcode209做题笔记
java·笔记·算法
Musennn4 小时前
leetcode 15.三数之和 思路分析
算法·leetcode·职场和发展
GISer_Jing5 小时前
前端性能指标及优化策略——从加载、渲染和交互阶段分别解读详解并以Webpack+Vue项目为例进行解读
前端·javascript·vue
CM莫问7 小时前
<论文>(微软)避免推荐域外物品:基于LLM的受限生成式推荐
人工智能·算法·大模型·推荐算法·受限生成
康谋自动驾驶8 小时前
康谋分享 | 自动驾驶仿真进入“标准时代”:aiSim全面对接ASAM OpenX
人工智能·科技·算法·机器学习·自动驾驶·汽车
C++ 老炮儿的技术栈8 小时前
什么是函数重载?为什么 C 不支持函数重载,而 C++能支持函数重载?
c语言·开发语言·c++·qt·算法
&白帝&9 小时前
vue右键显示菜单
前端·javascript·vue.js