Gem WorkflowCore —— SaaS 快速开发套件之工作流引擎

jasl · September 27, 2018 · Last by jasl replied at February 14, 2020 · 20875 hits
Topic has been selected as the excellent topic by the admin.

前言:今年个人遇到了一些私事没有办法拿出足够精力组织 RubyConf China,最近做出一个比较重要的人生决定再次下场创业,在进入创业、办会两边倒之前下定决心抽了一周时间把一直想做的轮子组的最后一块拼图也是最难的一块搞出来,便是接下来要介绍的工作流引擎。

实际上我发现把工作流引擎做完善并且演示出来是一个非常耗费精力的需要长时间投入的工作,所以不像我之前完成的开源项目,我决定把这个刚完成技术验证的版本公开,露一露怯。

项目地址: https://github.com/rails-engine/workflow_core

工作流引擎可以说是企业信息系统的核心技术,用于解决审批流程的定义和流转。此外,IFTTT 类型的应用,包括苹果 iOS 12 的新功能 Shortcut,也都是由工作流引擎驱动,在数据处理领域也可以考虑使用。

虽然我们总是说 Ruby 或者说 Rails 开发效率高,但是对于复杂的信息系统而言,缺乏领域相关的基础设施使得 Ruby on Rails 并不会比 Java、C# 等传统技术有优势。相反,由于没有标准实践,大多数团队没有技术驱动业务的思维,导致基于 Ruby 的信息系统反而在可维护性上表现极差。

我认为,企业信息系统的三个关键组件为:动态表单、角色权限、流程,流程承载动态表单的数据(Payload),交给合适的角色来处理,这就是我造这三个轮子的动机。 注:表单只是工作流的一种 Payload,以此便可以解开表单引擎和工作流引擎的耦合

WorkflowCore 采用了比较经典的 PetriNet 数据结构实现,准确的说是基于 PetriNet 的一个为工作流优化的特殊型式 Workflow Net (个人推荐读读这个博客的相关文章),它能够表达几乎一切可能的流程流转方式,包括 BPMN 等业内标准规范的流程定义都是 PetriNet 的子集,简单介绍一下,PetriNet 是一个有向图,里面涉及了几个概念:

  • 圆形图示 Place:流程流转过程中的稳定状态
  • 矩形图示 Transition:造成状态改变的动作
  • 黑点图示 Token:待处理的任务,一个工作流里可以包含多个活跃的 Token

并且 Workflow net 有一个特性,如上图所示,工作流由 Place 开始,由 Place 结束,Place 之间不能相邻,必须是 Transition。

综合这些,我们就很容易设计出工作流引擎的核心了,这正是 WorkflowCore 所做的事:

  • 设计了 PetriNet 相关概念的模型
  • 为 Transition 定义了 fire(token) 方法作为接口

就这样简单,那么如何实现工作流的流转呢?只需要实现自己的 Transition 类型,覆写 on_fire(token) 方法,在其中编写当前的 Token 要如何销毁,新的 Token 要如何创建,结束~ 如果你需要一些比较特殊或者需要做比较脏的逻辑,这些代码都会封装在某一个 Transition 子类中,不会污染到其他地方,此外,Token 的创建销毁是无副作用的,也不用担心特殊的逻辑对流程有影响,但是如果流转过程对工作流外的资源操作了,这会产生 side-effect 需要自行考虑会不会对系统造成影响哈。

当然,实际作为 Gem,迁移操作要做到原子化,包括可能的嵌套事务的坑的避免,所以还是有一些琐碎的技术工作在其中的。

和之前的 Gem 一样,项目的 dummy app 提供了一套测试应用,展现的是一套审批系统,使用方法可以自行看 Readme 注:需要提前安装下 Graphviz

另外,dummy app 里使用了另一个未完成的组件 —— 基于 mRuby 的表达式执行沙箱 ScriptCore ,填过业务系统的坑的人一定都经历过解决极端复杂的条件的情况,一个几乎完整的 Ruby 执行环境应该足够解决绝大多数的复杂场景了,并且能够保证安全性和控制资源使用(不必担心死循环等情况)。遗憾的是,由于 mRuby 并没有原生的 BigDecimal,也没有 Date,Datetime 的行为和 MRI 也不一致(主要是时区方面),所以完全释放潜力还是需要做一些努力的,但这套思路是可行的,这套思路由 Shopify 提出,参见 Shopify Script

最后,由于精力所限,看样子我只能拿出一个预览版本的了,有类似经验或者需求的朋友欢迎交流,Readme 里我列出了一些需要确认的点或要填的坑。

非常感谢姜军,为我这方面相关开发提供了很多思路💓

很早就耳闻了,终于放出来了,赞一个~ (最近一段时间一直忙家里的事情,代码一直没有看,捂脸)

Reply to qhwa

其实就是上周才动手写的 😂

回忆起来才发现第一个活就跟工作流打交道了,而且当初用户权限认证还用的是 LDAP 来做,😂 😂 😂

huacnlee mark as excellent topic. 27 Sep 12:26

上周才开始写的代码 很高效啊 👍

利害。基于 java 的那几个工作流引擎,都是一个团队在做

放出来啦,赞!

指不定后面就要用到,支持一波!

Reply to zouyu

java 的是哪个

Reply to jdshi

jbpm activiti

Reply to lyfi2003

好啊,用得时候有什么问题建议随时骚扰

关注好久了。研究下怎么在前后分离的情况下,配合 graphql 使用

Reply to kimmg

这个触发流转动作没难度,表单这块 @hooopo 去年就在他们那用上了,前后端分离+GraphQL(他欠了我很多稿子

终于来了

早日开发出你的游戏引擎 hhhh

最近我也要做一个类似 bpmn 的应用,包括审批流程的制定和执行审批,不知道这些功能用你的引擎能否实现。大牛既然是做这方面的能否推荐一些云服务给我用用

https://github.com/meirwah/awesome-workflow-engines 找到一个 awesome list,但是我不太了解工作流引擎这块,想问下 OA 系统里面的审批规则制定和执行用那个库比较好,或者能用哪个 saas 服务,求推荐

弱弱的一问,Ruby Rails 市场上有相似的套件吗?

我在想这个能否整合进 Rail Core.

Reply to xcaptain

工作流是个很宽泛的概念,审批系统算,如果你看 Wiki,容器编排、CI 的 pipeline 也都可以算,不同的工作流系统设计上有他的侧重点,所以可复杂可简单,但本质上,(不是很精确的解释哈)都在解决流程的定义和流程的执行这两件事。

流程定义就是说设计一种数据结构,来表达业务流程,通常来说最后会落地成一张有向图,文中提到的 PetriNet 相当于是一种为(侧重于审批系统)工作流优化的图结构。实际系统中,由于流程可能会非常复杂,或者说需要可视化的与业务方人员沟通,这时就涉及到了流程的建模。

这里插一句:你提到的 BPMN 就是一种标准化的流程建模语言,他定义了业内专家总结出的业务系统中的流转方式,并且有比较完善的生态:可以被一些流程引擎读取以及流程设计器。但是缺点也明显,“企业级”有时候和“复杂”是挂钩的,而且因为标准定义只包含了业内共识的部分,各家会给自家的 BPMN 方言加大量的私货。

流程执行就是核心功能了,简单的说就是读进流程定义,创建流程的实例(用来持久化流程相关的用户数据和状态),根据流程和实例的状态来执行流程。

认同这两点,结合业务,从头设计一套针对需求的工作流系统并不难,比如 hooopo 提过他那边业务类似电商,流程简单用状态机糊一个就行了,知人曾经魔改的 Netflix/Workflowable

至于 SaaS,之前看过 Kissflow,还不错,纯收费的,国内用可能不太接地气。

至于开源项目,工作流是 Java 和 C# 的传统阵地,比如 jBPM 和衍生出来的 Activiti、Flowable。jBPM 和他衍生品的特点是他自身就是一套完整的服务,并且暴露 Restful API,所以你可以把他们作为核心,然后外围构建自己的系统。

缺点很明显了,因为是独立的服务,跟业务系统的数据上的交互和同步会非常难做,数据出了问题对于审批系统,后果挺严重的。

我对审批系统的理解是由三部分组成:表单、权限、流程。你直接看 jBPM 的组件图,大致也是 流程、表单、用户和权限 基本跟我的理解是一致的。

所以这就是我做了一系列轮子的动机,有了这些轮子,就可以直接做在一个系统里了,同时通过组件化的方式明确边界。

最后说工作流在 Ruby 和 Rails 上的生态,我也在一直调研,可能我做的这套轮子是整个 Rails 社区独家,而且除了 WorkflowCore 其他组件都被一些团队(商业产品)验证过了,WorkflowCore 算是我个人对这个领域的总结,如正文所说,还是比较早期,我因为不在做这个领域了所以要暂时放下,但是思路上是没问题的。

你可以参考我上边提到的一些项目,也可以了解 WorkflowCore 原理后复制他示例的代码来构建你的系统,我个人经验看,Rails 做这个比 Java 那些开发效率要高很多,写起来也容易。

PS: 我记得 13 年读大四的时候,帮同学看过他的毕设,他正好是实习做 HIS 系统,基于 Rails 2.1

Reply to ksec

我调研过,我做的应该是全网独家。。。

合并进核心没必要,企业级 Rails 项目框架还有 http://trailblazer.to/,也提供了一些针对特定场景的重要轮子。

其实 ActionCable、ActiveStorage 之类默认包括在 rails 里,其实他们根本就不算 rails core 的,从这几个组件的设计上讲,都是 rails 的 engine。

Rails 的核心是 Railtie —— 工具链、ActionPack —— 控制器和视图(的接口)、ActiveModel 和 ActiveRecord —— 模型和 CRUD

其他都是外围,放在一起可以说是 rails team 的工程和宣传上的策略。

另外那些组件从 Basecamp 抽出来的,又 Rails team 和 Basecamp dev team 有很多交集,所以约等于 Rails 官方维护这些组件。

Reply to ksec

正因为看过一圈没有所以我才做的吧

Reply to jasl

感谢,那么状态机和你说的 PetriNet 有什么区别呢,在我看来一个工作流应该就是一个状态机,外界传入不同的事件从而改变自身状态,不知使用 PetriNet 有什么好处。

我调研了一下 uber 开源的 cadence,应该是类似状态机的实现,但是也能处理工作流,测试了一下 https://github.com/samarabbas/cadence-samples/tree/master/cmd/samples/expense 这个例子,就是个审批模型,但是 cadence 目前似乎存在缺陷没法查询工作流内部状态以及按照 bpmn 的标准生成工作流。

在上家公司用过 airflow,不知和 PetriNet 有什么区别,似乎 PetriNet 也是个有向无环图,从状态起点达到状态终点。

网上有人用 aws step function 或者是 azure logic app 实现自己的业务流程,我看文档 step function 最大执行时间能达到 1 年,也就是说一个流程可以在里面跑 1 年,对于审批系统来说足够用了,你觉得是否可以试试。

https://www.flovate.com/workflow-management-vs-business-process-management/

我可能把 workflow engine 和 business process engine 弄混了,严格来说 airflow 和 cadence 应该是属于 workflow engine 的,按照上面这篇文章作者的意思是着眼的角度更小,更关注具体任务的执行。一会儿我再去调研下 activity 的接口比较下差异

Reply to xcaptain

状态机的问题是他只能表达一个状态,比如我在帖子里提到的图例,比如:提请假,报直属领导同意,同时 报 HR 专员同意,然后抄送部门负责人,最后申请人确认后生效。并行的两个步骤。状态机就无法实现了。PetriNet 没有这问题

Reply to xcaptain

你要做的 his 属于 bpms,这领域名气比较大是 java 的 jbpm 和衍生的几个 比如 activiti flowable

你说的请假的例子按照这个图走呢,到把需要确认的 HR 当作一个组,每次 HR 做出审批通过操作的时候都检测一下是不是最后一个待审批的 HR,这样是否实现了并行审批的效果?

Reply to xcaptain

你发这种也很常见的。

需要哪些流转方式取决于甲方实际的流程,如果你主导的话前期多多和客户接触了解清楚他们的各种流程是最好的。

你作为乙方除非能说服甲方否则就要按照他们的要求来的,我举那个例子也不是很恰当,实际有更多复杂甚至不合理的

Reply to jasl

你可以注册个 kissflow 知人之类体验一下,还有 activiti 那些开源项目自己起一个感受下

具体到设计上,知人那套 workflowable 魔改加上 并行流程支持的做法还是很直接的 楼上 @yaocanwei 也是这思路

试用了一下 flowable,效果非常棒,工作流引擎这东西真是非常重要,不然全靠自己写的话复杂的业务流程能写出一堆 bug 来

Reply to xcaptain

flowable 算是 jbpm 原教旨版了... jbpm 系列的发展也可以说是波澜壮阔了

jasl in Ruby 开发的业务流程引擎有吗? mention this topic. 26 Apr 19:47

花了两天时间简单写了个 DSL https://github.com/dsh0416/petri-dsl/

用法:

require 'petri'

network = Petri::Net.new do |net|
    net.start_place :start, name: 'Start'
    net.end_place :end, name: 'End'

    net.transition :leader_evaluate, name: 'Leader Evaluate', consume: :start do |t|
        t.produce :leader_approved, name: 'Leader Approved', with_guard: :approved
        t.produce :rejected, name: 'Rejected', with_guard: :rejected
    end

    net.transition :hr_evaluate, name: 'HR Evaluate', consume: :leader_approved do |t|
        t.produce :hr_approved, name: 'HR Approved', with_guard: :approved
        t.produce :rejected, with_guard: :rejected
    end

    net.transition :report_back, name: 'Report Back', consume: :hr_approved, produce: :end

    net.transition :resend_request, name: 'Resend Request', consume: :rejected do |t|
        t.produce :start, with_guard: :resend
        t.produce :end, with_guard: :discard
    end
end

puts network.compile

# {:places=>[{:label=>:start, :name=>"Start"}, {:label=>:end, :name=>"End"}, {:label=>:leader_approved, :name=>"Leader Approved"}, {:label=>:rejected, :name=>"Rejected"}, {:label=>:hr_approved, :name=>"HR Approved"}], :transitions=>[{:label=>:leader_evaluate, :name=>"Leader Evaluate", :consume=>[:start], :produce=>[{:label=>:leader_approved, :guard=>:approved}, {:label=>:rejected, :guard=>:rejected}]}, {:label=>:hr_evaluate, :name=>"HR Evaluate", :consume=>[:leader_approved], :produce=>[{:label=>:hr_approved, :guard=>:approved}, {:label=>:rejected, :guard=>:rejected}]}, {:label=>:report_back, :name=>"Report Back", :consume=>[:hr_approved], :produce=>[{:label=>:end, :guard=>nil}]}, {:label=>:resend_request, :name=>"Resend Request", :consume=>[:rejected], :produce=>[{:label=>:start, :guard=>:resend}, {:label=>:end, :guard=>:discard}]}], :start_place=>:start, :end_place=>:end}

大概可以相对方便地来描述 workflow 了。

后继产品:Petri Net workflow for Rails by Hooopo

当然 Hooopo 的代码太暴力了,我也要写个版本,跟他比一比

jasl in FlowCore - SaaS 快速开发套件之工作流引擎 mention this topic. 20 Feb 01:38
You need to Sign in before reply, if you don't have an account, please Sign up first.