月度归档:2013年09月

gen_server:call/3超时异常时重启策略无效?

今天尝试了一个带监督进程的应用程序,程序功能是根据输入参数返回fibonacci数列上的数,当输入参数不合法时,程序会崩溃退出,由监督进程根据指定策略重启worker进程。程序基本结构如下(截取片段):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
-module(fibo_server).
-behaviour(gen_server).
-export([init/1, handle_call/3, terminate/2]).
-export([handle_cast/2, handle_info/2, code_change/3]).
-export([start/0, get_fibo/1]).
-record(state, {}).
 
start() ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
 
get_fibo(Seq) ->
    gen_server:call(?MODULE, Seq, 10000).
 
fibo(0) ->1;
fibo(1) ->1;
fibo(N) when is_integer(N) -> fibo(N-1) + fibo(N-2).
 
%% ====================================================================
%% Behavioural functions 
%% ====================================================================
init([]) ->
    io:format("~p init.~n", [?MODULE]),
    process_flag(trap_exit, true),
    {ok, #state{}}.
 
handle_call(Request, From, State) ->
    Reply = fibo(Request),
    {reply, Reply, State}.
 
terminate(Reason, State) ->
    io:format("~p terminate.~n", [?MODULE]),
    ok.

fibo_server对客户端暴露get_fibo/1函数,以Seq作为传入参数,get_fibo/1再调用gen_server:call/3函数,间接会调用内部函数fibo/1来获取结果,fibo/1函数实现了fibonacci数列的递归定义。在这个fibo/1函数的实现里,当传入参数Seq为非整数时,程序会因找不到匹配的函数子句而退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-module(fibo_sup).
-behaviour(supervisor).
-export([init/1]).
-export([start/0]).
 
start() ->
    {ok, Pid} = supervisor:start_link({local, ?MODULE}, ?MODULE, []),
    unlink(Pid).
 
%% ====================================================================
%% Behavioural functions 
%% ====================================================================
init([]) ->
    FiboServerChild = {fibo_server,{fibo_server,start,[]},
        permanent,2000,worker,[fibo_server]},
    {ok,{{one_for_one,3,10}, [FiboServerChild]}}.

监督者进程fibo_sup对worker进程的重启策略为one_for_one,10s内最多重启3次。

运行程序如下,先启动监督者进程,再由监督者启动worker进程,正常工作:

1
2
3
4
5
1> fibo_sup:start().
fibo_server init.
true
2> fibo_server:get_fibo(5).
8

此时输入非法参数a,程序抛出function_clause异常,崩溃退出,从日志中可以看出,fibo_server进行过一次重启。此时输入合法参数,fibo_server能正常返回,说明监督者进程的重启策略起作用了:

1
2
3
4
5
6
7
8
9
10
11
12
13
3> fibo_server:get_fibo(a).
fibo_server terminate.
fibo_server init.
 
** exception exit: {{function_clause,[{fibo_server,fibo,
    [a], [{file,"../fibo/src/fibo_server.erl"}, {line,130}]},
    {fibo_server,handle_call,3, [{file,"../fibo/src/fibo_server.erl"},
    {line,64}]}, {gen_server,handle_msg,5, [{file,"gen_server.erl"},{line,588}]},
    {proc_lib,init_p_do_apply,3, [{file,"proc_lib.erl"},{line,227}]}]},
    {gen_server,call,[fibo_server,a,10000]}}
    in function  gen_server:call/3 (gen_server.erl, line 188)
4> fibo_server:get_fibo(5).
8

此时输入一个大一点的整数,比如100,递归计算第100个fibonacci数的计算量很大,程序会阻塞在gen_server:call/3上,无法在10秒之内返回。结果表明,程序因timeout异常退出,再输入小整数也会抛超时异常,worker进程未获得重启,这与预期不一致:

1
2
3
4
5
6
5> fibo_server:get_fibo(100).
** exception exit: {timeout,{gen_server,call,[fibo_server,100,10000]}}
     in function  gen_server:call/3 (gen_server.erl, line 188)
6> fibo_server:get_fibo(5).
** exception exit: {timeout,{gen_server,call,[fibo_server,5,10000]}}
     in function  gen_server:call/3 (gen_server.erl, line 188)

为何监督者进程的重启策略对function_clause异常生效,对gen_server:call/3引起的timeout异常却不生效?

后来请教了同事,原来是自己理解有偏差,没理解gen_server行为模式的本质,虽然我们只有一个文件(fibo_server.erl),但实际上这个实现了gen_server行为模式的fibo_server模块是一种C/S架构的程序,可以理解为Erlang运行时运行着一个服务器,我们从客户端调用get_fibo/1函数时调用了gen_server:call/3函数,这个函数是个rpc调用,在另外一个进程(服务器进程)执行,与一般C/S程序不同的是,gen_server行为模式要求我们来提供回调函数,供服务器进程调用。gen_server实现了一个通用服务器的程序框架,客户端调用和服务器处理客户端请求的支配权都在程序员手上。例子中的function_clause异常和timeout异常,一个是服务器端错误,是服务器程序调用回调函数时发生错误崩溃退出;一个是客户端超时异常,服务器端的程序仍在正常运行,等执行完毕后返回,只是客户端等不及了。之前我把function_clause异常和timeout异常理解为是同一种类型异常,认为它们都会导致fibo_server程序崩溃退出,以至于产生了错误的期望。

回去查gen_server行为模式的man page发现,上面有说到gen_server:call/2函数在指定的Timeout时间内未返回时会抛出timeout异常,表示此次调用失败,如果这个异常被捕捉了,客户端程序继续运行,那么服务器进程处理完handle_call/3函数后会将消息发送到客户端进程的信箱中,时间未知,所以健壮性好点的客户端程序必需考虑将这类已过期的消息过滤或丢弃掉。

--EOF--

MySQL无法插入Emoji表情问题

测试发现iPhone提交的Emoji表情无法插入MySQL数据库,Java层抛出的异常显示为:

1
2
java.sql.SQLException: Incorrect string value: '\xF0\x9F\x98\x84...' 
    for column 'remark' at row 1

解决这个问题的方法是将MySQL(5.5.3 or later)的字符集从utf8改为utf8mb4,utf8mb4表示支持4个字节表示的UTF-8编码。要理解这个方法,需要知道以下前提知识:

1. UTF-8编码
UTF-8编码是Unicode字符集的一种可变长编码方式,也是目前国际上最通用的一种编码方式,它的好处在于完全兼容ASCII码和ISO 8859-1(Latin-1)编码,最多4个字节就能表示Unicode字符集中的所有字符(U+000000 - U+10FFFF, 共1114112个code points,即码位),用它来编码Unicode字符集时的可编码区间和所需字节数如下:

1
2
3
4
5
6
7
8
9
+-----------------------+--------+
| U+000000 - U+00007F   | 1个字节 |
+-----------------------+--------+
| U+000080 - U+0007FF   | 2个字节 |
+-----------------------+--------+
| U+000800 - U+00FFFF   | 3个字节 |
+-----------------------+--------+
| U+010000 - U+10FFFF   | 4个字节 |
+-----------------------+--------+

作为近亲,顺便也了解下UTF-16和UTF-32编码。UTF-16也是可变长编码方式,用它来编码Unicode字符集时的可编码区间和所需字节数如下:

1
2
3
4
5
+-----------------------+--------+
| U+000000 - U+00FFFF   | 1个字节 |
+-----------------------+--------+
| U+010000 - U+10FFFF   | 2个字节 |
+-----------------------+--------+

因此,单从存储空间的角度上看,如果存储的内容大部分为英、意、法等Latin-1字符,那么选择UTF-8编码较为合适,如果存储的内容大部分为CJK(东亚文字,Chinese, Japanese, Korean),那么选择UTF-16编码更合适。

至于UTF-32,它是一个定长的编码方式,对所有的Unicode字符采用统一的4字节长度编码方式,它的好处是计算机处理方便,坏处显而易见,太费存储空间,实际中较少使用。

2. MySQL对utf8的处理方式
MySQL在建表时可以指定字符编码为utf8,但是奇葩的是,MySQL的CHARSET=utf8只能表示Unicode字符集中一部分,通过查看属性可知它最多只能编码所有可以3个字节表示的Unicode字符:

1
2
3
4
5
6
mysql> show character set where charset = 'utf8';
+----------+-----------------------------+---------------------+--------+  
| Charset  | Description                 | Default collation   | Maxlen |  
+----------+-----------------------------+---------------------+--------+  
| utf8     | UTF-8 Unicode               | utf8_general_ci     |      3 |  
+----------+-----------------------------+---------------------+--------+

也就是说,Unicode码位U+010000-U+10FFFF之间的字符在MySQL中是无法用CHARSET=utf8来表示的,CHARSET=utf8只能表示约5.88% 的Unicode字符。注:(U+00FFFF + 1) / (U+10FFFF + 1) = 5.88%。

MySQL 5.5.3以后加了一种utf8mb4的编码类型,它的Maxlen为4,支持4字节长度的UTF-8编码,如下:

1
2
3
4
5
6
mysql> show character set where charset = 'utf8mb4';
+----------+-----------------------------+---------------------+--------+  
| Charset  | Description                 | Default collation   | Maxlen |  
+----------+-----------------------------+---------------------+--------+  
| utf8mb4  | UTF-8 Unicode               | utf8mb4_general_ci  |      4 |  
+----------+-----------------------------+---------------------+--------+

根据最新的Unicode 6.1版本,Emoji表情所在码位为U+1F300 - U+1F64F,因此需要用4个字节编码,所以,出现了本文开头出现的SQLException异常。

Emoji表情无法插入只是MySQL utf8字符集无法处理U+010000以上Unicode字符的一个特例,有了utf8mb4之后,为了今后少踩些这方面的坑,应尽量采用CHARSET=utf8mb4来指定数据表字符集。

--EOF--

『格调』

『格调』有人的地方就有等级,聪明的先行者们懂得制定有效的制度来实现人人生而平等,尽量减少等级给民众带来的伤害。但要消除等级实行人人死而平等的平均主义也不符合发展规律。『格调』把社会等级分成9个,实际上概括起来就是三个等级:上层阶级、中产阶级和下层阶级。每个阶级下的众生都有着自身独特的生活方式、衣着品味、言行举止和处世哲学,这些因素是一个人家庭背景和社会地位的自然体现。

将社会等级缩减为3个以后,所谓的上中下等级人数分布便成了一个梭子型曲线,两头小中间大,无论放到哪个社会哪种政经体制,中产阶级总是占大多数。除了最上层阶级,其他各阶级的人们都在千方百计的涌向自己所在的上一层阶级,谈论上一层阶级的话题,学习上一层阶级的价值观。然而,事实也并非总是如此,阶级差距过大的两个阶层,彼此之间的价值观冲突太大,容易引起下层阶级对上层阶级的美学侵犯。当中产阶级羡慕上层阶级希腊帽和甲板鞋背后的物质生活时,下层阶级却会嘲讽上层阶级多数情况下的自命不凡;当上层阶级崇古崇英情节泛滥时,下层阶级却对英式服装、文学典故、行为举止和仪式表现出茫然和不屑的姿态。

上层阶级热衷于谈论社会等级本身,谈论越多,他们能体会到越多的优越感,下层阶级谈不上热衷,但是也不排斥谈论,他们笑话上层阶级空洞的贵族式的自命不凡和中产阶级令人生厌的附庸风雅。而作为人口基数最大的中产阶级,对社会等级则是最为敏感,他们一边努力朝上层阶级靠拢,一边担心自己沦为下层阶级,敏感的结果造成这个阶级弥漫着一种焦虑感,同时也养成了这个阶级独特的谨慎和势利:因为怕被批评,所以谨慎,因为怕被替代,所以势利。保罗·福塞尔以美国社会为蓝本进行嘲讽,而他关于社会等级与生活品味的评论其实也适用于我们当下的社会,到街上观察一下,这里看不见上层阶级,来往的人群中都是中产阶级及以下的,除了老人和儿童,大部分人的脸上都写满了焦虑和匆忙,这背后有着等级制度给社会带上的深深印记,所谓相由心生,大家都无法掩盖自己内心对上一阶级的追求与向往。社会无法做到真正的平等,人们要付出最多的艰辛来获取地位。

正因为等级制度的潜在性和残酷性,中低层阶级将获取优越感的途径部分转移到了全民竞技体育项目(如篮球,足球,橄榄球等)以及政商界的阴谋论与小道消息。人们可以从一场比赛的胜利中获取满足感,若他还能对球队获胜的原因有着权威的分析和独到的见解,那么就能给予他一种幻觉,仿佛自己是正在最高阶层研讨会上进行决策和管理的上层阶级。同理,那些人热衷于从野史和流言蜚语中窥探名人和重大事件的八卦消息,也能给予他一种可以操控那些大人物或者决定事件成败的权力。所以,体育赛事和八卦期刊可以作为一种安抚和慰藉功能的工具,来调和不同阶层之间的矛盾。

--EOF--