背景
最近笔者在看后端操作数仓跑数任务的时候,发现后端有一个很有意思的编排任务的页面,里面实现的功能就是,箭头后面的任务需要等它前面所有任务跑完才会跑当前任务,比如任务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])