couch_db_updater:refresh_validate_doc_funs/1
今回はcouch_db_updater:init/1
から呼び出している関数の中でまだ見ていないrefresh_validate_doc_funs/1
を読んで行きます。
couch_db_updater.erl:
1refresh_validate_doc_funs(Db0) ->
2 Db = Db0#db{user_ctx = #user_ctx{roles=[<<"_admin">>]}},
3 DesignDocs = couch_db:get_design_docs(Db),
4 ProcessDocFuns = lists:flatmap(
5 fun(DesignDocInfo) ->
6 {ok, DesignDoc} = couch_db:open_doc_int(
7 Db, DesignDocInfo, [ejson_body]),
8 case couch_doc:get_validate_doc_fun(DesignDoc) of
9 nil -> [];
10 Fun -> [Fun]
11 end
12 end, DesignDocs),
13 Db0#db{validate_doc_funs=ProcessDocFuns}.
1行目でadminロールの設定をしていますが意図は分からず。couch_db_updater:init/1
から呼び出されているので、adminロールということで問題ないんですかね。2行目でDesignDocs
を取得しています。このDesignDocsが良く分からないので、見ていきます。
couch_db.erl:
1get_design_docs(Db) ->
2 FoldFun = skip_deleted(fun
3 (#full_doc_info{deleted = true}, _Reds, Acc) ->
4 {ok, Acc};
5 (#full_doc_info{id= <<"_design/",_/binary>>}=FullDocInfo, _Reds, Acc) ->
6 {ok, [FullDocInfo | Acc]};k
7 (_, _Reds, Acc) ->
8 {stop, Acc}
9 end),
10 KeyOpts = [{start_key, <<"_design/">>}, {end_key_gt, <<"_design0">>}],
11 {ok, _, Docs} = couch_btree:fold(by_id_btree(Db), FoldFun, [], KeyOpts),
12 Docs.
skip_deleted/1
が呼び出されているので、そのまま追っていきます。
couch_db.erl:
1skip_deleted(FoldFun) ->
2 fun
3 (visit, KV, Reds, Acc) ->
4 FoldFun(KV, Reds, Acc);
5 (traverse, _LK, {Undeleted, _Del, _Size}, Acc) when Undeleted == 0 ->
6 {skip, Acc};
7 (traverse, _, _, Acc) ->
8 {ok, Acc}
9 end.
第一引数がvisit
の時はFoldFun
を実行する関数を返します。KV
は#full_doc_info
です。traverse
は2つあり、一方はwhen Undeleted == 0
とあるので、削除されてたら{skip, Acc}
、そうでない場合は{ok, Acc}
を返すことになります。削除されてたらskipなのでskip_deleted/1
といったところでしょうか。Undeleted == 0
は2重否定のようになっており、ちょっと分かりづらいかな。
get_design_docs/1
の方に戻ると、#full_doc_info.id
が<<"_design/",_/binary>>
の場合のみアキュムレータにFullDocInfo
を入れる関数FoldFun
を定義しています。この関数がvisit
の時に呼び出されることになります。<<"_design/",_/binary>>
の後ろの方の意味が分からなかったのですが、バイナリ型の値で先頭が”_design/“にマッチする、という意味でした。
couch_btree:fold/4
残りは、start_key
とend_key_gt
を定義してcouch_btree:fold/4
を呼び出す部分。ここまでのところは、Btreeをトラバースする際のフィルタ条件の設定を行ってきたように見えます。
couch_btree.erl:
1fold(#btree{root=nil}, _Fun, Acc, _Options) ->
2 {ok, {[], []}, Acc};
3fold(#btree{root=Root}=Bt, Fun, Acc, Options) ->
4 Dir = couch_util:get_value(dir, Options, fwd),
5 InRange = make_key_in_end_range_function(Bt, Dir, Options),
6 Result =
7 case couch_util:get_value(start_key, Options) of
8 undefined ->
9 stream_node(Bt, [], Bt#btree.root, InRange, Dir,
10 convert_fun_arity(Fun), Acc);
11 StartKey ->
12 stream_node(Bt, [], Bt#btree.root, StartKey, InRange, Dir,
13 convert_fun_arity(Fun), Acc)
14 end,
15 case Result of
16 {ok, Acc2}->
17 FullReduction = element(2, Root),
18 {ok, {[], [FullReduction]}, Acc2};
19 {stop, LastReduction, Acc2} ->
20 {ok, LastReduction, Acc2}
21 end.
couch_util:get_value(dir, Options, fwd)
は第二引数の{key, value}のリストの中に第一引数で指定したkeyがあればそのvalueを、なければ第三引数を返します。今回のコンテキストではfwd
がDir
に設定し、make_key_in_end_range_function/3
を呼び出します。
couch_btree.erl:
1make_key_in_end_range_function(#btree{less=Less}, fwd, Options) ->
2 case couch_util:get_value(end_key_gt, Options) of
3 undefined ->
4 case couch_util:get_value(end_key, Options) of
5 undefined ->
6 fun(_Key) -> true end;
7 LastKey ->
8 fun(Key) -> not Less(LastKey, Key) end
9 end;
10 EndKey ->
11 fun(Key) -> Less(Key, EndKey) end
12 end;
今回は呼び出しの流れから、第二引数にfwd
が、第三引数のOptions
にend_key_gt
が指定されているのでそこだけ読むと、fun(Key) -> Less(Key, <<"_design0">>) end
という関数が返ることになります。前回、couch_db_updater/init_db/6
の中で設定されなかったless
の関数が、ここで使われています。
fold
の方に戻ると、InRange
はfun(Key) -> Less(Key, <<"_design0">>) end
で、StartKey
は<<"_design/">>
になり、stream_node/8
の呼び出しは以下のようになります。
stream_node(IdBtree, [], Bt#btree.root, <<"_design/">>, fun(Key) -> Less(Key, <<"_design0">>) end, fwd, Fun, []).
couch_btree:stream_node/8
stream_node/8
の中を見ていきます。
couch_btree.erl:
1stream_node(Bt, Reds, Node, StartKey, InRange, Dir, Fun, Acc) ->
2 Pointer = element(1, Node),
3 {NodeType, NodeList} = get_node(Bt, Pointer),
4 case NodeType of
5 kp_node ->
6 stream_kp_node(Bt, Reds, adjust_dir(Dir, NodeList), StartKey, InRange, Dir, Fun, Acc);
7 kv_node ->
8 stream_kv_node(Bt, Reds, adjust_dir(Dir, NodeList), StartKey, InRange, Dir, Fun, Acc)
9 end.
1行目でNode
の1つ目の要素を取り出しています。このNode
は前記のように#btree.root
にあたるので、root
の第一要素を取得することになります。これまで#btree.root
がどういう構成になっているのか見ていなかったので探してみたところ、以下のような箇所がありました。
couch_btree.erl:
1size(#btree{root = nil}) ->
2 0;
3size(#btree{root = {_P, _Red}}) ->
4 % pre 1.2 format
5 nil;
6size(#btree{root = {_P, _Red, Size}}) ->
7 Size.
上記のコードだけでは分からないので想像ですが、#btree.root
の第一要素_P
は恐らくBtreeのファイルポインタなのではないかと思います。そう仮定して読み進めてみます。stream_node/8
に戻ると、#btree.root
からポインタを取り出し、get_node/2
を呼び出します。
couch_btree.erl:
1get_node(#btree{fd = Fd}, NodePos) ->
2 {ok, {NodeType, NodeList}} = couch_file:pread_term(Fd, NodePos),
3 {NodeType, NodeList}.
続けてcouch_file:pread_term/2
を読んでみます。
couch_file.erl:
1pread_term(Fd, Pos) ->
2 {ok, Bin} = pread_binary(Fd, Pos),
3 {ok, couch_compress:decompress(Bin)}.
4
5
6%%----------------------------------------------------------------------
7%% Purpose: Reads a binrary from a file that was written with append_binary
8%% Args: Pos, the offset into the file where the term is serialized.
9%% Returns: {ok, Term}
10%% or {error, Reason}.
11%%----------------------------------------------------------------------
12
13pread_binary(Fd, Pos) ->
14 {ok, L} = pread_iolist(Fd, Pos),
15 {ok, iolist_to_binary(L)}.
16
17
18pread_iolist(Fd, Pos) ->
19 case gen_server:call(Fd, {pread_iolist, Pos}, infinity) of
20 {ok, IoList, <<>>} ->
21 {ok, IoList};
22 {ok, IoList, Md5} ->
23 case couch_util:md5(IoList) of
24 Md5 ->
25 {ok, IoList};
26 _ ->
27 exit({file_corruption, <<"file corruption">>})
28 end;
29 Error ->
30 Error
31 end.
pread_iolist/2
のコメントに、Pos
は「termがシリアライズされているファイルのオフセット」とあるので、#btree.root
の第一要素はファイルポインタで良さそうです。pread_iolist/2
の1行目のgen_server:call/3
で以下が呼び出されます。
couch_file.erl:
1handle_call({pread_iolist, Pos}, _From, File) ->
2 {RawData, NextPos} = try
3 % up to 8Kbs of read ahead
4 read_raw_iolist_int(File, Pos, 2 * ?SIZE_BLOCK - (Pos rem ?SIZE_BLOCK))
5 catch
6 _:_ ->
7 read_raw_iolist_int(File, Pos, 4)
8 end,
9 <<Prefix:1/integer, Len:31/integer, RestRawData/binary>> =
10 iolist_to_binary(RawData),
11 case Prefix of
12 1 ->
13 {Md5, IoList} = extract_md5(
14 maybe_read_more_iolist(RestRawData, 16 + Len, NextPos, File)),
15 {reply, {ok, IoList, Md5}, File};
16 0 ->
17 IoList = maybe_read_more_iolist(RestRawData, Len, NextPos, File),
18 {reply, {ok, IoList, <<>>}, File}
19 end;
?SIZE_BLOCK
はcouch_file.erlで以下のように定義されています。
couch_file.erl:
1deefine(SIZE_BLOCK, 4096).
コメントにもあるように、read_raw_iolist_int/3
でおおよそ8KBほど読み出します。そこから1ビット取り出してPrefix
に、31ビット取り出してLen
に、残りをRestRawData
に束縛します。Prefix
が1の場合はextract_md5/1
を呼び出しています。
couch_file.erl:
1-spec extract_md5(iolist()) -> {binary(), iolist()}.
2extract_md5(FullIoList) ->
3 {Md5List, IoList} = split_iolist(FullIoList, 16, []),
4 {iolist_to_binary(Md5List), IoList}.
先頭16バイトがMD5の値になっているようです。maybe_read_more_iolist/4
の第二引数が16 + Len
となっていることから、前期のLen
にはこのMD5の値の長さは含まれていないことになります。そしてPrefix
が0の場合はMD5の値は格納されていないようです。MD5の値が設定されている場合は、pread_iolist/2
でMD5の値のチェックを行っています。ファイルから読み込んだiolistをbinaryに変換し、pread_term/2
まで戻ります。pread_term/2
の中で、couch_compress:decompress/2
にそのbinaryを渡します。
couch_compress.erl:
1decompress(<<?SNAPPY_PREFIX, Rest/binary>>) ->
2 {ok, TermBin} = snappy:decompress(Rest),
3 binary_to_term(TermBin);
4decompress(<<?TERM_PREFIX, _/binary>> = Bin) ->
5 binary_to_term(Bin).
binaryの先頭が?SNAPPY_PREFIX
にマッチする場合はsnappy:decompress/1
で戻してからbinary_to_term/1
でtermに戻します。?TERM_PREFIX
にマッチする場合はそのままbinary_to_term/1
を呼び出してtermに戻します。
ここまででようやくget_node/2
が終わり、{NodeTyp, NodeList}をファイルから読み出しました。get_node/2
の呼び出しによって、IdBtree
内のデータをファイルから一度に全部読み込んだ気がするのですが、大丈夫なのかな…。
stream_node/8
に戻ります。BtreeはIdBtree
なので、NodeType
はkv_node
になるはずです。ですので、stream_kv_node/8
を読んでいきます。その前にadjust_dir(Dir, NodeList)
の部分を読んでみたいと思います。
couch_btree.erl:
1adjust_dir(fwd, List) ->
2 List;
3adjust_dir(rev, List) ->
4 lists:reverse(List).
今回はfwd
なのでNodeList
はそのままstream_kv_node/8
に引き渡されます。
couch_btree:stream_kv_node/8
stream_node/8
のコードが大分上部の方にあるので、ここまでの流れでstream_kv_node/8
がどういう値で呼び出されるか書いておきます。
stream_kv_node(IdBtree, [], NodeList, <<"_design/">>, fun(Key) -> Less(Key, <<"_design0">>) end, fwd, Fun, []).
この流れでstream_kv_node/8
がどのような処理になるか見ていきます。
couch_btree.erl:
1stream_kv_node(Bt, Reds, KVs, StartKey, InRange, Dir, Fun, Acc) ->
2 DropFun =
3 case Dir of
4 fwd ->
5 fun({Key, _}) -> less(Bt, Key, StartKey) end;
6 rev ->
7 fun({Key, _}) -> less(Bt, StartKey, Key) end
8 end,
9 {LTKVs, GTEKVs} = lists:splitwith(DropFun, KVs),
10 AssembleLTKVs = [assemble(Bt,K,V) || {K,V} <- LTKVs],
11 stream_kv_node2(Bt, Reds, AssembleLTKVs, GTEKVs, InRange, Dir, Fun, Acc).
less/3
が呼び出されてます。#btree.less
に設定される関数をまだ読んでないのでこの時点では何とも。
couch_btree.erl:
1less(#btree{less=Less}, A, B) ->
2 Less(A, B).
ですよね。DropFun
はIdBtree
のless
に設定された関数を呼び出す関数です。次のlists:splitwith/2
でDropFun
を指定していることから、NodeList
は
[{Key, ...},...]
ですかね。
次にsplitwith/2
で恐らくStartKey
よりも前のデータであるLTKVs
の各要素に対してassemble/3
を呼び出してリストを作成しています。これは見るまでもなく、join
に設定していた関数が呼ばれることになるので、AssembleLTKVs
は#full_doc_info
のリストになります。それを指定してstream_kv_node2/8
を呼び出しています。
couch_btree.erl:
1stream_kv_node2(_Bt, _Reds, _PrevKVs, [], _InRange, _Dir, _Fun, Acc) ->
2 {ok, Acc};
3stream_kv_node2(Bt, Reds, PrevKVs, [{K,V} | RestKVs], InRange, Dir, Fun, Acc) ->
4 case InRange(K) of
5 false ->
6 {stop, {PrevKVs, Reds}, Acc};
7 true ->
8 AssembledKV = assemble(Bt, K, V),
9 case Fun(visit, AssembledKV, {PrevKVs, Reds}, Acc) of
10 {ok, Acc2} ->
11 stream_kv_node2(Bt, Reds, [AssembledKV | PrevKVs], RestKVs, InRange, Dir, Fun, Acc2);
12 {stop, Acc2} ->
13 {stop, {PrevKVs, Reds}, Acc2}
14 end
15 end.
ようやくInRange
の関数が呼び出されるところまで来ました。Key
が<<"_design0">>
よりも前であればtrue
となります。
[{K,V} | RestKVs]
はStartKey(<<"_design/">>)
以上のKeyを持つドキュメントのリストが指定されているので、
<<"_design/">> ≦ Key < <<"_design0">>
であればアキュムレータに追加されます。これがDesignDocになります。
Conclusion
DesignDocとは何かをポイントにコードを読んでいきました。DesignDocはKeyが<<”_design/“>> ≦ Key < <<”_design0”>のドキュメントであることが分かりましたが、どんな役割なのかはまだ分かりません。
また、今回のコードリーディングの過程で、データベースファイルからドキュメントを読み出す部分を見ていきました。ドキュメントはErlangのtermがbinaryに変換されてディスクに書き込まれることが分かりました。