3. 过程控制¶
在一个较为复杂的事务中,一些操作,只有在一定的条件下才会执行;或者,在不同的条件下执行的时机不同。
如图所示的过程中,Action2
就是一个可选的操作,它只有在系统的某个开关打开的情况下才会执行。
3.1. __optional¶
对于这样的情况,可以使用 __optional
来描述:
auto ShouldExecAction2(const TransactionInfo&) -> bool {
return SystemConfig::shouldExecAction2();
}
__sequential
( __req(Action1)
, __optional(ShouldExecAction2, __asyn(Action2))
, __asyn(Action3)
, __asyn(Action4)
, __rsp(Action5));
从代码中可以看出, __optional
有两个参数:第一个参数是一个谓词,即一个返回值是一个 bool 的函数,
如果为真,则 __optional
的第二个参数: 一个 Action 将会得到执行。这是 C++
解决这类问题的常用手段。
如果谓词是一个 仿函数
,即一个类, Transaction DSL 对其有着和基本操作一样的约束,即它也必须是自满足的。
所以,它必须亲自访问环境来确定一个条件是否成立。
3.1.1. TransactionInfo¶
无论是谓词还是基本操作,这些需要用户定义的类,都必须是子满足的,所以, 它们自身计算所需的信息都必须亲历其为的到环境中查找。
由于 Transaction 自身也是环境的一部分,所以 Transaction 必须通过参数将自身的信息传递给基本操作或谓词,
从而让它们有能力得到一切需要的信息,这就是 TransactionInfo
的由来。
TransactionInfo
是一个接口类。通过它,你首先可以获取到 实例标识 ( Instance ID )。
因为有些系统对于同种类型的领域对象会创建多个实例,而每个实例都可 能会有自己的 Transaction;
通过 Instance ID ,用户定义的类就可以知道当前的 Transaction 属于哪个实例。
另外,Transaction 会通过 TransactionInfo
告知自身的运行时状态:是成功还是失败,如果失败,是什么原因导致的失败等等信息。
3.2. 路径选择¶
现在我们让事情更为复杂一些。
在图所示的事务中, Action2
即可以在 Action3
之前运行,也可以在它之后运行,究竟选择哪种方式,取决于相应的配置。
auto IsAction2RunFirst(TransactionInfo const& info) -> bool {
Object* object = Respository::findInstance(info.getInstanceId());
return object->isAction2RunFirst();
}
__transaction
( __req(Action1)
, __optional
( IsAction2RunFirst
, __asyn(Action2)
, __asyn(Action3))
, __optional
( __not(IsAction2RunFirst),
, __asyn(Action3)
, __asyn(Action2))
, __rsp(Action4));
首先,在这个例子中,谓词的实现使用了 TransactionInfo
来获取 Instance ID , 进而通过它找到了对象,从而完成了判断。
其次,我们使用了两个顺序的 __optional
来进行路径选择,这两条路径是互斥 的。而互斥的保证是通过 __not
来完成的。
3.2.1. __switch¶
当一段流程存在多条路径选择时,我们可以选择使用多个 __optional
来决定执行路径。
但是,我们从例子中同样可以看出,每个 __optional
都必须有自己的谓词,当谓词是一种互斥关系时,
我们需要在不同的 __optional
里,使用 __not
来描述这种 互斥。无疑,这会让事务程序员觉得麻烦。
另外,多个并列的 __optional
无法让事务代码的阅读者直观的看出这些路径之间的互斥关系。
所以,当存在多条互斥的路径时,最好应该使用 __switch
来描述:
__transaction
( __req(Action1)
, __switch( __case
( IsAction2RunFirst
, __asyn(Action2)
, __asyn(Action3))
, __otherwise
( __asyn(Action3)
, __asyn(Action2)))
, __rsp(Action4));
从代码中可以看出,在一个 __switch
里,一条路径可以使用 __case
来描述, 而 __case
则和 __optional
一样,
存在两个参数:谓词和操作。
当存在多条路径时,__case
的顺序则非常重要: Transaction DSL 会按照顺序依次匹配,一旦找到一条路径,将会执行其操作,
并忽略其它路径,即便其它路径的 谓词也可能匹配。
如果所有的 __case
谓词都不匹配,则 __switch
会返回事务的当前状态。 __otherwise
则是一个语法糖,
用来描述无条件匹配。所以,它应该作为一个 __switch
的最后一条路径,否则,在它之后的任何 __case
都不会得到调用。
__switch
要求至少两条路径选择。如果只存在一条路径时,使用 __optional
。
3.2.2. 找到合适的描述方式¶
在一个通用编程语言中,在面临路径选择时,你可以找到多种等价的描述方式。 为了让程序简洁,直观,我们应该选择最恰当的那一种。
同样的,对于本例,我们可以找到它的等价描述方式。如图所示:
__transaction
( __req(Action1)
, __optional(IsAction2RunFirst, __asyn(Action2))
, __asyn(Action3)
, __optional(__not(IsAction2RunFirst), __asyn(Action3))
, __rsp(Action4));
3.3. 异常处理¶
一个事务是一个不可分割的操作,它或许会包括多个步骤,但这些步骤要么全部成功,要么全部失败。
所以,一个事务从开始到结束,中间发生任何错误,都会导致整个事务的失败。一旦一个事务失败, 就会执行 回滚 (rollback)操作,以将系统恢复到事务开始前的状态。
当整个事务成功执行后,需要执行 提交 (commit) 操作,自此,整个事务对于系统的改动才算真正生效。 在提交后,整个系统无法再通过事务的 回滚 操作恢复系统的状态。
Transaction DSL 提供了同样的机制:使用 Transaction DSL 定义的任何事务, 在运行时, 如果中间某个操作发生了错误,则整个事务就进入失败的状态。
但不幸的是,对于一个具体的,由用户自己定义的事务而言,Transaction DSL 无从得知,当失败时,应该执行的具体回滚机制是什么。 所以 Transaction DSL 无法提供自动的回滚策略。或许对于某些系统,确实存在统一的模式,但另外一些系统则不然。
而在 Transaction DSL 的层面,则只能提供相应的机制;如果某些系统确实存在统一的回滚策略, 则可以利用这些机制在 Transaction DSL 之上层面进行统一定义。
如果没有统一的策略,同样可以利用 Transaction DSL 所提供的机制定义差异化的回滚操作。
3.3.1. __procedure¶
Transaction DSL 提供了 __procedure
来定义一个过程,无论这个过程中的所有操作全部成功,
还是执行到某一步时发生了失败,都会进入结束模式。用户可以自己定义结束模式里应该执行的操作是什么。
如果按照之前对于事务的描述,则用户可以在结束模式里根据过程进入结束模式时的状态,进行提交或回滚操作。
所以,__procedure
包含了两个参数:第一个参数是此过程应该执行的正常操作,
第二个的参数则是以 __finally
修饰的结束模式中应该执行的操作。
比如,对于 __optional
中的例子,如果系统要求此事务无论成败最终都应该执行 Action5
,
但如果失败的话则需要对之前的操作进行 回滚 。我们就可以将其描述为:
__procedure
( __req(Action1)
, __sync(Action2)
, __concurrent(__asyn(Action3), __asyn(Action4))
, __finally
( __rsp(Action5)
, __on_fail(__sync(Rollback))));
之所以额外提供 __procedure
的概念,是因为,通过它,用户可以在一个事务中定义多个过程,每个过程都可以利用这种机制,
从而让用户拥有更细力度的控制。例如,在下面的事务定义中, 就存在两个过程:
__transaction
( __procedure
( __asyn(Action1)
, __asyn(Action2)
, __finally(__rsp(Action3)))
, __asyn(Action4)
, __procedure
( __asyn(Action5)
, __finally(__sync(Action6))));
需要特别指出的是,过程自身也是一个操作,如果一个过程发生了失败,在其 __finally
里定义的操作执行结束之后,仍然会让导致整个事务失败。
比如,在本例中,如果 Action2
发生了失败,将会引起 Action3
的执行;
无论 Action3
执行成功还是失败,在它执行结束之后,均导致整个事务以终止运行。
所以,其后的操作并不会得到运行,即便它们被定义为 __procedure
。
当然, __procedure
是可以嵌套的,比如:
__transaction
( __asyn(Action1)
, __procedure
( __asyn(Action2)
, __finally(__sync(Action3)))
, __sync(Action4)
, __asyn(Action5))
, __finally(__sync(Action6)));
由于 __transaction
的最后一行是一个 __finally
,这就意味着本 __transaction
是一个 __procedure
,而
这个 __procedure
内部又嵌套了另外一个 __procedure
。
在这个事务中,如果 Action2
发生了错误, Action3
将会得到执行,
然后会跳过 Action4
和 Action5
,直接进入外层过程的 __finally
,执行 Action6
。
3.3.2. __procedure 的恢复¶
我们前面已经指出,一个 __procedure
,如果其主体部分发生了错误,会跳转到 __finally
,而无论 __finally
里的 Action
成功与否,最终整个 __procedure
都会以失败结束。
但是,如果你的确想让一个在主体失败了的 __procedure
有可能以成功方式结束,则不要使用 __finally
,而使用 __recover
。如果
主体部分失败,但 __recover
里的 Action 却成功了,则整个 __procedure
会在结束时返回成功。
所以,在下面的代码中,如果 Action1
发生失败,则会跳过 Action2
,转入执行 Action3
; 如果 Action3
执行成功,
则会继续执行 Action4
及后续过程。
__transaction
( __procedure
( __asyn(Action1)
, __asyn(Action2)
, __recover(__rsp(Action3)))
, __asyn(Action4)
, __procedure
( __asyn(Action5)
, __finally(__sync(Action6))));
但是,如果 Action3
执行失败,则仍然,整个过程就失败了,此时, Action4
及后续过程将不会得到执行。
而对于下面这个事务,如果 Action2
发生了失败,则会执行 Action3
,如果 Action3
执行成功,
则继续执行 Action4
及后续过程;否则,将跳过 Action4
和 Action5
,转入执行 Action6
,
如果 Action6
成功,则整个事务将依然是成功的,否则,事务将以失败结束。
__transaction
( __asyn(Action1)
, __procedure
( __asyn(Action2)
, __recover(__sync(Action3)))
, __sync(Action4)
, __asyn(Action5))
, __recover(__sync(Action6)));
所以, __recover
和 __finally
最大的不同的是,前者比后者多了一个给过程故障恢复的机会。