Dynflow中文文档

dynflow是一个工作流引擎,foreman-katello中主要就是通过它来实现的工作流编排工作

特性概览

  • 跟踪运行进程的进度
  • 异步运行代码
  • 当出错时返回任务,在需要的时候跳过一些任务
  • 检测独立的部分并且同步运行
  • 可以将简单的操作组合为复杂操作
  • 从第三方库扩展工作流
  • 在本地数据库事务和外部事务之间保持一致性
  • 挂起长时间运行的步骤,不阻塞线程池
  • 在可能的时候可以取消步骤
  • 通过中间件来扩展操作
  • 采用不同的适配器:存储引擎后端、事务,或者执行器

Dynflow是开发来以支持katello和foreman中的服务编排的

词汇表

  • Action - 执行计划的构建块,是一个继承自Dynflow::Action的ruby类,定义每个阶段需要运行的代码
  • Phase - 每个动作都有三个阶段: plan,run,finalize
  • Input - 一个传入到操作中的组成数据的hash
  • Output - 一个由操作产生的数据的hash.它是持久化的而且可以传递给其他操作
  • Execution plan - 工作流定义:
  • Trigger and action - 进入计划阶段,以操作的plan开始,执行会立即开始
  • Flow - run/finalize阶段的定义,保存有关可以并行/顺序执行的的步骤的信息
  • Executor - 运行 基于执行计划(execution plan)的run和finalize方法的服务,它可以在相同的进程中作为计划阶段运行 或者 在不同的进程中(使用远程执行器)
  • world - Dynflow运行代码的宇宙/空间?:它保存了所有需要的配置信息。通常每个dynflow进程中只有一个world,除此之外,它还保存了Persistence,Logger,Executor以及所有其他执行操作所需要的东西。这一概念允许我们避免共享状态。而且,world可以相互对话,这对于生产和高可用设置很有用,在多台主机上拥有多个world来处理执行计划。如果你仍然很疑惑并且是来自RoR世界(指是一名Rails开发?),就把它想象成是Ruby on Rails上的对象

例子

在examples目录下有action中的代码示例。运行这些文件(除了example_helper.rb以外)会让dynflow运行时进行初始化(包含可以探索特性和实验的web控制台)

如何使用

1.创建世界world

2.开发 vs 生产

开发环境中,执行运行在同一个进程中,而在生产环境中,会有一个执行器进程的概念

3.Action解析

[图片上传失败...(image-f9a24-1638328748521)]

当action被触发的时候,Dynflow在这个操作上执行plan方法,这代表着执行计划。它通过调用plan_action以及plan_self方法来构建执行计划,有效地的列出此执行计划应该运行的操作。或者说,它编译了一组 run方法将会调用的操作。而且它也代表着给了这些操作一个执行顺序。比如如下代码:

# 需要删除作为input数组传入进来的文件数组
def plan(files)
  files.each do |filename|
    plan_action MyActions::File::Destroy, filename
  end
end

需要注意的是,在计划要运行的代码中不必只是调用其他action而已。事实上在plan中计划它自身也是很普遍的,这意味着它会把它自己的run方法调用放在执行计划阶段。为了实现这种效果你可以使用plan_self。这可以在MyActions::File::Destroy中用到

class MyActions::File::Destroy < Dynflow::Action
  def plan(filename)
    plan_self path: filename
  end

  def run
    File.rm(input.fetch(:path))
  end
end

在上面的例子中,plan_self看上去只是plan_action MyActions::File::Destroy, filename的一个缩写而已。需要注意地是plan_action总是会触发指定action的plan方法,而plan_self只是触发run和finalize方法,所以通过plan_action方法,我们可以实现无限循环。

同样需要注意地是,run方法不接受任何输入参数。它是通过input方法来引用参数的,其中的参数是我们在调用plan_self方法时传递的,input方法返回的是一个hash

与Input类似,run方法还会产生output。在run方法执行完成后,finalize方法可以调用。操作可以使用其他操作的的输出output作为它们的输入的一部分来建立依赖。操作的状态在每个阶段会被序列化。

从前面的接触中可以看到,大致有3个阶段:plan,run,finalize。计划阶段由trigger操作触发启动。run和finalize方法只有当你在plan方法中使用plan_self方法时才会调用。

3.1 Input和Output

输入和输出都是可由Action#input和Action#output方法访问的散列。他们需要序列化为JSON,所以它应该只包含基本Ruby类型的组合,如:哈希,数组,字符串,整数等。

注意

应该避免直接赋值

self.output = {key: value}

这样可能会删除其他数据,因此最好使用

output.update(key: data, another_key: another_data)
# or for one key
output[:key] = data
3.2 Triggering

触发操作表示启动plan阶段,紧接着就会立即执行。任何操作都是由如下操作触发的:

world_instance.trigger(AnAction, *args)

注意:

Foreman和Katello中的操作通常是由ForemanTask.sync_taskForemanTask.async_task触发的所以如下的部分并没有那么重要,如果你是使用ForemanTasks的话。

World#trigger方法返回TriggerResult类型的对象。它是使用Algebrick变体类型定义的

3.3 Scheduling任务调度

排程指的就是可以让任务在未来某个指定时间执行。任何操作都可以通过如下调用方式来延迟调用:

world_instance.delay(AnAction,
                     { start_at: Time.now + 360, start_before: Time.now + 400 },
                     *args)

这一段脚本将会延迟AnAction的调用并且最终会在start_at到start_before之间运行。设置start_before为nil将会不带超时限制的延迟执行该任务。

当操作被延迟了,一个执行计划对象会被创建为状态为scheduled,但是它仍然不会运行执行计划,计划会在start_at时间到时执行。如果计划没有按时执行(例如,在start_before之后),执行计划会被标记为failed(它的状态被设置为stopped并且结果为error)

在这种情况下,参数必须被保存下来(以供之后计划启动时调用),所以必须有一种机制能够安全的序列化和反序列化它们以让它们能够存到数据库中。这是由serializer处理的。可以为每个操作单独设置serializer通过覆盖它的delay方法。

延迟计划的计划执行是由DelayedExecutor处理的,一个间歇性检查延迟的执行计划并执行它们的对象。已排程的计划不会自行执行任何操作,它们仅仅是等待被拾取并且被DelayedExecutor计划。这表示如果没有DelayedExecutor存在,它们的计划将会被推迟直到有调度器生成

3.4 计划阶段

计划总是使用线程来触发的action。计划阶段为运行阶段配置action的input。它通过执行action实例的plan方法来启动并传递来自World#trigger方法的参数

# 先调用trigger方法触发
world_instance.trigger(AnAction, *args)
# 之后会执行
an_action.plan(*args) # an_action是AnAction 的实例

计划方法继承自Dynflow::Action并且默认情况下只要run方法存在的话它就会调用plan_self使用第一个参数作为输入参数。它也会调用fianlize方法只要它存在。

class AnAction < Dynflow::Action
  def run
    output.update self.input
  end
end

world_instance.trigger AnAction, data: 'nothing'

上面的例子中,将会计划它自身将输入复制到输出中。

在大多数情况下plan方法是被覆盖来使用变形的参数计划它自身或者计划其他action。在Rails应用中,计划阶段的参数经常是ActiveRecord对象,将其用作action的输入参数。

让我们先看看参数变换:

class AnAction < Dynflow::Action
  def plan(any_array)
    # pick just numbers
    plan_self numbers: any_array.select { |v| v.is_a? Number }
  end

  def run
    # compute sum - simulating a time consuming operation
    output.update sum: input[:numbers].reduce(&:+)
  end
end

注意:

为执行job的action的输入提供足够的数据是一个很好的实践。这表示不用太多(比如直接传递ActiveRecord的attributes),因为这或许会产生性能影响而且会造成问题,当改变attributes的时候。

另一方面,输入里应该包含足够的数据来执行job,而无需去访问外部资源。因此,不要传递一个AR id进来然后再在run方法中重新查询这条记录,我们仅仅需要使用其中的一部分属性,最好是直接使用这些属性作为action的输入。

从实际和性能方面的考虑的话,遵从如下规则就是最好的实践

下面是一个action的例子:

class SumNumbers < Dynflow::Action
  def plan(numbers)
    plan_self numbers: numbers
  end

  def run
    output.update sum: input[:numbers].reduce(&:+)
  end
end

class SumManyNumbers < Dynflow::Action
  def plan(numbers)
    # references to planned actions
    planned_sub_sum_actions = numbers.each_slice(10).map do |numbers|
      plan_action SumNumbers, numbers
    end

    # prepare array of output references where each points to sum in the
    # output of particular action
    sub_sums = planned_sub_sum_actions.map do |action|
      action.output[:sum]
    end

    # plan one last action which will sum the sub_sums
    # it depends on all planned_sub_sum_actions because it uses theirs outputs
    plan_action SumNumbers, sub_sums
  end
end

world_instance.trigger SumManyNumbers, (1..100).to_a

上面的例子将会按照10个一组来并行计算数值。在所有子计算完成后一个最终数值再累加所有的子累加

注意

这个例子只是用于描绘planning的能力。在现实中这种计算密集的任务在MRI上运行的并不好。工作池可能会消耗完。但是这也问题不大,因为Dynflow主要是用于编排外部资源的(所以性能是否够好,不是太影响,主要用于编排)

Action在计划阶段获取会访问本地数据库,请看 数据库与事务 部分。

3.5 运行阶段

run方法实现了该Action主要实现的功能。在这个阶段输入是不可变的。这是所有可能失败的步骤的正确位置。Action的运行阶段允许有一些副作用比如:文件操作、调用其他系统、等等。本地数据库在这个阶段不应该被访问,请看 数据库与事务

3.6 最终阶段

finalize阶段的主要目的是在action成功结束后能够访问本地数据库,比如:基于新数据建立索引,更新记录作为完全创建,等等。最终阶段不会修改输入和输出。Action可以在最终阶段访问本地数据库并且必须确保幂等性,请看 数据库与事务

4. 依赖

正如前面提到,action可以使用其他action的输出作为输入。当这样做时,Dynflow会自动检测它们之间的依赖关系并构建相应的执行计划。

def plan
  first_action  = plan_action AnAction
  second_action = plan_action AnAction, first_action.output[:a_key_in_output]
end

第二个Action用到了第一个Action的输出,所以第二个Action依赖于第一个Action。

再比如:

def plan
  first_action  = plan_action AnAction
  second_action = plan_action AnAction
end

这种情况表示两个Action是相互独立的,那么它们将会并行地执行。

也有其他方式可以描述Action之间的依赖关系。Dynflow用户可以通过sequence和concurrence。这两个方法都将带上一个块来调用

5.数据库和事务

Dynflow被设计用于服务编排。通常按照如下流程来执行,我们使用一个user来作为示例。

  1. 触发User Creation,参数是一个未保存的AR user对象
  2. 计划: user存储到本地数据库(在Dynflow主机应用中)。记录标记为未完成
  3. 运行:使用REST调用的所有user需要的外部调用。这个阶段会在所有外部调用成功之后结束
  4. 最终:在数据库中的记录标记为 已完成准备好被使用。

在plan和finalize之间是有事务的(所有的action的plan方法是在一个事务中的)。如果在plan阶段发生了任何错误的话,任何在planning阶段对本地数据库的改变都会撤回。finalize阶段也一样,如果出现任何错误,所有变更都会撤回。因此所有finalize方法都必须保证幂等性。

Dynflow内部使用Sequel作为ORM,但是用户也可以选择他们所需要的来访问数据。有个接口TransactionAdapters::Abstract,它的实现可以提供不同ORM的事务。最常见的应该就是TransactionAdapters::ActiveRecord了

module Demo
  class Action1 < Dynflow::Action
    input_format do
      param :id, Integer
    end
    def plan(id)
      Dynflow::TransactionAdapters::ActiveRecord.new.transaction do
        puts "pppppppppppppppppppppppppppp Action1"
        User.create(name: 'eddy2')
        # plan_self(id: id)
        plan_action Action2
        super
      end
    end
    def run
      puts "rrrrrrrrrrrrrrrrrrrrrrrrrrrr Action1,input: #{input}"
    end

    def finalize
      puts "ffffffffffffffffffffffffffff Action1"
    end
  end
  class Action2 < Dynflow::Action
    def plan
      #在Action2中报错,Action1中的数据变更也会撤回
      raise 'wrong22!'
      puts "pppppppppppppppppppppppppppp Action2"
      plan_self(id: id)
    end
    def run
      puts "rrrrrrrrrrrrrrrrrrrrrrrrrrrr Action2"
    end
  end
end

6.组合

使用Dynflow可以很容易地将Action组合在一起。典型的例子是一个Action将其他小型的Action组合在一起,而真正的实现逻辑都在小型的Action中:

class CreateInfrastructure < Dynflow::Action
  def plan
    sequence do
      concurrence do
        plan_action(CreateMachine, 'host1', 'db')
        plan_action(CreateMachine, 'host2', 'storage')
      end
      plan_action(CreateMachine,
                  'host3',
                  'web_server',
                  :db_machine      => 'host1',
                  :storage_machine => 'host2')
    end
  end
end

这种情况下,CreateInfrastructure不带run方法,它的目的仅仅是用于编排其他Action的运行顺序。

7.订阅

使用组合的方式可以帮助我们很好地拆解业务逻辑为小型的代码,但是它并不直接支持用插件来扩展。所以就有了订阅。

Action可以从插件、gem来订阅,或者任何其他早已加载了的Action,这样子它们可以自己扩展计划进程。

让我们看一个例子:

# This action can be extended without doing any
# other steps to support it.
class ACoreAppAction < Dynflow::Action
  def plan(arguments)
    plan_self(args: arguments)
    plan_action(AnotherCoreAppAction, arguments.first)
  end

  def run
    puts "Running core action: #{input[:args]}"
    self.output.update success: true
  end
end

下面是定义在插件中的另一个Action,它订阅了上述Action。

class APluginAction < Dynflow::Action
  # plan this action whenever ACoreAppAction action is planned
  def self.subscribe
    ACoreAppAction
  end

  def plan(arguments)
    # arguments are same as in ACoreAppAction#plan
    plan_self(args: arguments)
  end

  def run
    puts "Running plugin action: #{input[:args]}"
  end
end

8.挂起

有时任务代表采用不同服务的任务(比如pulp的同步任务)。Dynflow会尽量不浪费计算机资源,所以它提供了工具来释放线程以执行其他Action,当等待外部任务的时候。

Dynflow允许actions挂起并且基于外部事件来唤醒。让我们来模拟一下外部服务:

class AnExternalService
  def start_synchronization(report_to)
    Thread.new do
      sleep 1
      report_to << :done
    end
  end
end

AnExternalService可以调用start_synchronization这个方法并且它会稍后就会返回到report_to参数所代表的Action。它发送了事件 :done

class AnAction < Dynflow::Action
  EXTERNAL_SERVICE = AnExternalService.new

  def plan
    plan_self
  end

  def run(event)
    case event
    when nil # first run
      suspend do |suspended_action|
        EXTERNAL_SERVICE.start_synchronization suspended_action
      end
    when :done # external task is done
      output.update success: true
      # let the run phase finish normally
    else
      raise 'unknown event'
    end
  end
end

运行流程如下:

  1. AnAction触发
  2. 开始执行计划
  3. 运行阶段开始
  4. run方法被调用但是没有事件(nil)
  5. 与案例分支匹配,启动外部同步
  6. Action启动同步
  7. Action被挂起,run方法的运行立即结束在suspend被调用后,它的块参数就是会在挂起之后立即计算
  8. Action会保存在内存中以用于之后唤醒,当收到事件的时候,但是它并不会阻塞任何线程
  9. Action收到done事件
  10. run方法再次执行
  11. 输出被更新为success: true
  12. 由于没有finalize阶段,action结束

这种事件机制非常复杂,它可以用于构建轮询Action。

9.轮询

要支持轮询的话,Dynflow中有一个模块可以实现。只需要引入它

在action中还需实现3个方法:

  • done? - 通过外部数据判断任务已经完成
  • invoke_external_task - 调用外部任务
  • poll_external_task - 轮询外部任务状态并返回一个状态(JSON序列化数据比如:Hash,Array,String, etc.)将其存储在output中
 def done?
    external_task[:progress] == 1
  end

  def invoke_external_task
    triger_the_task_with_rest_call
  end

  def poll_external_task
    data     = poll_data_with_rest_call
    progress = calculate_progress data # => a float in 0..1
    { progress: progress
      data:     data }
  end
end

Action在运行阶段会做如下事情:

  1. 在第一次运行时执行 invoke_external_task
  2. 挂起然后间歇性:
    1. 唤醒任务
    2. 调用poll_external_task轮询
    3. 调用done?检查是否完成
      • true -> 完成run阶段
      • false -> 进行下一次轮询

还有2个方法可以用于处理外部任务的数据,它可以手动覆盖:

* external_task - 读取外部任务存储的数据,默认情况下读取self.output[:task]
* external_task= - 写入外部任务存储的数据,默认情况下会写入self.output[:task] = value

还有一些其他已实现的特性:

* 轮询间隔逐渐延长
* 轮询失败时进行重试

10.状态

每个Action阶段都可以是以下几种状态中的一种:

  • Pending - 还未执行
  • Running - 其中一个Action阶段正在执行
  • Success - Action阶段执行成功
  • Error - 执行中出错
  • Suspended - 只会发生在run阶段,Action正在休眠等待唤醒
  • Skipped - 失败的Action可以被标记为已跳过
  • Skipping - Action被标记为需要跳过,但是执行计划目前还没执行完成,等执行计划完成后就会被标记为跳过

执行计划有如下状态:

* Pending - 计划还未执行
* Scheduled - 已为延迟执行安排好调度
* Planning - 正在执行计划
* Planned - 已计划,run阶段还未开始
* Running - 正在运行run和finalize阶段
* Paused - 已暂停。只会在执行失败或者重新开始时发生
* Stopped - 执行计划已经完成

执行计划还有以下结果:

* Success - 在没有错误或跳过的情况下完成
* Warning - 有跳过的步骤
* Error - 一个或多个Action失败
* Pending - 执行仍然在运行

TODO: 如何访问到这些状态呢?

11.错误处理

plan阶段和run,finalize阶段的报错有个不同:

  • plan阶段的报错,在调用trigger方法后就会直接报错,而run,finalize不会,这是因为run,finalize方法是在执行器中去执行的,而不是当前触发plan的这个线程了

如果在运行阶段发生错误,执行就会暂停。你可以在控制台查看错误。然后我们可以手动来修复问题,之后再重试该任务。在修复问题期间,我们可以先跳过该任务,修复完成后再继续执行跳过的任务。

如果在finalize阶段有错误的话,整个finalize阶段都会撤回并且可以在问题被修复时继续

TODO rescue策略

12.控制台

TODO 如何访问控制台

13.测试

TODO

14.长时间运行的actions

Dynflow作为一个任务编排工具,并没有去考虑多cpu计算(或者说多核计算)。即便有多个执行器,执行计划也只会在一个执行器上执行,所以不用JRuby的话,它并不能扩展。

长时间运行的action还有一个问题就是会阻塞worker。执行器只有一个被限制的worker池,如果更多的worker忙碌的话,会导致性能下降。

长时间运行的阻塞的action也是有问题的。

解决方案:

* 使用Action挂起 - 在条件满足时挂起Action,释放Worker
* 卸载计算 - 也就是将重cpu的部分卸载到不同的服务中 

15.多队列

默认情况下,用的是单队列单worker的方式来运行所有Action。这会导致一些高优先级的action被阻塞。

要定位问题的话,可以定义额外的队列将其绑定到额外的worker池。

要使用队列,需要在定义world的时候定义额外的队列:

config = Dynflow::Config.new
config.queues.add(:slow, :pool_size => 5)
world = Dynflow::World.new(config)

使用队列的action只需要覆盖queue方法即可,比如:

class MyAction < Dynflow::Action
  def queue
    :slow
  end

  def run
    sleep 60
  end
end

16.中间件

每个Action都有一条中间件链,其包装了Action执行的各个阶段。它与Rack中间件很相似。要从Dynflow::Middleware继承创建新的中间件。有5个方法可以被覆盖:plan,run,finalize,plan_phase,finalize_phase。

def plan(*args)
  pass *args
end

当覆盖时可以在pass方法之前或之后插入代码,这会执行链中的下一个中间件或者它自身(当其是链中最后一个中间件时)。大多数情况下pass总是会在覆盖的方法中调用的。

每个Action都有一个中间件链。中间件可以通过在Action类中使用use方法来添加。

class AnAction < Dynflow::Action
  use AMiddleware, after: AnotherMiddleware
end

use方法有3个选项:

  • before - 让其在指定中间件之后
  • after - 让其在指定中间件之前
  • replace - 让其替换指定中间件

17.子计划

要使用子计划的话,就需要引入Dynflow::Action::WithSubPlans模块,并且覆盖create_sub_plans方法。在create_sub_plans方法内部,使用trigger方法来创建子任务来以不特定的顺序执行。父任务将会在不阻塞线程的情况下等待子任务结束。

class MyAction < Actions::EntryAction
  include Dynflow::Action::WithSubPlans

  ...

  def create_sub_plans
    [
      trigger(Actions::OtherAction, action_param1, action_opts),
      trigger(Actions::AnotherAction)
    ]
  end
end

18.执行计划钩子

Dynflow允许Action在执行计划生命周期中建立钩子。要使用钩子,必须在Action中定一个方法并将它注册为钩子。当前每个执行计划的状态都有钩子事件,当执行计划转换到状态时,钩子事件被执行。此外还有两个钩子failure success 这是在计划变更为stopped时(以error或者success结束)时调用的。

方法可以通过execution_plan_hooks.use注册为钩子,再提供一个选项:on => HOOK_EVENT作为参数,HOOK_EVENT可以时钩子事件中的一个。万一选项没提供的话,方法就会在每次状态变更时都进行执行。类似地,以继承威力,钩子可以通过execution_plan_hooks.do_not_use来禁用,选项也是一样的。在Action上定义的钩子会被子类所继承。

钩子会在执行计划下的每个Action中执行,但是执行顺序是无法保证的。

class MyAction < Actions::EntryAction
  # Sets up a hook to call #state_change method when the execution plan changes
  #   its state
  execution_plan_hooks.use :state_change

  # Sets up a hook to call #success_notification method when the execution plan
  #   finishes successfully,
  execution_plan_hooks.use :success_notification, :on => :success

  # Disables running #state_change method when the execution plan starts or
  #   finishes planning
  execution_plan_hooks.do_not_use :state_change, :on => [:planning, :planned]

  def state_change(_execution_plan)
    # Do something on every state change
  end

  def success_notification(_execution_plan)
    # Display a notification
  end
end

工作原理

1.TODO Action States

  • 通用阶段和目前的阶段
  • 如何启动执行计划

2.world解析

world代表的是dynflow的运行时而且它是作为一个外部服务运作的。它保存了所有的配置信息以及Dynflow需要用来执行任务的子组件。

Dynflow world由以下几个子组件组成:

  1. persistence持久化 - 提供持久化能力
  2. coordinator - 提供world之间的协调能力
  3. connector - 提供world之间的消息传递的能力
  4. executor - 它是world运行执行计划的运行时本身。并非所有的worlds都必须有执行器存在(也可能存在纯客户端worlds: 在生产环境下很有用?)
  5. client dispatcher客户端分发器 - 负责客户端请求和其他world之间的交互通信
  6. executor dispatcher执行器分发器 - 负责从其他worlds获取请求以及发送响应
  7. delayed executor - 负责计划以及调度任务的执行

底层技术隐藏在适配器抽象后面,允许为作业选择合适的技术,同时保持其余的DYNFLOW完整

3.client world vs. executor world

最简单的例子是,world同时处理客户端请求和执行器本身。这对于多合一(单机?)部署和开发环境很有用

在生产环境中,你也许希望分离开客户端world(通常是运行webserver的进程中或者客户端库中)和executor world(作为独立的服务运行)。这样的设定让执行可以变得更加的稳定和可用。

在Dynflow的架构中可能会出现多个客户端以及多个executor worlds

executor world有它自己的客户端分发器,所以它可以作为客户端触发其他执行计划(对于子计划很有用)

4.单数据库模型

Dynflow可以从持久层、协调层(实时同步)以及连接器方面识别到潜在的技术。

然后,我们意识到到对于大多数使用场景来说,一个单共享数据库以及足以满足用户执行作业的需求了。墙纸使用不同的技术意味着无用的开销。

因此,完全有可能使用但数据库来完全所有的工作。

注意:

一些简易的数据库比如sqlite已经足够让Dynflow启动并运行且完成工作了。最好的是,推荐使用PostgreSQL,因为Dynflow可以监听特性以有更好的响应时间。

5.world内部通信

  1. 客户端准备执行计划,将其保存到持久层让后将其id传递给客户端分发器

  2. 客户端分期创建一个IVar其代表其未来执行完成时的值。客户端可以使用这个值来等待执行计划完成或者创建到其结束的时候

  3. 客户端分发器创建一个信件,包含执行的请求,携带上receiver id 设置为AnyExecutor

  4. 连接器使用调度算法来选择将请求发送给哪个执行器并且替换AnyExecutor为它。将信件发送给指定的执行器

  5. 执行器端的连接器接收到信件并且询问执行器分发器来处理它

  6. 执行器分发器在执行计划上获取锁并且初始化一个执行。同时,让客户端知道作业已经被接收(这里在客户端要处理可能会有额外的逻辑,比如超时)

    7-9. 执行器层将Accepted响应返回给客户端

    10 执行器完成执行

    11-12 执行器层发送finished响应

    13 客户端分发器处理原始的IVar,这样客户端就可以找到完成的执行了。

    这些行为在单world场景下也是相同的:在这种情况下world同时参与客户端和执行器端,通过内存内的连接器来直连

6. 持久层

持久性确保执行计划的序列化状态持续用于恢复和状态跟踪。执行计划数据存储在其中,实际状态。

与协调器不同,所有持久的数据都不必同时为所有世界提供:每个世界都需要它自身工作的数据。此外,所有数据不必在世界之间完全同步(只要世界上可用于世界上有关相关执行计划的最新数据)。

7.连接器

提供worlds之间的消息出啊等你。消息有如下结构的信件格式:

  • sender_id - 产生信件的world的id
  • receiverd id - 应该接收该信件的world的id,或者AnyExecutor(在有负载均衡的情况下)
  • request id - 客户端唯一的id,用于比对请求-响应
  • message - 消息体:连接器并不关心这个,因为它已经序列化了

8.协调器

这个组件(在多执行器设置下尤其重要):确保不会有2个执行器同时执行一个相同的执行计划(别名 锁)。也提供了系统中可用的world的信息(world注册)。不像持久层,它并不需要存储数据(可能重新计算),但是它需要提供一个全局化的共享状态。

协调器适用的主要类型的对象是一个由以下组成的记录:

  • type - 记录类型
  • id - 记录id(同类型下唯一)
  • data - 基于type的任意格式的数据

有一个特殊类型记录叫做lock,它保持着属主信息。它被用来保存 哪个执行器是正在执行哪个执行计划的信息:执行器不允许启动执行,除非它成功获得了属于它的lock锁。

9.单例Action

10.线程池 TODO

11 挂起 TODO

你可能感兴趣的:(Dynflow中文文档)