ZStack源码剖析之核心库鉴赏——FlowChain

本文首发于泊浮目的专栏:https://segmentfault.com/blog/camile

版本 日期 备注
1.0 2017.12.19 文章首发
1.1 2021.5.21 添加小结

前言

在ZStack(或者说产品化的IaaS软件)中的任务通常有很长的执行路径,错误可能发生在路径的任意一处。为了保证系统的正确性,需提供一种较为完善的回滚机制——在ZStack中,通过一个工作流引擎,ZStack的每一个步骤都被包裹在独立的工作流中,可以在出错的时候回滚。此外,通过在配置文件中组装工作流的方式,关键的执行路径可以被配置,这使得架构的耦合度进一步降低。

系统解耦合的手段除了之前文章所提到的分层、分割、分布等,还有一个重要手段是异步,业务之间的消息传递不是同步调用,而是将一个业务操作分成多个阶段,每个阶段之间通过共享数据的方式异步执行进行协作。

这即是一种在业务设计原则中——流程可定义原则的具象化。接触过金融行业的同学肯定知道,不同的保险理赔流程是不一样的。而承保流程和理赔流程是分离的,在需要时进行关联,从而可以复用一些理赔流程,并提供一些个性化理赔流程。

演示代码

就以创建VM为例,在ZStack中大致可以分以下几个步骤:

    <bean id="VmInstanceManager" class="org.zstack.compute.vm.VmInstanceManagerImpl">
        <property name="createVmWorkFlowElements">
            <list>
                <value>org.zstack.compute.vm.VmImageSelectBackupStorageFlow</value>
                <value>org.zstack.compute.vm.VmAllocateHostFlow</value>
                <value>org.zstack.compute.vm.VmAllocatePrimaryStorageFlow</value>
                <value>org.zstack.compute.vm.VmAllocateVolumeFlow</value>
                <value>org.zstack.compute.vm.VmAllocateNicFlow</value>
                <value>org.zstack.compute.vm.VmInstantiateResourcePreFlow</value>
                <value>org.zstack.compute.vm.VmCreateOnHypervisorFlow</value>
                <value>org.zstack.compute.vm.VmInstantiateResourcePostFlow</value>
            </list>
        </property>
<!--  还有很多,介于篇幅不再列出 -->

可以说是代码即文档了。在这里,ZStack显式声明这些Flow在Spring XML中,这些属性将会被注入到createVmWorkFlowElements中。每一个Flow都被拆成了一个个较小的单元,好处不仅是将业务操作分成了多个阶段易于回滚,还是可以有效复用这些Flow。这也是编程思想中“组合”的体现。

如何使用

除了这种配置型声明,还可以在代码中灵活的使用这些FlowChain。在这里,我们将以Case来说明这些FlowChain的用法,避免对ZStack业务逻辑不熟悉的读者看的一头雾水。

一共有两种可用的FlowChain:

  • SimpleFlowChain
  • ShareFlowChain

SimpleFlowChain

我们先来看一个Case

    @Test
    public void test() {
        FlowChain chain = FlowChainBuilder.newShareFlowChain();

        chain.then(new ShareFlow() {
            int a;

            @Override
            public void setup() {
                flow(new NoRollbackFlow() {
                    @Override
                    public void run(FlowTrigger trigger, Map data) {
                        a = 1;
                        increase();
                        trigger.next();
                    }
                });

                flow(new NoRollbackFlow() {
                    @Override
                    public void run(FlowTrigger trigger, Map data) {
                        a = 2;
                        increase();
                        trigger.next();
                    }
                });
            }
        }).done(new FlowDoneHandler(null) {
            @Override
            public void handle(Map data) {
                success = true;
            }
        }).start();

        Assert.assertTrue(success);
        expect(2);
    }

我们可以看到,这就是一个工作流。完成一个工作流的时候(回调触发时)执行下一个工作流——由trigger.next触发。不仅如此,还可以添加Rollback属性

    @Test
    public void test() throws WorkFlowException {
        final int[] count = {0};

        new SimpleFlowChain()
                .then(new Flow() {
                    @Override
                    public void run(FlowTrigger chain, Map data) {
                        count[0]++;
                        chain.next();
                    }

                    @Override
                    public void rollback(FlowRollback chain, Map data) {
                        count[0]--;
                        chain.rollback();
                    }
                })
                .then(new Flow() {
                    @Override
                    public void run(FlowTrigger chain, Map data) {
                        count[0]++;
                        chain.next();
                    }

                    @Override
                    public void rollback(FlowRollback chain, Map data) {
                        count[0]--;
                        chain.rollback();
                    }
                })
                .then(new Flow() {
                    @Override
                    public void run(FlowTrigger chain, Map data) {
                        chain.fail(null);
                    }

                    @Override
                    public void rollback(FlowRollback chain, Map data) {
                        count[0]--;
                        chain.rollback();
                    }
                })
                .start();

        Assert.assertEquals(-1, count[0]);
    }

rollback由FlowTrigger的fail触发。这样我们可以保证在发生一些错误的时候及时回滚,防止我们的系统处于一个有脏数据的中间状态。同时,Map也可以用来在Flow之间传递上下文。

ShareFlowChain

public class TestShareFlow {
    int[] count = {0};
    boolean success;

    private void increase() {
        count[0]++;
    }

    private void decrease() {
        count[0]--;
    }

    private void expect(int ret) {
        Assert.assertEquals(count[0], ret);
    }

    @Test
    public void test() {
        FlowChain chain = FlowChainBuilder.newShareFlowChain();

        chain.then(new ShareFlow() {
            int a;

            @Override
            public void setup() {
                flow(new NoRollbackFlow() {
                    @Override
                    public void run(FlowTrigger trigger, Map data) {
                        a = 1;
                        increase();
                        trigger.next();
                    }
                });

                flow(new NoRollbackFlow() {
                    @Override
                    public void run(FlowTrigger trigger, Map data) {
                        a = 2;
                        increase();
                        trigger.next();
                    }
                });
            }
        }).done(new FlowDoneHandler(null) {
            @Override
            public void handle(Map data) {
                success = true;
            }
        }).start();

        Assert.assertTrue(success);
        expect(2);
    }


    @Before
    public void setUp() throws Exception {
        new BeanConstructor().build();
    }
}

比起SimpleFlowChain,ShareFlowChain则是一个Inner class,在相同的作用域里,传递数据变得更加的方便了。

它的实现

在ZStack中,FlowChain作为核心库,其实现也是非常的简单(可以直接参考SimpleFlowChainShareFlowChain),本质就是将任务放入List中,由内部方法进行迭代,在此基础上做了一系列操作。下面将开始分析它的源码。

从接口说起

public interface FlowChain {
    List<Flow> getFlows();

    FlowChain insert(Flow flow);

    FlowChain insert(int pos, Flow flow);

    FlowChain setFlowMarshaller(FlowMarshaller marshaller);

    FlowChain then(Flow flow);

    FlowChain done(FlowDoneHandler handler);

    FlowChain error(FlowErrorHandler handler);

    FlowChain Finally(FlowFinallyHandler handler);

    FlowChain setData(Map data);

    FlowChain putData(Map.Entry... es);

    FlowChain setName(String name);

    void setProcessors(List<FlowChainProcessor> processors);

    Map getData();

    void start();

    FlowChain noRollback(boolean no);

    FlowChain allowEmptyFlow();
}

接口的名字非常的易懂,那么在这里就不多作解释了。FlowChain仅仅定义了一个Flow最小应有的行为。

//定义了Flow的回滚操作接口
public interface FlowRollback extends AsyncBackup {
    //回滚操作
    void rollback();
    //设置跳过回滚操作
    void skipRestRollbacks();
}
//定义了触发器的行为接口
public interface FlowTrigger extends AsyncBackup {
    //触发失败,调用errorHandle
    void fail(ErrorCode errorCode);
    //触发下一个flow
    void next();
    //setError后,在下次调用next的时才会调用errorHandle
    void setError(ErrorCode error);
}

源码解析

Flow

public interface Flow {
    void run(FlowTrigger trigger, Map data);

    void rollback(FlowRollback trigger, Map data);
}

Flow的定义其实非常的简单——一组方法。执行和对应的回滚,一般在ZStack中都以匿名内部类的方式传入。

Chain的用法

在之前的SimpleFlowChain的case中。我们可以看到一系列的链式调用,大致如下:

new SimpleFlowChain().then(new flow()).then(new flow()).then(new flow()).start();

then本质是往List<flow> flows里添加一个flow。

    public SimpleFlowChain then(Flow flow) {
        flows.add(flow);
        return this;
    }

再来看看start

    @Override
    public void start() {
        // 检测flow中是否设置了processors。一般用来打trace
        if (processors != null) {
            for (FlowChainProcessor p : processors) {
                p.processFlowChain(this);
            }
        }
        //如果flows为空但是之前在设置中允许为空,那么就直接直接done部分的逻辑。不然就报错
        if (flows.isEmpty() && allowEmptyFlow) {
            callDoneHandler();
            return;
        }

        if (flows.isEmpty()) {
            throw new CloudRuntimeException("you must call then() to add flow before calling start() or allowEmptyFlow() to run empty flow chain on purpose");
        }
        //每个flow必须有一个map,用来传递上下文
        if (data == null) {
            data = new HashMap<String, Object>();
        }
        //标记为已经开始
        isStart = true;
        //如果没有名字的话给flow 取一个名字,因为很有可能是匿名使用的flow
        if (name == null) {
            name = "anonymous-chain";
        }

        logger.debug(String.format("[FlowChain(%s): %s] starts", id, name));
        //打印trace,方便调试
        if (logger.isTraceEnabled()) {
            List<String> names = CollectionUtils.transformToList(flows, new Function<String, Flow>() {
                @Override
                public String call(Flow arg) {
                    return String.format("%s[%s]", arg.getClass(), getFlowName(arg));
                }
            });
            logger.trace(String.format("execution path:\n%s", StringUtils.join(names, " -->\n")));
        }
        //生成一个迭代器
        it = flows.iterator();
        //从it中获取一个不需要跳过的flow开始执行。如果没有获取到,就执行done逻辑
        Flow flow = getFirstNotSkippedFlow();
        if (flow == null) {
            // all flows are skipped
            callDoneHandler();
        } else {
            runFlow(flow);
        }
    }

再来看一下runFlow中的代码

    private void runFlow(Flow flow) {
        try {
            //看报错信息就可以猜到在做什么防御措施了:如果一个transaction在一个flow中没有被关闭而跳到下一个flow时,会抛出异常。这个防御机制来自于一个实习生写的bug,当时被排查出来的时候花了非常大的力气——现象非常的诡异。所以现在被写在了这里。
            if (TransactionSynchronizationManager.isActualTransactionActive()) {
                String flowName = null;
                String flowClassName = null;
                if (currentFlow != null) {
                    flowName = getFlowName(currentFlow);
                    flowClassName = currentFlow.getClass().getName();
                }

                throw new CloudRuntimeException(String.format("flow[%s:%s] opened a transaction but forgot closing it", flowClassName, flowName));
            }
            //toRun就是一个当前要run的flow
            Flow toRun = null;
            if (flowMarshaller != null) {
            //flowMarshaller 实际上是一个非常恶心的玩意儿。尤其在一些配置好掉的xml flow突然因为一些条件而改变接下来执行的flow令人很无语...但是也提供了一些灵活性。
                toRun = flowMarshaller.marshalTheNextFlow(currentFlow == null ? null : currentFlow.getClass().getName(),
                        flow.getClass().getName(), this, data);
                if (toRun != null) {
                    logger.debug(String.format("[FlowChain(%s): %s] FlowMarshaller[%s] replaces the next flow[%s] to the flow[%s]",
                            id, name, flowMarshaller.getClass(), flow.getClass(), toRun.getClass()));
                }
            }
       
            if (toRun == null) {
                toRun = flow;
            }

            if (CoreGlobalProperty.PROFILER_WORKFLOW) {
                //对flow的监视。比如flow的执行时间等
                stopWatch.start(toRun);
            }

            currentFlow = toRun;

            String flowName = getFlowName(currentFlow);
            String info = String.format("[FlowChain(%s): %s] start executing flow[%s]", id, name, flowName);
            logger.debug(info);
            //在flow中还允许定义afterDone afterError afterFinal的行为。稍后将会介绍
            collectAfterRunnable(toRun);
            //终于到了run,这里就是调用者传入的行为来决定run中的逻辑
            toRun.run(this, data);
             //fail的逻辑稍后解析
        } catch (OperationFailureException oe) {
            String errInfo = oe.getErrorCode() != null ? oe.getErrorCode().toString() : "";
            logger.warn(errInfo, oe);
            fail(oe.getErrorCode());
        } catch (FlowException fe) {
            String errInfo = fe.getErrorCode() != null ? fe.getErrorCode().toString() : "";
            logger.warn(errInfo, fe);
            fail(fe.getErrorCode());
        } catch (Throwable t) {
            logger.warn(String.format("[FlowChain(%s): %s] unhandled exception when executing flow[%s], start to rollback",
                    id, name, flow.getClass().getName()), t);
            fail(errf.throwableToInternalError(t));
        }
    }

fail

    @Override
    public void fail(ErrorCode errorCode) {
        isFailCalled = true;
        setErrorCode(errorCode);
        //放入Stack中,之后Rollback会根据Stack中的flow顺序来
        rollBackFlows.push(currentFlow);
        //rollback会对this.rollBackFlows中flow按照顺序调用rollback
        rollback();
    }

FlowTrigger

//定义了触发器的行为接口
public interface FlowTrigger extends AsyncBackup {
    //触发失败,调用errorHandle
    void fail(ErrorCode errorCode);
    //触发下一个flow
    void next();
    //setError后,在下次调用next的时才会调用errorHandle
    void setError(ErrorCode error);
}

之前已经看过fail的代码。接下来来看看nextsetError

    @Override
    public void next() {
        //如果flow没有run起来的情况下,是不能调用next的
        if (!isStart) {
            throw new CloudRuntimeException(
                    String.format("[FlowChain(%s): %s] you must call start() first, and only call next() in Flow.run()",
                            id, name));
        }
        //当rollback开始的时候也不允许next
        if (isRollbackStart) {
            throw new CloudRuntimeException(
                    String.format("[FlowChain(%s): %s] rollback has started, you can't call next()", id, name));
        }
        //将当前flow的push进rollback用的stack
        rollBackFlows.push(currentFlow);

        logger.debug(String.format("[FlowChain(%s): %s] successfully executed flow[%s]", id, name, getFlowName(currentFlow)));
        //获取下一个flow。在这里才是真正意义上的next
        Flow flow = getFirstNotSkippedFlow();
        if (flow == null) {
            // no flows, or all flows are skipped
            if (errorCode == null) {
                callDoneHandler();
            } else {
                callErrorHandler(false);
            }
        } else {
            runFlow(flow);
        }
    }

可以看一下getFirstNotSkippedFlow,本质上是利用了迭代器的特性。

    private Flow getFirstNotSkippedFlow() {
        Flow flow = null;
        while (it.hasNext()) {
            flow = it.next();
            if (!isSkipFlow(flow)) {
                break;
            }
        }

        return flow;
    }

接下来是setError

    @Override
    public void setError(ErrorCode error) {
        setErrorCode(error);
    }

//往下看
    private void setErrorCode(ErrorCode errorCode) {
        this.errorCode = errorCode;
    }

根据之前的next逻辑:

        if (flow == null) {
            // no flows, or all flows are skipped
            if (errorCode == null) {
                callDoneHandler();
            } else {
                callErrorHandler(false);
            }
        } else {
            runFlow(flow);
        }

我们可以大致猜想到,如果在next的时候当前error不为空,则调用错误handle。这样在setError后还可以做一些事情。

无论是调用errorHandle还是doneHandle,都会调用finalHandle。finalHandle也允许用户定义这部分的逻辑,使flow更加的灵活。

更好的选择

由于该库是为ZStack定制而生,故此有一些防御性判断,源码显得略为verbose。如果有同学对此感兴趣,想将其应用到自己的系统中,笔者推荐使用:jdeferred

Java Deferred/Promise library similar to JQuery

由于JavaScript 中的代码都是异步调用的。简单说,它的思想是,每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数。

在这里列出几个较为简单的示范,或者有兴趣的读者也可以参考这里

import org.jdeferred.DeferredManager;
import org.jdeferred.Promise;
import org.jdeferred.impl.DefaultDeferredManager;
import org.junit.After;
import org.junit.Assert;
import org.junit.Test;

import java.util.concurrent.TimeUnit;


public class deferSimpleTest {

    private static int var = 0;
    final DeferredManager dm = new DefaultDeferredManager();

    @After
    public void cleanUp() {
        var = 0;
    }


    @Test
    public void test() {
        Promise p1 = dm.when(() -> {
            var += 1;
        }).then(result -> {
            var += 1;
        });

        Promise p2 = dm.when(() -> {
            var += 1;
        }).then(result -> {
            var += 1;
        });

        dm.when(p1, p2).done(Void -> var += 1);
        Assert.assertEquals(5, var);
    }

    @Test
    public void test2() {
        final DeferredManager dm = new DefaultDeferredManager();

        Promise promise = dm.when(() -> {
                var += 1;
            }).then(result -> {
                var += 1;
            });

        dm.when(promise).done(Void -> var += 1);
        Assert.assertEquals(3, var);
    }

    @Test
    public void testBadCallback() {
        Promise promise = dm.when(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        dm.when(promise).done(Void -> {
                    var += 1;
                    throw new RuntimeException("this exception is expected");
                }
        ).fail(Void -> {
            System.out.print("fail!");
            var -= 1;
        });
        Assert.assertEquals(0, var);

    }
}

如果你在使用Java8,那么也可以通过CompletableFuture来得到“类似”的支持。

小结

本文和大家一起了解了FlowChain的实现,但其实这并不是什么新颖的东西。该组件的思想参考了SAGA——SAGA 事务模式的历史十分悠久,比分布式事务的概念提出还要更早。SAGA 的意思是“长篇故事、长篇记叙、一长串事件”,它起源于 1987 年普林斯顿大学的赫克托 · 加西亚 · 莫利纳(Hector Garcia Molina)和肯尼斯 · 麦克米伦(Kenneth Salem)在 ACM 发表的一篇论文《SAGAS》(这就是论文的全名)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容