This commit is contained in:
yumoqing 2026-03-12 18:05:08 +08:00
parent 172b7115cb
commit 34ac11fa71
6 changed files with 190 additions and 142 deletions

View File

@ -127,12 +127,13 @@ output_ctx使用节点执行结果的字典数据作为数据来渲染结果u
* done 执行成功状态 * done 执行成功状态
* failed 执行失败状态 * failed 执行失败状态
* cancelled 取消状态 * cancelled 取消状态
* completed 处理完成
状态变化: 状态变化:
* pending 转running 如果节点没有join属性在下个循环直接转running 并调用节点执行方法,记录开始执行时间 * pending 转running 如果节点有join属性检查join条件满足条件就转执行否则等条件满足 没有join属性则直接转running 并调用节点执行方法,记录开始执行时间, 手工节点则由人工点击工作开始转换
* running zhuan done 节点执行成功,并记录完成时间 * running zhuan done 节点执行成功,并记录完成时间
* running 转 failed 节点执行失败, 并记录完成时间 * running 转 failed 节点执行失败, 并记录完成时间
* pending 转 cancel 取消执行(手工或流程实例被取消) * pending 转 cancel 取消执行(手工或流程实例被取消)
* done cancelled, failed 转 completed 检查从节点出发的边,做节点转移
--- ---
### 3.4 subflow_instance ### 3.4 subflow_instance

View File

@ -12,6 +12,7 @@ Features:
- Query APIs for UI - Query APIs for UI
""" """
import asyncio import asyncio
from random import randint
from appPublic.log import debug from appPublic.log import debug
from appPublic.timeUtils import timestampstr from appPublic.timeUtils import timestampstr
from sqlor.dbpools import get_sor_context from sqlor.dbpools import get_sor_context
@ -134,25 +135,28 @@ class FlowEngine:
# ------------------------------ # ------------------------------
# Definition / Instance # Definition / Instance
# ------------------------------ # ------------------------------
def __init__(self): def __init__(self, backid):
self.aio = [] self.aio = []
self.backid = backid
async def bgtask(self): async def bgtask(self):
env = ServerEnv() env = ServerEnv()
self.aio = [] self.aio = []
cnt = 0 cnt = 0
while True: while True:
aio = []
async with get_sor_context(env, 'dagflow') as sor: async with get_sor_context(env, 'dagflow') as sor:
self.aio = await sor.R('flow_instance', {'status': 'running'}) sql = """select * from flow_instance
if cnt >= 60: where backid=${backid}$
debug(f'{self.aio=}') and status = 'running'"""
cnt = 0 aio = await sor.sqlExe(sql,{
for r in self.aio: 'backid': self.backid
self.step(r.org_id, r.id) })
await asyncio.sleep(2) for r in aio:
cnt += 1 self.step(r)
await asyncio.sleep(1)
async def create_definition(self, org_id, name, version, dsl_text): async def create_definition(self, org_id, name, description, version, dsl_text, ctxfields):
env = ServerEnv() env = ServerEnv()
async with get_sor_context(env, 'dagflow') as sor: async with get_sor_context(env, 'dagflow') as sor:
fid = env.uuid() fid = env.uuid()
@ -160,7 +164,10 @@ class FlowEngine:
'id': fid, 'id': fid,
'org_id': org_id, 'org_id': org_id,
'name': name, 'name': name,
'description': description,
'version': version, 'version': version,
'ctxfields': json.dumps(ctxfields),
'created_at': timestampstr(),
'dsl': dsl_text 'dsl': dsl_text
}) })
return fid return fid
@ -169,21 +176,51 @@ class FlowEngine:
env = ServerEnv() env = ServerEnv()
async with get_sor_context(env, 'dagflow') as sor: async with get_sor_context(env, 'dagflow') as sor:
iid = env.uuid() iid = env.uuid()
await sor.C('flow_instance', { ns = {
'id': iid, 'id': iid,
'backid': self.backid,
'round': 1,
'org_id': org_id, 'org_id': org_id,
'flow_def_id': flow_def_id, 'flow_def_id': flow_def_id,
'status': 'running', 'status': 'running',
'ctx': json.dumps(ctx or {}), 'ctx': json.dumps(ctx or {}),
'active_nodes': json.dumps([]) 'created_at': timestampstr()
}) })
await sor.C('flow_instance', ns.copy())
await self.create_start_node_execution(sor, ns)
return iid return iid
async def create_start_node_execution(self, sor, inst):
env = ServerEnv()
id = env.uuid()
await sor.C('node_execution', {
'id': id,
'type': 'start',
'instance_id': inst['id'],
'org_id': inst['org_id'],
'inst_round': inst['round'],
'status': 'done',
'node_id': 'start',
'created_at': timestampstr()
})
async def create_end_node_execution(self, sor, inst):
env = ServerEnv()
id = env.uuid()
await sor.C('node_execution', {
'id': id,
'type': 'end',
'instance_id': inst['id'],
'org_id': inst['org_id'],
'inst_round': inst['round'],
'status': 'pending',
'node_id': 'end',
'created_at': timestampstr()
})
# ------------------------------ # ------------------------------
# Execution # Execution
# ------------------------------ # ------------------------------
async def step(self, org_id, instance_id): async def step(self, inst):
""" """
从节点执行表中取出所有状态不是结束的执行节点检查是否可以推进一步 从节点执行表中取出所有状态不是结束的执行节点检查是否可以推进一步
结束状态为 结束状态为
@ -193,17 +230,6 @@ class FlowEngine:
推进如果 推进如果
env = ServerEnv() env = ServerEnv()
async with get_sor_context(env, 'dagflow') as sor: async with get_sor_context(env, 'dagflow') as sor:
insts = await sor.R('flow_instance', {
'id': instance_id,
'org_id': org_id
})
if not insts:
return
inst = insts[0]
if inst['status'] != 'running':
return
flow_def = (await sor.R( flow_def = (await sor.R(
'flow_definition', 'flow_definition',
{'id': inst['flow_def_id']} {'id': inst['flow_def_id']}
@ -213,85 +239,78 @@ class FlowEngine:
ctx = json.loads(inst['ctx']) ctx = json.loads(inst['ctx'])
active = set(json.loads(inst['active_nodes']) or [dsl['start']]) active = set(json.loads(inst['active_nodes']) or [dsl['start']])
next_nodes = set() next_nodes = set()
sql = "select * from node_execution where instance_id=${instance_id}$ and status != 'completed'"
for node_id in active: active_nes = await sor.sqlExe(sql, {
'instance_id': inst.id
})
for ne in active_nes:
node_id = ne.node_id
node = dsl['nodes'][node_id] node = dsl['nodes'][node_id]
node['id'] = node_id
ntype = node['type'] ntype = node['type']
status = ne.status
# -------- Human node -------- if status == 'pending':
if ntype == 'human': if ntype == 'human':
rows = await sor.R('human_task', { if ne.assignee is None:
'instance_id': instance_id, await self.auto_assign(sor, inst, ne, node)
'node_id': node_id, elif ntype == 'task':
'status': 'pending' await self.try_start_running(sor, inst, ne, node)
}) elif ntype == 'subflow':
if not rows: await self.try_start_subflow(sor, inst, ne, node)
await sor.C('human_task', { elif ntype == 'start':
'id': env.uuid(), await self.node_transfer(sor, inst, ne, node, flow_def)
'org_id': org_id, await sor.U('node_execution', {
'instance_id': instance_id, 'id': ne.id,
'node_id': node_id, 'status': 'completed'
'role': node.get('role'),
'status': 'pending',
'input': json.dumps(ctx),
'timeout_at': env.after(node.get('timeout', 0))
}) })
next_nodes.add(node_id) elif ntype == 'end':
continue flg = await self.is_ok_to_step_next(sor, node, edges, inst)
if ntype == 'task': if flg:
if node.get('join'): await sor.U('node_execution', {
await sor.C('node_execution', { 'id': ne.id,
'id': env.uuid(), 'status': 'completed'
'org_id': org_id, })
'instance_id': instance_id, await sor.U('flow_instance', {
'node_id': node_id, 'id': inst.id,
'input_ctx': json.dumps(ctx), 'status': 'completed'
'status': 'pendding' })
break;
elif status in ['done', 'cancelled', 'failed']:
await self.node_transfer(sor, inst, ne, node, flow_def)
await sor.U('node_execution', {
'id': ne.id,
'status': 'completed'
}) })
self.check_instance_completed(sor, inst)
# ---- 如果节点状态在pending状态 ---
if node['status'] == 'pending' and ntype != 'human':
is_ok = await self.is_ok_to_start(sor, node, inst)
if is_ok:
await self.start_running(sor, node)
continue
# -------- Normal node --------
"""
await sor.C('node_execution', {
'id': env.uuid(),
'org_id': org_id,
'instance_id': instance_id,
'node_id': node_id,
'input_ctx': json.dumps(ctx),
'status': 'pendding'
})
"""
if nstatus == 'done' or nstatus == 'failed':
n_nodes = await self.node_transfer(node, dsl)
next_nodes += n_nodes
if next_nodes and all(dsl['nodes'][n]['type'] == 'end' for n in next_nodes):
await sor.U('flow_instance', {
'id': instance_id,
'status': 'finished',
'active_nodes': json.dumps(list(next_nodes))
})
else:
await sor.U('flow_instance', {
'id': instance_id,
'active_nodes': json.dumps(list(next_nodes))
})
# ------------------------------ # ------------------------------
# Human task APIs # Human task APIs
# ------------------------------ # ------------------------------
async def node_transfer(self, node): async def auto_assign(self, sor, inst, ne, node):
if node['status'] not in ['done', 'failed', 'cancel']: recs = await env.get_org_users(sor, inst.org_id, ne.role)
return if recs:
i = randint(len(recs) - 1)
await sor.U('node_execution', {
'id':ne.id,
'assignee': recs[i].id
})
async def check_instance_completed(self, sor, inst):
sql = "select * from node_execution where instance_id=${instance_id} and type='end'"
recs = await sor.sqlExe(sql, {'instance_id', inst.id})
if recs:
if recs[0].status == 'completed':
await sor.U('flow_instance', {
'id': inst.id,
'status': 'completed'
})
async def node_transfer(self, sor, dsl, inst, ne, node, flow_def):
nnode = [] nnode = []
for edge in dsl.get('edges', []): for edge in dsl.get('edges', []):
if edge['from'] != node_id: if edge['from'] != ne.node_id:
continue continue
cond = edge.get('when') cond = edge.get('when')
if cond and not eval(cond, {}, {'ctx': ctx}): if cond and not eval(cond, {}, {'ctx': ctx}):
@ -305,7 +324,47 @@ class FlowEngine:
on_array = await self.get_multiple_array(m_on): on_array = await self.get_multiple_array(m_on):
for e in on_array: for e in on_array:
nnodes.add((edge['to'], str(e))) nnodes.add((edge['to'], str(e)))
return nnodes for node, ctx_ext in nnodes:
x = await self.is_ok_to_create_new_node_exe(sor, node, edges, inst)
if x:
env = ServerEnv()
id = env.uuid()
ns = {
'id': id,
'type': node['type'],
'inst_round': inst.round,
'org_id': inst.org_id,
'node_id': node['id'],
'input_ctx': node.get('input_ctx'),
'output_ctx': node.get('output_ctx'),
'status': 'pending',
'ctx': json.dumps(inst.ctx.copy(), ensure_ascii=False),
'ctx_ext': json.dumps(ctx_ext.copy(), ensure_ascii=False),
'created_at': timestampstr()
}
if node['type'] == 'human':
ns.update({
'role': node.get('role')
})
elif node['type'] == 'task':
pass
elif node['type'] == 'subflow':
eng = env.template_engine
ctx = inst.ctx
if node.get('input_ctx'):
ctx = eng.renders(node['input_ctx'], ctx)
subinst_id = await self.create_instance(org_id, node.flow_def_id, ctx=ctx)
ns.update({
'subinst_id': subinst_id
})
elif node['type'] == 'end':
ns.update('
await self.create_end_node_execution(sor, inst)
await sor.C('node_execution', ns)
if node.join
async def is_ok_to_create_new_node_exe(self, sor, node, edges, inst): async def is_ok_to_create_new_node_exe(self, sor, node, edges, inst):
join = node.get('join) join = node.get('join)
@ -331,7 +390,7 @@ class FlowEngine:
}) })
return id return id
async def is_ok_to_start(self, sor, node, edges, inst): async def is_ok_to_step_next(self, sor, node, edges, inst):
join = node.get('join') join = node.get('join')
if not join: if not join:
return True return True
@ -439,28 +498,24 @@ where instance_id=${instance_id}$
'ctx': json.dumps(ctx) 'ctx': json.dumps(ctx)
}) })
def get_engine():
env = ServerEnv()
cnt = len(env.flow_engines)
id = randint(cnt-1)
return env.flow_engines[id]
async def add_new_workflow(request, params_kw={}): async def add_new_workflow(request, params_kw={}):
name = params_kw.name name = params_kw.name
dsl = params_kw.dsl dsl = params_kw.dsl
env = request._run_ns env = request._run_ns
orgid = await env.get_userorgid() orgid = await env.get_userorgid()
async with get_sor_context(env, 'dagflow') as sor: ctxfileds = params_kw.ctxfields or []
ns = { description = params_kw.description
'id': env.uuid(), engine = get_engine()
'name': name, id = await engine.create_definition(org_id, name, description, '1', dsl, ctxfields)
'dsl': dsl,
'version': '1',
'created_at': timestampstr(),
'org_id': orgid
}
await sor.C('dagflow_definition', ns)
return {
'status': 'SUCCEEDED',
'data': {
'dagflow_id': ns['id']
}
}
return { return {
'status': 'FAILED', 'status': 'SUCCEEDED',
'error': 'add workflow error' 'data': {
'dagflow_id': id
}
} }

View File

@ -1,18 +1,29 @@
import asyncio import asyncio
from functools import partial from functools import partial
from appPublic.jsonConfig import getConfig
from ahserver.serverenv import ServerEnv from ahserver.serverenv import ServerEnv
from ahserver.configuredServer import add_cleanupctx from ahserver.configuredServer import add_cleanupctx
from .dagflow import FlowEngine from .dagflow import FlowEngine
async def dagbacktask(engine, app): async def dagbacktask(engines, app):
task = asyncio.create_task(engine.bgtask()) tasks = []
for e in engines:
task = asyncio.create_task(e.bgtask())
tasks.append(task)
yield yield
task.cancel() for task in tasks:
task.cancel()
def load_dagflow(): def load_dagflow():
engine = FlowEngine() config = getConfig()
f = partial(dagbacktask, engine) flow_engines = []
cnt = config.dagflow_job_cnt or 1
for i in range(cnt):
e = FlowEngine(i)
flow_engines.append(e)
f = partial(dagbacktask, flow_engines)
add_cleanupctx(f) add_cleanupctx(f)
env = ServerEnv() env = ServerEnv()
env.workflow_engine = engine env.flow_engines = flow_engines

View File

@ -7,11 +7,12 @@
}], }],
"fields": [ "fields": [
{"name": "id", "title": "实例ID", "type": "str", "length": 32, "nullable": "no"}, {"name": "id", "title": "实例ID", "type": "str", "length": 32, "nullable": "no"},
{"name": "backid", "title": "后台id", "type": "short", "nullable": "no"},
{"name": "round", "title": "当前论次", "type": "short"},
{"name": "org_id", "title": "机构id", "type": "str", "length": 32, "nullable": "no"}, {"name": "org_id", "title": "机构id", "type": "str", "length": 32, "nullable": "no"},
{"name": "flow_def_id", "title": "流程定义ID", "type": "str", "length": 32}, {"name": "flow_def_id", "title": "流程定义ID", "type": "str", "length": 32},
{"name": "status", "title": "状态", "type": "str", "length": 16}, {"name": "status", "title": "状态", "type": "str", "length": 16},
{"name": "ctx", "title": "上下文(JSON)", "type": "text"}, {"name": "ctx", "title": "上下文(JSON)", "type": "text"},
{"name": "active_nodes", "title": "当前节点(JSON)", "type": "text"},
{"name": "created_at", "title": "创建时间", "type": "timestamp"} {"name": "created_at", "title": "创建时间", "type": "timestamp"}
], ],
"indexes": [ "indexes": [

View File

@ -8,6 +8,7 @@
"fields": [ "fields": [
{"name": "id", "title": "执行ID", "type": "str", "length": 32, "nullable": "no"}, {"name": "id", "title": "执行ID", "type": "str", "length": 32, "nullable": "no"},
{"name": "type", "title": "节点类型", "type": "str", "length": 32}, {"name": "type", "title": "节点类型", "type": "str", "length": 32},
{"name": "inst_round", "title": "执行论次", "type": "short"},
{"name": "org_id", "title": "机构id", "type": "str", "length": 32}, {"name": "org_id", "title": "机构id", "type": "str", "length": 32},
{"name": "instance_id", "title": "流程实例ID", "type": "str", "length": 32}, {"name": "instance_id", "title": "流程实例ID", "type": "str", "length": 32},
{"name": "node_id", "title": "节点ID", "type": "str", "length": 64}, {"name": "node_id", "title": "节点ID", "type": "str", "length": 64},
@ -25,7 +26,6 @@
{"name": "running_at", "title": "执行时点", "type": "timestamp"}, {"name": "running_at", "title": "执行时点", "type": "timestamp"},
{"name": "stopping_at", "title": "结束时点", "type": "timestamp"}, {"name": "stopping_at", "title": "结束时点", "type": "timestamp"},
{"name": "created_at", "title": "执行时间", "type": "timestamp"} {"name": "created_at", "title": "执行时间", "type": "timestamp"}
{"name": "next_nodeid", "title": "下一节点id", "type", "str", "length": 32}
], ],
"indexes": [ "indexes": [
{"name": "idx_node_exec_inst", "idxtype": "index", "idxfields": ["instance_id"]} {"name": "idx_node_exec_inst", "idxtype": "index", "idxfields": ["instance_id"]}

View File

@ -1,20 +0,0 @@
{
"summary": [{
"name": "subflow_instance",
"title": "子流程实例",
"primary": "id",
"catelog": "relation"
}],
"fields": [
{"name": "id", "title": "子流程ID", "type": "str", "length": 32, "nullable": "no"},
{"name": "parent_instance_id", "title": "父流程实例ID", "type": "str", "length": 32},
{"name": "parent_node_id", "title": "父节点ID", "type": "str", "length": 64},
{"name": "child_instance_id", "title": "子流程实例ID", "type": "str", "length": 32},
{"name": "status", "title": "状态", "type": "str", "length": 16},
{"name": "created_at", "title": "创建时间", "type": "timestamp"}
],
"indexes": [
{"name": "idx_subflow_parent", "idxtype": "index", "idxfields": ["parent_instance_id"]},
{"name": "idx_node_exec_inst", "idxtype": "index", "idxfields": ["instance_id"]}
]
}