首页 > 代码库 > pipeline-filter模式变体之尾循环

pipeline-filter模式变体之尾循环

pipeline-filter作为一种处理数据的模式(见【POSA】卷4)可以将应用的任务划分为几个自我完备的数据处理步骤,并连接到一个数据管道。本文介绍一种不太常见的pipeline-filter的变体——尾循环的pipeline-filter,当然这也是在特定的需求场景下才会出现的。

首先,我们来看一个常见的pipeline-filter的模式图:


模式的思路比较简单明了,就是对数据的处理步骤进行拆分。然后以统一的编程接口加上递归的方式,将它们串在一起。

最近在写的一个Message broker中处理消息通信的时候也采用了这种模式来切分消息处理步骤。在发送消息的时候这种模式使用得非常的顺畅,因此很自然得在接收消息的时候同样采用了这种模式。我们可以先来简单来看一个发送消息的时候整个pipeline就是像下面这样:


关于名词的说明:本文中谈到的handler可以类比为filter,提到的handler-chain可以类比为pipeline,只是叫法不同。

如果不去多想,接收消息的pipeline也应该跟发送消息类似,只是produce-handler变成了consume-handler。但在代码实现的时候,rabbitmq-java-client的实现方式使得这种模式的运用有些受阻。正常情况下,我们的理解是一个消息的处理或一个消息集合的处理,会穿过一个pipeline,但官方提供的java-client对于接收消息的实现是socket-blocking以等待消息推送到client的(push)方式。因为官方client的这种实现方式,使得外部封装的做法最好是将socket-blocking搬迁到一个独立的EventLoop-Thread上,然后获取到消息之后,解封送并以事件的方式对外提供消息,而客户端在该事件中实现自己的处理逻辑即可,也就是说是一种异步接收的方式,仔细想想也确实应该是这种push的方式。

由此可见在接收消息时的pipeline还是很不同于发送消息的。对接收消息而言,filter分成两个部分,第一部分的多个filter只执行一次(主要是在真正开始接收消息之前,处理一些前置任务,比如权限检查,参数验证等);第二部分的多个filter却要不断得在另外一个EventLoop-Thread上循环执行(因为这些filter涉及到对接收到的message进行处理)。所以,在接收消息时的示意图为:


其中,下面框起来的两个handler是在EventLoop-Thread上循环执行的。

显然,上面用于produce的那种pipeline-filter不能应对这种变化(既无法跨线程也无法在就其中的几个进行循环)。而此时不可能独立得为consume单独实现一套新的pipeline-filter(因为在pipeline-filter的基础设施上,我们已经将produce,consume以及request,response、publish,subscribe等都抽象为消息传输(carry))。因此,我们只能在同一套运行机制上,找到一种办法来满足所有的消息传输方式。

我们的做法是,实现一个“过渡handler”(此处的handler即为filter,只是取名不同而已),并实现handle方法。该handler用于将后续的逻辑过渡到一个独立的EventLoop-Thread上并启动EventLoop-Thread(把传递给当前handler的上下文以及chain对象传递到事件处理线程上去),其后的所有handler都在该线程上循环执行。

其实现代码段如下:

public void handle(@NotNull MessageContext context,
                       @NotNull IHandlerChain chain) {
        if (!context.isSync()) {
            ReceiveEventLoop eventLoop = new ReceiveEventLoop();
            eventLoop.setChain(chain);
            eventLoop.setContext(context);
            eventLoop.setChannelDestroyer(context.getDestroyer());
            eventLoop.setCurrentConsumer((QueueingConsumer) context.getOtherParams().get("consumer"));
            context.setReceiveEventLoop(eventLoop);

            //repeat current handler
            ((MessageCarryHandlerChain) chain).setEnableRepeatBeforeNextHandler(true);

            eventLoop.startEventLoop();
        } else {
            chain.handle(context);
        }
    }

如上,ReceiveEventLoop即为一个独立的EventLoop-Thread,启动之后,对于pipeline发起的线程而言,它启动的本次调用链(handle方法的递归调用)已经结束。因此主线程将会从该调用的触发点向下继续执行

而后续的filter在eventloop线程上独立运行:

public void run() {
    try {
        while (true) {
            QueueingConsumer.Delivery delivery = this.currentConsumer.nextDelivery();

            AMQP.BasicProperties properties = delivery.getProperties();
            byte[] msgBody = delivery.getBody();

            context.getChannel().basicAck(delivery.getEnvelope().getDeliveryTag(), false);

            //.....

            this.context.setConsumedMsg(msg);
            this.chain.handle(this.context);
        }
    } catch (InterruptedException e) {
        logger.info("[run] close the consumer‘s message handler!");
    } catch (IOException e) {
        logger.error("[run] occurs a IOException : " + e.getMessage());
        this.shutdown();
    } catch (ConsumerCancelledException e) {
        logger.info("[run] the consumer has been canceled ");
        this.shutdown();
    } catch (Exception e) {
        logger.error("[run] occurs a Exception : " + e.getMessage());
        this.shutdown();
    }

    logger.info("******** thread id " + this.getThreadID() + " quit from message receiver ********");
}

有一个问题:我们必须拿到对EventLoop-Thread的控制权(外部可以在任意时刻关闭该eventloop),而获取其控制权的关键代码就是上上段代码中的:

context.setReceiveEventLoop(eventLoop);

我们将当前EventLoop的实例包装成一个Thread类型的属性,并为其开放了相应的开、关方法,将其控制权丢给外部:

public void startEventLoop() {
      this.currentThread.start();
}

public void shutdown() {
      this.channelDestroyer.destroy(context.getChannel());
      this.currentThread.interrupt();
}

然后在主线程发起接收消息的方法最后会返回一个IReceiverCloser接口的实例,在其接口方法close中调用shutdown,来对其进行关闭:

//launch pipeline
        carry(ctx);

        return new IReceiverCloser() {
            @Override
            public void close() {
                synchronized (this) {
                    if (ctx.getReceiveEventLoop().isAlive()) {
                        ctx.getReceiveEventLoop().shutdown();
                    }
                }
            }
        };

另一个问题,handler-chain是如何知道从某个handler之后转入eventloop线程会开始循环执行?是如何实现的?它来自于第一段代码中的如下这句代码:

((MessageCarryHandlerChain) chain).setEnableRepeatBeforeNextHandler(true);
这句代码会在进入下一个handler之前设置一个“循环执行”的标志。下面看看,我们是如何来改造handlerchain来达到这个目的的,在MessageCarryHandlerChain(它实现了IHandlerChain接口)的实现中,有如下四个变量:

    private List<AbstractHandler> handlerChain;
    private int     pos          = 0;
    private boolean enableRepeat = false;
    private int     repeatPos    = -1;

  • handlerChain:解析并顺序存储每个handler
  • pos:用来记录当前已经执行到哪个handler
  • enableRepeat:用来标识是否启用重复执行handler
  • repeatPos:用于记录重复执行的handler的起始位置
在设置启用enableRepeat属性的时候,会记录当前的位置状态:
public void setEnableRepeatBeforeNextHandler(boolean enableRepeat) {
        this.enableRepeat = enableRepeat;
        if (this.enableRepeat) {
            this.repeatPos = this.pos;
        } else {
            this.repeatPos = Integer.MIN_VALUE;
        }
    }

MessageCarryChain的handle实现,这也是handler串接的核心:
public void handle(MessageContext context) {
        if (this.repeatPos != Integer.MIN_VALUE) {
            if (this.pos < handlerChain.size()) {
                AbstractHandler currentHandler = handlerChain.get(pos++);
                currentHandler.handle(context, this);
            } else if (this.enableRepeat) {
                this.pos = this.repeatPos;
            }
        }
    }
在处理第一条到来的消息时,对应到上面while(true)中的最后一句:
this.chain.handle(this.context);

调用会进入MessageCarryChain的handle方法,并执行:
if (this.pos < handlerChain.size()) {

}

判断分支,在其中会触发下一个handler的handle方法,并一直执行下去直到判断条件不成立之后会执行else逻辑,将之前保存的起始循环的handler的位置置为新的handler的位置。
于是,当下一次,事件循环在上面while(true)中收到新的消息时,会再次执行上面的if判断分支(因为在接收上一条消息时,已将pos重置了,所以If判断条件又重新成立)并以循环位置的handler作为起始,直到走到handlerChain中的最后一个之后,又将当前位置的pos重置为repeatPos保存的位置(注意repeatPos在第一次被设置之后不再改变),如此周而复始。也就在表面上形成了上图所示的尾循环。
为什么说只是表面上形成了呢。因为在表述中,我故意“忽略”了这样一个现实——pipeline-filter模式根本上还是利用了递归来实现的,递归会有个还原点,用于“保护现场”之后再“还原现场”。因此,上面MessageCarryChain中的handle代码段中的:
currentHandler.handle(context, this);

对于每个被执行的handler都是还原点,当第一轮handler执行完成(调用完这句:this.pos = this.repeatPos;)都会在还原点层层回退(执行还原点之后的该方法内部的剩余代码)。因此,在收到第二个消息时,其实是触发了新一轮的handler-chain执行流程,只是因为pos在之前被置为chain中的循环起始位置,而不是从0开始而已。但对于后面尾循环的handler而言,它们递归调用的本质没有改变,所以其实只是看起来在尾部“循环”而已。
说明:其实如果你回顾pipeline-filter模式的本质,它们是用来处理数据的。我这里不管是发送还是接收消息兼顾处理了消息之外的一些逻辑。如果这里只处理消息,其实是可以不用跨线程以及尾循环的。我只是利用了这种模式,将消息通信的各个环节进行拆分,组合,重用从而不可回避得遇到了这个问题,如果回到纯粹的pipeline-filter模式,是不需要这么做的。
如果我没表述清楚的,请直接看代码:Messagebus-Consume

pipeline-filter模式变体之尾循环