我有一个复杂的系统,涉及许多救援人员、工作和监控过程。作业具有父子依赖关系,并且它们通过一系列状态(使用状态机)运行,这就是监视过程的原因。我们依靠数据库状态来确保跨进程跟踪是同步的。
这是一个粗略的想法:
class ParentMonitor < ActiveRecord::Base
has_many children, class: ChildMonitor
state_machine :state, initial: :work_needed do
event :succeed do
transition :work_needed => :work_succeeded
end
event :fail do
transition :work_needed => :work_failed
end
end
def child_transition
return if children.any? { |child| child.work_needed? }
if children.any? { |child| child.work_succeeded? }
succeed
else
fail
end
end
end
class ChildMonitor < ActiveRecord::Base
belongs_to: owner, class: ParentMonitor
state_machine :state, initial: :work_needed do
event :succeed do
transition :work_needed => :work_succeeded
end
after_transition :to => :work_succeeded, :do => :notify_owner
event :fail do
transition :work_needed => :work_failed
end
after_transition :to => :work_failed, :do => :notify_owner
end
def notify_owner
owner.child_transition
end
end
发生的事情是,对于最初的几个这样的作业(比如几百个作业中的一打或两个),即使所有的子作业都在work_succeeded
或work_failed
中,ParentMonitors仍然处于work_needed
状态。通过跟踪和测试,我确定每次调用ParentMonitor#child_transition
时,处于"工作需要"状态的子列表都被依次减少,直到某一点它使数据库加载并将所有子列表替换为"工作需要"的值。尽管有些已经完成了。
此外,我没有看到任何UPDATE
日志在日志文件中为这些前几个孩子,直到它突然开始记录更新。当它似乎重置所有子线程的状态时,日志记录是同步的。
这让我认为这些变化都发生在内存中,由于一些缓存状态,但我已经添加了reload
, save
和find
调用,他们似乎没有影响变化。我也试过在uncache
中包装这些调用,但没有帮助。
事实证明,这是由于写操作是在长时间运行的事务中进行的,因为状态机gem在状态更改和任何after
钩子结束之间保持打开一个事务。我们已经编写了在主监视循环上运行几个小时的钩子。
我们通过在状态更改之间而不是在回调中执行操作来解决这个问题。
顺便说一下,错误的行为与最新的红皮书中描述的完全一样,即大多数RDMBS中实现的"弱隔离"并发的副作用:
异常的例子包括读取另一个事务产生的中间数据,读取被终止的数据,在执行同一事务期间为同一项读取两个或多个不同的值,以及由于并发写入同一项而"丢失"事务的一些影响