这是从一个小服务器里碰到的小问题提取出来的小模式。

要解决的问题

在上一个项目里,我们发现系统里的数据流动有这两种模式:

  1. 业务逻辑 -> 客户端
  2. 业务逻辑 -> 其他业务逻辑 -> ...

说白了就是,一个业务逻辑需要有面向客户端面向其他业务 逻辑的两套接口。在数据入口处,我们已经做了处理,把客户端过 来的数据和其他业务逻辑过来的数据统一起来了:

1
2
3
4
5
Client -> Protocol Decoder >---------+
                                     |
                                     +--> Business Logic
                                     |
Other Business Logic >---------------+

这是因为所有的协议数据都是同构的,而且来源相同(都来自网络层), 要实现“Protocol Decoder”很简单;但是在数据出口处,有下面 两个原因导致我们没办法也对出去的数据做统一处理:

  1. 我们用gen_server处理请求,直接向客户端回复的数据没必要用 同步call,让业务逻辑所在的进程直接发送有助于提高系统的并发 性能,所以在具体模块的实现上就必须区分是向客户端回复还是向 其他业务逻辑回复:客户端的数据用cast,其他业务逻辑的数据用 call;
  2. 业务逻辑之间的数据,来源和结构都不一致,在用gen_server的 情况下比较难处理得好。

所以我们所有的旧代码都是在业务逻辑中写死了数据流向,而没有对出 去的数据做routing,即便是相同的逻辑,也要提供两个接口,一个面 向客户端,一个面向其他业务逻辑。

解决思路

分析一下不难看出,现在是业务逻辑对数据流向有很强的依赖,那应该 可以把业务逻辑和数据流向分开来,业务逻辑只提供数据,至于这些数 据应该去哪,由别的规则确定。这个方案最简单的实现就是,由调用者 (也就是数据入口处)决定数据出去时的流向,因为数据入口处实际上 只有两种情况:从网络来的请求从其他业务逻辑来的请求

1
2
3
4
5
6
7
8
9
Client -> Protocol Decoder >-+                          +----> Client
                             |                          |
                  Set Destination to Client             |
                             |                          |
                             +------> Business Logic >--+
                             |                          |
          Set Destination to Other Business Logic       |
                             |                          |
Other Business Logic >-------+                          +----> Other Business Logic

gen_cb

gen_cb就是把数据流向交给调用者决定的实现。原理很简单,调用 者的每个请求都要提供两个回调函数,一个由本地代码执行,一个由 处理请求的进程执行,通过两个回调的配合,可以把具体请求调整为 异步或者同步请求,所以实现gen_cb的模块不需要考虑同步异步之 类的事情。

同步调用:

1
gen_cb:call(Dest, Message, gen_cb:receive_cb/1, gen_cb:reply_cb/1).

异步调用:

1
gen_cb:call(Dest, Message, none, none)

当然,由于实现方式很灵活,gen_cb能干的不止是同步/异步的调 整,你还可以把各种回调串起来,为某个模块添加各种filter和hook……

限制

在Erlang节点之间传递函数变量是有点危险的事情,因为需要保证节 点之间的代码是一致的,所以不鼓励在分布式环境里用gen_cb,除 非你真的知道自己在干什么。

另外在实现细节里,ge_cb传递的Replier回调(见代码)会 有一块闭包,所以gen_cb的消息要比gen_server大上不少。

代码

托管在Github上,可以当作rebar依赖直接用。

TODO

  1. 添加测试;
  2. 添加休眠(hibernation)代码切换(code change)支持;
  3. 添加license.