一 图论概述

1 图的分类

1 无向图

图 graph由顶点和边组成,顶点的又穷非空集合为V,边的集合为E,记做G(V,E) 顶点vertex,数据元素的集合,顶点的集合,又穷非空, 边edge,数据元素关系的集合,顶点关系的集合,可以为空,边分为有向和无向两种

无向边记做(A,B),或者(B,A),使用小括号

无向图,记做undirected Graph 无向边的边构成的图,G=(V,E),V={A,B,C,D},E={(A,B),(A,C),(B,C),(B,D),(C,D)}

2 有向图

有向边记做<A,B>,即从顶点A指向顶点B,<B,A>表示顶点B指向顶点A,使用尖括号,有向边也叫做弧,边表示为弧尾指向弧头。

有向图directed graph

有方向的边组成的图

G=(V,E) V={A,B,C,D} E={<A,B>,<A,C>,<C,B>,<B,D>}

3 图的其他概念

1 稀疏图 sparse graph

图中边很少,最稀疏的情况是只有顶点没有边,这就是数据结构SET

2 稠密图 dense Graph

图中边很多,最稠密的情况,任意2个顶点之间都有关系

3 完全图 complete graph

包括了所有可能的边,达到了稠密图最稠密的情况,任意两个顶点之间都有边相连


有向的边的完全图,叫做有向完全图,边数为n*(n-1) 无向的边的完全图,叫做无向完全图,边数为n(n-1)/2

4 子图

如果图G(V,E)满足V` <=V,且E` <=E,则G`是G的子图

换句话说,就是一个图的部分顶点和部分边组成的图为子图,有向图需要注意边的方向

前面的图包含后面的图,后面的图可以称为前面的图的子图

一个图的部分顶点可能是所有顶点,其部分边也可能是所有边

有向图,边是由方向的,如果找不到对应的方向,则不是其子图,或者方向相反,则不是其子图

5 边的权weight和网

给边赋予的值称为权,权可以表示距离,所需的时间,耗费的时间等

网network 图中有边有权,图称为网

6 自环 loop

若一条边的两个顶点为同一个顶点,则此边称为自环

边中存在这样一个边(u,v) 或者<u,v> ,u=v

7 简单图

无重复的边或者顶点到自身的边(自环)的图

下面的两个图都不是简单图

4 邻接关联

1 邻接

图的边集合为E

无向图,若 (u,v) 属于 E,则称u和v相互邻接,互为邻接顶点

有向图,若<u,v>属于E,则边u邻接到v,或者v邻接于u

简单说,就是2点之间有条边,2点邻接

2 关联(依附)

若 (u,v)属于E或者 <u,v>属于E,则称边依附于顶点u,v或者顶点u,v 与边相关联。

5 路径path

1 基础路径

图G(V,E),其任意一个顶点序列,相邻2个顶点都能找到边或者弧依次链接,就说明有路径存在,有向图的弧注意方向,所有的顶点都属于V,所有的边都属于E。

顶点之间形成了路径,此处称为弧。

路径长度

等于顶点数减一,等于此路径上的边数

2 简单路径

路径上的顶点不重复出现,这两的路径就是简单路径

无向图中A到D的路径有A-B-D,A-C-D,A-C-B-D 等

有向图中A到D的路径有A-B-D,A-C-B-D等

3 回路

路径的起点和终点相同,称为回路 A-B-C-A-B-A

4 简单回路

除了路径的起点和终点相同外,其他顶点都不相同 A-B-C-A

6 连通

1 连通

无向图中,顶点存在路径,则两个顶点是连通的

注意: 连通是指A-D之间有路径,而不是说这两个个顶点要邻接

2 连通图

无向图中,如果图中任意两个顶点之间都连通,就是连通图

3 连通分量

无向图中,指的是极大连通子图

无向图未必是连通图,但是它可以包含连通子图

4 强连通

有向图中,顶点键存在2条相关的路径,及从A到B有路径,也存在从B到A的路径,两个顶点是强连通的

上述中第三个图中有强连接,如B-D和D-B,A-C-B和B-A

5 强联通图

有向图中,如果图中任意2个顶点都是强联通的图

6 强联通分量

有向图中,指的是"极大连通子图"

有向图未必是强联通图,但是可以包含强联通分量

7 度 degree

一个顶点的度指的是与该顶点相关联的边的条数,顶点v的度记做TD(v),无向图顶点的边数叫做度

有向图的顶点有入度和出度,顶点的度数为入度和出度之和 TD(v)=ID(v)+OD(v)

入度(In-degree): 一个顶点的入度是指与其关联的各个边中,以其为终点的边数

出度(Out-degree): 出度则是相对的概念,指以该顶点为起点的边数。

8 生成树

1 简单描述

它是一个极小连通子图,它要包含图的所有n个节点,但只只要有构成一颗数的n-1条边


如果一个图有n个顶点,且少于n-1条边,则一定是非连通图,因为至少要有n-1条边才行

如果一个图有n个顶点,且多于n-1条边,则一定有环存在,一定有2个顶点之间存在第二条路径,但不一定是连通图

如果一个图由n个顶点,且有n-1条边,但不一定是生成树,要整好等于n-1条边,且这些边足以构成一颗数

2 有向树

一个有向树恰好有一个入度为0的顶点,其他顶点的入度都为1,注意,这里不关心出度。

3 生成树森林

若干有向树构成有向树森林

有向无环树不一定能转化为数,但数一定是有向无环图。

二 邻接矩阵

1 概述

图是由vertex 和edge组成,所以可以分为2个数组表示 顶点使用一维数组表示,如v0,v1,v3 边使用二维数组表示,由顶点构成二维数组

2 无向邻接矩阵

下图中,若存在边。则为1,否则为0

| |A |B |C| D|| | -------- | -------- | | A | 0 | 1 | 1 |0| 2| | B|1|0|1| 1|3| |C|1|1|0|1|3| |D|0|1|1|0|2| ||2|3|3|2|||

此处的相关的最后行和最后一列表示度数,如果上述的对角线上的数字为1,则表示有了自环 如果除了对角线全是1,说明没有自环,且是一个无向完全图 上面的矩阵,称为图的邻接矩阵 顶点的度数,等于对应行或者列求和 邻接点,矩阵中为1的值对应的行与列的顶点就是邻接点。 无向图的邻接矩阵是一个对称矩阵

3 有向邻接矩阵

有向邻接矩阵因为是有向的,只有方向正确才是1,否则都是0

| | A | B | C | D || | -------- | -------- | -------- | | A | 1 | 1 | 1| 0| 2| |B| 0|0 |0|1| 1| |C| 0|1 | 0 |0 || |D|0 | 0 | 0| 0|| ||0|2|||||

有向图的邻接矩阵不一定对称,对称的说明两个顶点之间存在环

三 运维流程系统设计

1 概述

某一个基点上有一批任务需要执行,如何执行

一个接一个排队开始执行,但是这样的执行可能很没有效率,而且没必要,如获取两个毫不相关的信息,谁先执行都可以,同时执行也没问题,这样的任务便可以并行处理,而不只是串行化进行处理。

任务的执行过程中无非是串行和并行的问题,但串行的效率可能太低。

任务处理和任务流是没有关系的,任务的编排和任务分配不同

2 如何设计一个有向无环图(DAG)

1 有向无环图 directed acyclic graph (DAG)

无环路的有向图 假设有下面几种情况

两个任务,任务本身就是顶点,任务先后执行

三个任务。任务1执行完成后,才能分别执行任务2和任务3

四个任务,执行任务1完成后,才能分别执行任务2和任务3,最后执行任务4.执行任务4 的时机应该是任务2 and 任务3

可以看到任务的执行过程就是流程的设定(pipeline),所以要设计一个流程系统来跑任务

2 起点和终点选择

1 入度为0的顶点就是起始的点 DAG 可以有多个起始点 我们的系统约定有且只有一个起始点

终点的判断

出度为0的顶点,pipeline执行结束 pipeline可能有多个终点

3 环路预防

pipeline设计的过程中应当注意避免出现环路,因为出现环路就不是DAG了

自环检查,弧头指向顶点自身

多顶点构成环路的检测


环路检测必须实现,否则当定义好的流程执行起来,有可能进入环路后,永远执行不能终止。

3 schema 元数据表设计

使用数据库表的存储方式定义DAG 问题是如何使用数据库的表描述一个DAG DAG也是图,是图就有顶点,边,所以可以设计2个表,顶点表,边表,边表用于描述一个图,为了存储多个图,定义一个图的表

1 图的定义(graph)

字段名 类型 说明
id int 主键
name varchar 非空,唯一,图的名称
desc varchar 可为空,描述

2 顶点表定义 vertex

字段名 类型 说明
id int 主键
name varchar 非空,顶点的名称
g_id int 外键,描述其属于哪个图

3 边表 edge

字段名 类型 说明
id int 主键
tail int 外键,弧尾顶点,顶点在vertex 表中必须存在
head int 外键,弧头顶点,定在在vertex表中必须存在
g_id int 外键,描述边属于哪一个图

通过弧尾,弧头顶点来描述有向边

4 具体关系如下

其中,graph表中主要是流水线的名称,id和描述,顶点表(vertex)中主要包含顶点名称,顶点id及顶点所属的流水线,因为顶点必须和流水线之间建立关系,script 表示顶点要执行的脚本,及流水线要执行的脚本。

edge表中主要包括弧尾和弧头以及此顶点属于哪个表,用于描述流水线的执行顺序

4 script 的设计

1 设计思路

流程定义表中,任务的处理和描述

在任务调度系统中,任务的实现我们使用script脚本实现


方法一

supprocess 执行bash 脚本script

优点:简单,易行 缺点:要启动外部进程,bash 脚本表达能力较弱,难调试


方法二 嵌入其他语言的脚本,如lua语言 优点:不启动子进程,功能强大。 缺点:技术要求高,需要学习其他脚本语言。

2 python 中执行lua脚本

安装

pip   install lupa 
#!/usr/bin/poython3.6
#conding:utf-8
from  lupa import  LuaRuntime
lua=LuaRuntime()  #对其进行实例化处理
print  (lua.eval('1+10'))  #调用方法,执行基本的函数运算操作

def pythonc(n):   # 定义python方法
    import  socket
    print('socket',n)
    return   socket.gethostname()


# 定义lua 脚本函数,并传递两个值,一个是f,及函数,另一个则是常数
luafunc=lua.eval('''  
        function(f,n)
            return  f,n
        end
    ''')

print (luafunc(pythonc(1),10))  #调用函数并打印

add=lua.eval('''
    function  (x,y)
        return  x+y 
    end
''')

print (add(10,20))

结果如下

5 执行条件(input)

1 概述

脚本在执行之前,可能需要提供一些参数,才能开始执行脚本 此处需要在顶点表vertex中增加input字段,用于存储需要传递的参数。

2 表字段设计

字段名 类型 说明
id int 主键
name varchar 非空,顶点的名称
g_id int 外键,描述顶点属于哪一个图
script text 可以为空,存储任务脚本
input text 可以为空,存储json格式的输入参数定义

3 input 格式

定义如下,json 格式

{
    "name1" : {
        "type":"",
        "required" : True 
    },
    "name2" :{
        "type" :"",
        "requried": True,
        "default" :1
    }
    
}

name 就是参数的名称,后面定义该参数的类型,是否是必选参数等等属性及默认属性,其可以定义多个参数

4 作用

进入某个节点的时候,就必须满足条件,提供足够的参数 如果提供的参数满足要求,就进入节点,否则一直等待到参数满足 如果满足了,才能去执行script input 就是一个约束的定义


交互: input 可以不交互,缺省值为自动

6 任务执行

1 概述

当流程走到某一个顶点的时候,读取任务及脚本,执行这个脚本

2 手动执行和自动执行

1 手动执行

流程走到这个顶点等待用户操作,需要用户手动干预, 如由用户选择下一个执行顶点 如下一个顶点的任务需要一些配置参数,等待用户输入后才能进行下一步


2 自动执行 自动填写input,如使用缺省值,来满足用户为交互式填写的时候自动补全数据,脚本执行后,自动跳转到下一个节点,当然这个所谓的自动,程序不会智能的选择路径,需要提前指定好,执行完脚本,就可以跳转到下一个顶点了。

7 任务流转的设计

1 概述

当流程走到某一个顶点的时候,读取任务即脚本,或手动执行,或自动执行

2 流转分类

手动执行,需要人工选择下一个顶点,可以提供可视化界面供用户方便选择,


自动执行,就需要在信息中提供下一个节点的信息,供程序自动完成


那么,如何区分一个顶点是否自动执行

如果vertex表中的script字段修改为json. 如果next 不存在,则不能自动执行,需要手动操作 如果next存在,则程序自动跳转

3 消息格式

{
    "script" :"echo  test"
}

{
    "script" :"echo  test",
    "next" : 'B'  # 填写下一跳为顶点名称
}

{
    "script" : "echo test",
    "next" : 2  # 填写下一跳为顶点id
}

为了方便用户,next可以提供2种类型的参数: 1 int 表示vertex的id 2 str 表示使用vertex的name,但是是同一个graph id, 同一个DAG的定义中名字不能冲突,所以可以用。

8 流程结束

如果一个顶点的出度为0,则此节点为终点。

如何判断出度为0. 在edge表中,使用当前节点的顶点id作为弧尾,找不到弧头h的任何记录。

9 执行引擎之pipeline 设计

1 概述

前面的设计仅仅是流程DAG定义,流程真正执行的时候需要记录执行这个流程的任务流的数据,

2 表pipeline

字段名 类型 说明
id int 主键
g_id int 外键,指明使用的是哪一个流程DAG定义
current int 外键,顶点id,表示当前走到哪一个节点

这个表以后还要添加其他字段,存储一些附加信息。如谁加入的流程,执行时间等。


起点的选择,通过查询edge表来确定起点位置,当入度为0的点则是起点,通过顶点表和边表来进行处理


一个pipeline应该指向哪一个DAG,并选择DAG的起点,因为DAG 可能存在多个起点,即入度为0的顶点,需要指定,然后把这些信息记录在pipeline表中,current为起点顶点的id,提取current 顶点的input信息,用户输入满足了,才能执行script脚本


不管是手动执行还是自动执行,如果到了下一个节点,需要修改current字段的值,

任务流执行完毕,修改最后一个节点的状态为完成

3 举例

当前节点任务是打包,调用maven命令执行打包,先要提取inout,要求用户输入ip地址,输出目录等信息。然后才能执行打包脚本。

10 执行引擎历史轨迹设计

1 概述

pipeline表只能看到有哪些流正在运行,但是究竟走了DAG中的那些节点,不清楚,执行节点前输入了那些参数也是不清楚的

如何查询,回溯当前的pipeline的运行轨迹

2 track 表

字段名 类型 说明
id int 主键
p_id int 外键,哪一个流程的历史
v_idint 外键,顶点的ID,经历过的历史节点
input text 可以为空,输入的参数值
output text 可以为空,任务的输出

3 状态设计

在pipeline表,track表中增加state字段,用于描述在某个节点上执行的状态,是等待中,还是正在运行,还是成功或者失败,还是执行完毕。 STATE_WAITING=0 STATE_RUNNING=1 STATE_SUCCEED=2 STATE_FAILED=3 STATE_FINISH=4

11 最终设计模型

左边主要针对的是基础设计,右边主要针对的是引擎层面的设计,流程看似一样,但其实际是不同的

12 DAG 检测

1 DFS 算法

DFS ( depth first search)深度优先遍历,递归算法 需要改进算法适用于有向图 不能直接检测有向图是否有环

2 拓扑排序算法

拓扑排序就是把有向图中的顶点以线性方式排序,如果有弧<u,b>,则最后线性排序的结果,顶点u总是在顶点b的前面

一个有向图能被拓扑排序的充要条件是: 它必须是DAG

3 kahr算法

1 选择一个入度为0的顶点并输出它 2 删除以此顶点为弧尾的弧

重复上面2步,直到输出全部顶点为止,或者图中不存在入度为0的顶点为止。

实例如下

第一步,找到入度为0的点A,然后删除A和以A为弧尾的边,第二步,找到入度为0的C,删除顶点C和以C为弧尾的边,第三步,找到B,删除顶点B,并删除以B为弧尾的边,最后,删除入度为0的顶点D

上面2个图都不是DAG,左图一个环,右图2个环 这2个图都找不到入度为0的起始点,都不是DAG

上图中虽然能找到入度为0的顶点,但是移除它和关联的边,剩下的顶点找不到入度为0的顶点,其不是DAG

四 项目配置流程系统

1 概述

本项目使用service层和module层进行配置和处理工作,其中,service层主要处理相关数据,而module层的作用则是创建和操作数据库配置

2 项目创建

1 虚拟目录创建

mkdir pipeline 

cd pipeline/

pyenv  virtualenv  3.5.3  pipe 

pyenv local pipe

2 数据库相关配置

create database  pipeline charset utf8mb4;

grant all on  pipeline.*  to  pipe@localhost  identified by 'pipe';

flush privileges;

2 model层创建

1 概述

数据持久化最终的结果是需要通过关系数据库的表达来实现的

2 配置文件创建

创建config文件,用于存储数据库配置信息

#!/usr/bin/poython3.6
#conding:utf-8

USERNAME="pipe"
PASSWORD="pipe"
DBIP="localhost"
DBPORT=3306
DBNAME="pipeline"
PARAMS="charset=utf8mb4"
URL="mysql+pymysql://{}:{}@{}:{}/{}?{}".format(USERNAME,PASSWORD,DBIP,DBPORT,DBNAME,PARAMS)
DATABASE_DEBUG=True

3 创建模型模板

用于存储和封装数据类

添加插件sqlachemy 和 pymysql

pip install  sqlalchemy pymysql

model.py 内容如下

#!/usr/local/bin/python3.6
#coding:utf-8
# @Time    : 2019/11/27 11:18
# @Author  : ZhangBing
# @Email   : 18829272841@163.com
# @File    : model.py
# @Software: PyCharm

from pipeline.config import  URL,DATABASE_DEBUG
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String,ForeignKey,Text
from sqlalchemy.orm import relationship
from sqlalchemy.orm import sessionmaker


STATE_WAITING=0
STATE_RUNNING=1
STATE_SUCCEED=2
STATE_FAILED=3
STATE_FINISH=4

Base=declarative_base()
# 创建图,用于存储pipeline 流水
class  Graph(Base):
    __tablename__='graph'
    id=Column(Integer,primary_key=True,autoincrement=True)
    name=Column(String(48),nullable=False)
    desc=Column(String(128))

    # 创建relationship关系图,用于配置,此处的含义是可以使用此关系来处理查看此pipe下的顶点和边的信息
    vertexs=relationship("Vertex")
    edges=relationship("Edge")


# 创建顶点表,用于存储顶点信息
class  Vertex(Base):
    __tablename__='vertex'
    id=Column(Integer,primary_key=True,autoincrement=True)
    name=Column(String(48),nullable=False)
    graph_id=Column(Integer,ForeignKey('graph.id')) # 配置外键,用于
    input=Column(String(128),nullable=True)
    script=Column(Text,nullable=True)
    graph=relationship("Graph")
    # 从顶点看边,一个顶点对应两个边,肯定会出现问题,因此此处使用此方式来指定到对应的边,此处必须使用引号,否则会导致报错
    tails=relationship("Edge",foreign_keys="[Edge.tail]")
    heads=relationship("Edge",foreign_keys="Edge.head")

# 创建边表,用户存储边的相关信息
class  Edge(Base):
    __tablename__='edge'
    id=Column(Integer,primary_key=True,autoincrement=True)
    tail=Column(Integer,ForeignKey('vertex.id'),nullable=False)
    head=Column(Integer,ForeignKey('vertex.id'),nullable=False)
    graph_id=Column(Integer,ForeignKey('graph.id'),nullable=False)
# 用于记录当前流水线执行到那块了,自然需要节点信息

# 此处用于查看流水线的执行情况,如再那个流水线的那个节点。执行的结果状态如何
class Pipeline(Base):
    __tablename__='pipeline'
    id=Column(Integer,primary_key=True,autoincrement=True)
    current=Column(Integer,ForeignKey('vertex.id'))
    graph_id = Column(Integer, ForeignKey('graph.id'))
    state=Column(Integer,nullable=False,default=STATE_WAITING)
    vertex=relationship("Vertex")

# 此处用于创建记录表,此表必须和pipeline进行联系,并和顶点表俩西,获取对应的顶点的最终执行信息
class  Track(Base):
    __tablename__='track'
    id = Column(Integer, primary_key=True, autoincrement=True)
    pipeline_id=Column(Integer,ForeignKey('pipeline.id'))
    vertex_id=Column(Integer,ForeignKey('vertex.id'))
    input=Column(Text,nullable=True)
    output=Column(Text,nullable=True)
		state=Column(Integer,nullable=False,default=STATE_WAITING)  # 用于记录该节点是否执行任务成功

    vertex=relationship("Vertex")
    pipeline=relationship("Pipeline")

# 此处用于创建存储引擎
class  DBcreate:
    def  __init__(self):
        # 初始化时创建引擎
        self.__engine=None
        self.__session=None
        self.flag=False
    def  db_init(self,DB_URL,DATABASE_DEBUG):
        if not  self.flag:
            self.__engine = create_engine(DB_URL, echo=DATABASE_DEBUG)
            # 创建会话
            self.__session = sessionmaker(bind=self.__engine)()
            self.flag = True
        return  self
    @property
    def  session(self):
        if not  self.flag:
            raise   ArithmeticError("Not  initialized")
        return self.__session
    @property
    def  engine(self):
        if not self.flag:
            raise ArithmeticError("Not  initialized")
        return  self.__engine
    def  db_create(self):  # 创建表
        Base.metadata.create_all(self.__engine)
    def  db_delete(self):  # 删除表
        Base.metadata.drop_all(self.__engine)

db=DBcreate().db_init(URL,DATABASE_DEBUG)  # 通过此处的实例化实现了向外创建的目的 

db.db_create()  # 创建表 

if __name__ == "__main__":
    pass

运行结果如下

创建数据库如下

2 service 层处理

1 需求

1 定义DAG,及就是schema定义 2 执行某一个DAG流程

2 问题

DAG 是否允许修改

可以这样考虑。如果DAG 定义好还未使用,可以进行修改操作,一旦使用过,则不能修改,因此便需要在图graph表中增加一个字段用于区分是否是执行过的pipeline

3 DAG 定义

#!/usr/bin/poython3.6
#conding:utf-8
from .model import   db
from  .model import   Graph,Vertex,Edge
from  .model import   Pipeline,Track

# 创建 DAG
def  create_graph(name,desc=None): # 此处用于创建图表,主要需要传递的参数是name和desc 描述信息
    g=Graph()
    g.name=name
    g.desc=desc

    db.session.add(g)

    try:
        db.session.commit()
        return  g
    except:
        db.session.rollback()
# 创建顶点表
def  add_vertex(graph:Graph,name,input=None,script=None):
    v=Vertex()
    v.graph_id=graph.id
    v.name=name
    v.script=script
    v.input=input
    db.session.add(v)
    try:
        db.session.commit()
        return   v
    except:
        db.session.rollback()

# 创建边表,用存储边的数据信息
def add_edge(graph:Graph,tail:Vertex,head:Vertex):
    e=Edge()
    e.graph_id=graph.id
    e.tail=tail.id
    e.head=head.id
    db.session.add(e)
    try:
        db.session.commit()
        return  e
    except:
        db.session.rollback()

def del_vertex(id):  # 通过顶点表的id来删除相关的信息,需要删除顶点和顶点对应的边的信息
    query=db.session.query(Vertex).filter(Vertex.id==id)
    v=query.first()
    if  v: # 找到顶点。删除相关的边,然后删除顶点
        try:
            db.session.query(Edge).filter((Edge.tail==v.id)  | Edge.head==v.id).delete()  # 删除对应的边
            query.delete()  # 删除顶点
            db.session.commit()  # 提交。若失败,则回滚
        except:
            db.session.rollback()
        return  v

使用装饰器处理上述提交问题,避免繁琐,创建util 文件,用于处理数据库数据的提交和回撤问题

#!/usr/bin/poython3.6
#conding:utf-8

from  pipeline.model import db
from functools import  wraps

def transactional(fn):
    @wraps(fn)
    def __wapper(*args,**kwargs):
        ret=fn(*args,**kwargs)
        try:
            db.session.commit()
            return  ret
        except:
            db.session.rollback()
    return  __wapper

if __name__ == "__main__":
    pass

4 service 层修改结果如下

#!/usr/bin/poython3.6
#conding:utf-8
from .model import   db
from  .model import   Graph,Vertex,Edge
from  .model import   Pipeline,Track
from  util import transactional

# 创建 DAG
@transactional
def  create_graph(name,desc=None): # 此处用于创建图表,主要需要传递的参数是name和desc 描述信息
    g=Graph()
    g.name=name
    g.desc=desc
    db.session.add(g)
# 创建顶点表
    return   g
@transactional
def  add_vertex(graph:Graph,name,input=None,script=None):
    v=Vertex()
    v.graph_id=graph.id
    v.name=name
    v.script=script
    v.input=input
    db.session.add(v)
    return  v

# 创建边表,用存储边的数据信息

@transactional
def add_edge(graph:Graph,tail:Vertex,head:Vertex):
    e=Edge()
    e.graph_id=graph.id
    e.tail=tail
    e.head=head
    db.session.add(e)
    return  e

def del_vertex(id):  # 通过顶点表的id来删除相关的信息,需要删除顶点和顶点对应的边的信息
    query=db.session.query(Vertex).filter(Vertex.id==id)
    v=query.first()
    if  v: # 找到顶点。删除相关的边,然后删除顶点
        try:
            db.session.query(Edge).filter((Edge.tail==v.id)  | Edge.head==v.id).delete()  # 删除对应的边
            query.delete()  # 删除顶点
            db.session.commit()  # 提交。若失败,则回滚
        except:
            db.session.rollback()
        return  v

3 DAG 验证和测试数据处理

1 测试数据

测试数据函数,暂时放置在service.py中进行处理

```
def  test_create_dag():
try:
    g=create_graph('test1')  # 此处成功返回一个graph对象
    # 增加节点
    input='''
        {
            "ip" :{
                "type" :"str",
                "required" :"true",
                "default" : 192.168.1.200
                
            }
        }
    '''
    script={
        "script" : "echo  test1.A",
        'next' : 'B'
        }
    # 此处可以设置为为了用户方面,next可以设置接收两种类型,数字表示顶点的id,字符串表示用一个DAG中的该名称的顶点,其不能重复

    a=add_vertex(g,'A',None,json.dumps(script))  # 对数据进行处理
    b=add_vertex(g,'B',None,'echo  B')
    c=add_vertex(g,'C',None,'echo C')
    d=add_vertex(g,'D',None,'echo D')
    # 增加边
    ab=add_edge(g,a,b)
    ac=add_edge(g,a,c)
    cb=add_edge(g,c,b)
    bd=add_edge(g,b,d)


    # 创建环路

    g=create_graph('test2') # 环路

    # 增加顶点
    a = add_vertex(g,'A',None,'echo A')
    b = add_vertex(g, 'B', None, 'echo  B')
    c = add_vertex(g, 'C', None, 'echo C')
    d = add_vertex(g, 'D', None, 'echo D')
    # 增加边。abc之间环路
    ba=add_edge(g,b,a)
    ac=add_edge(g,a,c)
    cb=add_edge(g,c,b)
    bd=add_edge(g,b,d)


    # 创建DAG
    g=create_graph('test3')  # 多个顶点

    # 增加顶点
    a = add_vertex(g, 'A', None, 'echo A')
    b = add_vertex(g, 'B', None, 'echo  B')
    c = add_vertex(g, 'C', None, 'echo C')
    d = add_vertex(g, 'D', None, 'echo D')
    # 增加边

    ba=add_edge(g,b,a)
    ac=add_edge(g,a,c)
    bc=add_edge(g,b,c)
    bd=add_edge(g,b,d)

    # 多起点处理方式
    g = create_graph('test4')  # 多个顶点

    # 增加顶点
    a = add_vertex(g, 'A', None, 'echo A')
    b = add_vertex(g, 'B', None, 'echo  B')
    c = add_vertex(g, 'C', None, 'echo C')
    d = add_vertex(g, 'D', None, 'echo D')
    # 增加边

    ab = add_edge(g, a, b)
    ac = add_edge(g, a, c)
    cb = add_edge(g, c, b)
    db = add_edge(g, d, b)
except  Exception as  e:
    print (e)

```

2 执行写入测试数据

创建app文件

执行结果如下

3 DAG 验证概述

当增加一个DAG定义后,或者修改了DAG定义后,就需要对DAG 进行验证,判断是否是一个DAG图,如何知道写入的数据库的数据是有效的,则需要通过在graph表中增加一个checked字段,用于判断是否是通过验证的,在以后的创建流程操作中,若检测到其字段为0时,则表示其未通过DAG 验证


注意:如果有一个流程使用了这个DAG,其将不被允许修改没了实现这个功能,且不要每一次都查询一下这个DAG是否被使用,可以在graph表中提供一个字段sealed,一旦设置就不能修改和删除,表示有人使用了,


在DAG定义后,修改后,就立即进行DAG检验,这样使用的时候就不用每次都检验。


4 DAG 设计表结构结果

字段名 类型 说明
id int 主键
name varchar 非空,唯一,图的名称
desc varchar 可为空,描述信息
checked int 流程检验数据
sealed int 不可为空,默认为0,0表示未使用,1表示已经有执行流程使用了,被封闭不可修改

结果如下

class  Graph(Base):
    __tablename__='graph'
    id=Column(Integer,primary_key=True,autoincrement=True)
    name=Column(String(48),nullable=False)
    desc=Column(String(128),nullable=False)
    checked=Column(Integer,nullable=False,default=0)
    sealed=Column(Integer,nullable=False,default=0)
    
    # 创建relationship关系图,用于配置,此处的含义是可以使用此关系来处理查看此pipe下的顶点和边的信息
    vertexs=relationship("Vertex")
    edges=relationship("Edge")

重新处理表结构,结果如下

查询所有入度为0的顶点

SELECT  vertex.*  FROM  vertex  LEFT JOIN  edge  on    vertex.id=edge.head  WHERE  vertex.graph_id=1   AND  edge.head  is  NULL  

采用做链接找到edge中的null的方式,找到入度为0的顶点。 但这种方式找到的不适合进行验证,因为第一批入度为0的顶点找到之后,还需要再次查询,找到第二批顶点。其可以将所有的顶点,边都先查询一遍,然后再客户端数据库中进行相关的处理

5 函数创建

在service.py中进行创建此函数

def check_graph(graph:Graph):
    query=db.session.query(Vertex).filter(Vertex.graph_id==graph.id)
    vertexs=[vertex.id  for  vertex  in  query]  # 获取顶点列表
    query=db.session.query(Edge).filter(Edge.graph_id==graph.id)
    edges=[(edge.tail,edge.head)  for  edge  in query ]  # 此处获取边的列表
    while True:
        vis=[]  #存放索引,
        for  i,v  in enumerate(vertexs):  # 此处需要对顶点表中的每一个顶点和边表中的head进行匹配
            for _,h  in edges:
                if  h==v:  # 此处的v表示顶点的id,此处的h表示head及弧头的数据,若相等,则表示其顶点有弧头,则表示入度不为0,此处判断失败
                    break
            else:  # 此处表示其顶点表中的和边表中head的没有匹配的情况,及入度为0的顶点
                ejs=[]
                for  j,(t,_)  in enumerate(edges):  # 此处是处理弧尾的情况,
                    if t==v:  # 此处是弧尾和顶点相等的情况,则将其加入到对应的ejs中,弧尾相等。则表示其可以形成边的关系
                        ejs.append(j)  # 增加其顶点到对应的边关系中
                vis.append(i)  # 增加其边的索引到列表汇总
                for j in reversed(ejs):  # 删除列表中的边对应的索引。及删除和入度为0的顶点对应的边
                    edges.pop(j)
                break
        else: # 若遍历所有都没有找到入度为0的顶点,表明其本身就有环
            return  False
        for  i  in vis:
            vertexs.pop(i)
        if len(vertexs)  +  len(edges) ==0:  #此处为0,表示删除完成,则为DAG
            try:
                graph=db.session.query(Graph).filter(Graph.id==graph.id).first()
                if graph:
                    graph.checked=1 # 修改和更新状态
                db.session.add(graph)
                db.session.commit()
            except  Exception as e:
                db.session.rollback()
                raise e

执行在app中进行。如下

结果如下

4 流程的启动和inut 验证和执行器

1 执行引擎

起点 开启一个流程的时候,需要在界面中选择一个checke为1的DAG,选择一个顶点,顶点的选择,使用前面的左连接sql语句可以列出当前DAG中入度为0的节点作为初始顶点,供用户选择一个作为起点


将起点信息写入pipeline表,Track表,状态State都是WAITING

pipeline 表示正在执行的节点 track记录历史信息,当前还没有input 具体值的信息

2 执行引擎代码如下

def start(graph:Graph,vertex:Vertex,params=None):
    # 判断流程是否存在,且checked为1的则通过检验
    g=db.session.query(Graph).filter(Graph.id==graph.id).filter(Graph.checked==1).first()
    if not g:
        return
    v=db.session.query(Vertex).filter((Vertex.id==vertex.id) & (Vertex.graph_id==graph.id)).first()
    if not v:
        return
    # 写入pipeline 表
    p=Pipeline()
    p.current=v.id
    p.graph_id=g.id
    p.state=STATE_WAITING
    db.session.add(p)
    try:
        db.session.commit()
    except:
        db.session.rollback()
    t=Track()
    t.pipeline_id=p.id
    t.vertex_id=v.id
    t.state=STATE_WAITING
    db.session.add(t)
    try:
        db.session.commit()
    except:
        db.session.rollback()
    if g.sealed==0:
        g.sealed=1
        db.session.add(g)
    return  p

测试如下

def  test_start():
    g=Graph()
    g.id=1
    v=Vertex()
    v.id=1
    p=start(g,v)
    if p:
        print (p)
        print (p.vertex.script)

test_start()

结果如下

3 input 验证

开启一个流程后,起点可能会设置input,这时候就需要有一个界面,让用户填写参数,这是一个交互的过程,也可以实现为自动填写参数

提取起点的input参数并进行验证,验证通过,将输入值,保存到字典中,将值存入track表,将字典中的input值交给执行器完成,此处功能在mschedule项目中已经实现,此处不再累赘

4 执行器

使用Input获得字典,对script字段中的脚本进行替换。若是空字典,就直接执行脚本,启动线程,使用subprocess的Popen开启子进程执行,返回的结果保存到track的output中,判断成功失败,如果成功,则置状态为成功,继续,若失败,则置状态为失败,流程停止

### 执行器配置
from subprocess  import  Popen,PIPE
def execute(script,timeout=None):
    proc=Popen(script,shell=True,stdout=PIPE)
    code=proc.wait(timeout)
    txt=proc.stdout.read()
    return  code,txt

5 流转

1 概述

设想一种自动化执行流程,先不考虑input的交互步骤,假设没有input环节

1 用户选择了起点之后,如何开始执行脚本?

如果用户提交了一个起点后,start函数开始执行,就一直执行到最后一个节点,这个函数才退出,可以,但其存在问题

也就是用户提交起点后,起点脚本执行是需要时间的,这时候异步执行可能是一个好的方式,开启线程,专门负责从数据库的pipeline表中读取所有WAITING的节点,执行script,获取返回结果


2 如何流转

脚本执行成功,需要流转到下一个节点

2.1 终点 如果本节点的出度为0,就是终点了,将pipeline,track中的字段置位完成

2.2 自动选择 在脚本json中执行了next,把pipeline中current字段更新为最新的顶点id,状态为等待,track表中的原来的顶点id的状态修改为成功,新增一条跟踪顶点id记录,状态是等待。

2 异步执行

此处具体代码参考如下

https://blog.51cto.com/11233559/2432516

#!/usr/bin/poython3.6
#conding:utf-8
from concurrent.futures  import  ThreadPoolExecutor,as_completed
import  random
import  threading

def test_fun(s,key):
    print ("enter~~~~~~~~{}  {}s key={}".format(threading.current_thread(),s,key))
    threading.Event().wait(s)
    return  "ok {}".format(threading.current_thread())


with ThreadPoolExecutor(max_workers=3)  as  executor:
    futures={executor.submit(test_fun,random.randint(1,8),i):i  for i in range(7)}

    for future in as_completed(futures):
        id=futures[future]
        try:
            print (id,future.result())
        except  Exception as e:
            print (id,'failed')

结果如下

3 流转代码如下


from  concurrent.futures  import  ThreadPoolExecutor,as_completed

MAX_POOL_SIZE=5

executor=ThreadPoolExecutor(max_workers=MAX_POOL_SIZE)

def  iter_pipelines():# 此处可修改成yield  from
    query=db.session.query(Pipeline).filter(Pipeline.state==STATE_WAITING)
    pipelines= query.all()
    for pipeline  in  pipelines:
        yield  pipeline

#流转
def   shift():
    futures={}
    for pipeline  in  iter_pipelines():
        s=json.loads(pipeline.vertex.script) # 脚本的处理
        script=s['script'] # 拿到脚本
        f=executor.submit(execute,script)  # 送入函数和对应的参数
        futures[f]=pipeline,s

        for  f  in as_completed(futures):
            p,s=futures[f]  # 遍历相关对象

            try:
                code,txt=f.result()
                print (code,txt,p.current)
                if code==0:  # 基本正常,此处返回为0表示正常情况
                    t=db.session.query(Track).filter((Track.pipeline_id==p.id) & (Track.vertex_id==p.current())).one()
                    t.state=STATE_SUCCEED
                    t.out=txt
                    db.session.add(t)
                    if 'next'  in s: # 是否存在下一跳
                        n=s['next'] # 获取下一跳
                        if  type(n)==int:  # 此处对应的是执行下一个节点id对应的服务
                            pass
                        else:  # 此处是对应的next为name的情况
                            pass
                        p.current=next  # 更新节点数据
                        p.state=STATE_WAITING # 下一个节点的状态应该是正常
                        db.session.add(p)
                    else:  # 此处未进行处理,则需要通过外部点击的方式完成
                        p.state=STATE_SUCCEED
                        db.session.add(p)
                else:
                    pass
            except  Exception as e:
                print (e,'!!!!!!!!!!!!!!')