act-act about projects rss

CouchDB Source Code Reading part8

couch_db:update_doc/4

前回からの続きで、couch_db:update_docs/4の続きを。

couch_db.erl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    case (Db#db.validate_doc_funs /= []) orelse
        lists:any(
            fun({#doc{id= <<?DESIGN_DOC_PREFIX, _/binary>>}, _Ref}) ->
                true;
            ({#doc{atts=Atts}, _Ref}) ->
                Atts /= []
            end, Docs3) of
    true ->
        % lookup the doc by id and get the most recent
        Ids = [Id || [{#doc{id=Id}, _Ref}|_] <- DocBuckets],
        ExistingDocInfos = get_full_doc_infos(Db, Ids),

        {DocBucketsPrepped, PreCommitFailures} = prep_and_validate_updates(Db,
                DocBuckets, ExistingDocInfos, AllOrNothing, [], []),

        % strip out any empty buckets
        DocBuckets2 = [Bucket || [_|_] = Bucket <- DocBucketsPrepped];
    false ->
        PreCommitFailures = [],
        DocBuckets2 = DocBuckets
    end,
```

#db.validate_doc_funsに何か関数がバインドされているか、更新対象のドキュメントの中にDesignDocが含まれている場合はprep_and_validate_updates/6を呼び出します。

couch_db:prep_and_validate_updates/6

PreCommitFiluresはvalidationに引っかかった結果が返るのだと思いますが、DocBucketsPreppedが分からないので、この関数を追ってみます。

couch_db.erl

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
prep_and_validate_updates(_Db, [], [], _AllowConflict, AccPrepped,
       AccFatalErrors) ->
   {AccPrepped, AccFatalErrors};
prep_and_validate_updates(Db, [DocBucket|RestBuckets], [not_found|RestLookups],
        AllowConflict, AccPrepped, AccErrors) ->
    {PreppedBucket, AccErrors3} = lists:foldl(
        fun({#doc{revs=Revs}=Doc,Ref}, {AccBucket, AccErrors2}) ->
            case couch_doc:has_stubs(Doc) of
            true ->
                couch_doc:merge_stubs(Doc, #doc{}); % will throw exception
            false -> ok
            end,
            case Revs of
            {0, []} ->
                case validate_doc_update(Db, Doc, fun() -> nil end) of
                ok ->
                    {[{Doc, Ref} | AccBucket], AccErrors2};
                Error ->
                    {AccBucket, [{Ref, Error} | AccErrors2]}
                end;
            _ ->
                % old revs specified but none exist, a conflict
                {AccBucket, [{Ref, conflict} | AccErrors2]}
            end
        end,
        {[], AccErrors}, DocBucket),

    prep_and_validate_updates(Db, RestBuckets, RestLookups, AllowConflict,
            [lists:reverse(PreppedBucket) | AccPrepped], AccErrors3);
prep_and_validate_updates(Db, [DocBucket|RestBuckets],
        [{ok, #full_doc_info{rev_tree=OldRevTree}=OldFullDocInfo}|RestLookups],
        AllowConflict, AccPrepped, AccErrors) ->
    Leafs = couch_key_tree:get_all_leafs(OldRevTree),
    LeafRevsDict = dict:from_list([
        begin
            Deleted = element(1, LeafVal),
            Sp = element(2, LeafVal),

            {{Start, RevId}, {Deleted, Sp, Revs}}
        end ||
        {LeafVal, {Start, [RevId | _]} = Revs} <- Leafs
    ]),
    {PreppedBucket, AccErrors3} = lists:foldl(
        fun({Doc, Ref}, {Docs2Acc, AccErrors2}) ->
            case prep_and_validate_update(Db, Doc, OldFullDocInfo,
                    LeafRevsDict, AllowConflict) of
            {ok, Doc2} ->
                {[{Doc2, Ref} | Docs2Acc], AccErrors2};
            {Error, #doc{}} ->
                % Record the error
                {Docs2Acc, [{Ref, Error} |AccErrors2]}
            end
        end,
        {[], AccErrors}, DocBucket),
    prep_and_validate_updates(Db, RestBuckets, RestLookups, AllowConflict,
            [PreppedBucket | AccPrepped], AccErrors3).

最初のパターンマッチは、更新対象ドキュメントが無い(無くなった)ものなので読み飛ばします。次のExistingDocInfosのheadがnot_foundのパターンマッチを見ていきます。

couch_doc:has_stubs/1を呼び出し、その結果がtrueであればcouch_doc:merge_stubs/2を呼び出して空のドキュメント(#doc{})とマージしているように見えます。stubとは何かを見たいので、couch_doc:has_stubs/1のコードを見てみます。

couch_doc.erl

1
2
3
4
5
6
7
8
has_stubs(#doc{atts=Atts}) ->
    has_stubs(Atts);
has_stubs([]) ->
    false;
has_stubs([#att{data=stub}|_]) ->
    true;
has_stubs([_Att|Rest]) ->
    has_stubs(Rest).

ドキュメントのアタッチメントのdatastubのものが1つでもあればtrueが返るようになっています。そのままcouch_doc:merge_stubs/2を見てみます。

couch_db.erl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
merge_stubs(#doc{id = Id}, nil) ->
    throw({missing_stub, <<"Previous revision missing for document ", Id/binary>>});
merge_stubs(#doc{id=Id,atts=MemBins}=StubsDoc, #doc{atts=DiskBins}) ->
    BinDict = dict:from_list([{Name, Att} || #att{name=Name}=Att <- DiskBins]),
    MergedBins = lists:map(
        fun(#att{name=Name, data=stub, revpos=StubRevPos}) ->
            case dict:find(Name, BinDict) of
            {ok, #att{revpos=DiskRevPos}=DiskAtt}
                    when DiskRevPos == StubRevPos orelse StubRevPos == nil ->
                DiskAtt;
            _ ->
                throw({missing_stub,
                        <<"id:", Id/binary, ", name:", Name/binary>>})
            end;
        (Att) ->
            Att
        end, MemBins),
    StubsDoc#doc{atts= MergedBins}.

第二引数のドキュメントのアタッチメント(DiskBins)の名前をキーにしてdictionaryを作成し、第一引数のドキュメントのアタッチメント(MemBins)の名前と被っているのがあれば、DiskBins側のアタッチメントを採用する、というコードのようです。ただ、prep_and_validate_updates/6から呼び出す場合、第二引数は#doc{}なので、DiskBinsundefinedになるんじゃないかな…。呼び出し側のコメントにもも

% will throw exception

とあるし、動くことを想定しているコードなのかよく分かりませんでした。prep_and_validate_updates/6に戻り、バリデーションの結果問題がなければPreppedBucketの方に、問題があればAccErrorsの方にドキュメントが振り分けられます。ここまでが、ドキュメントが見つからずnot_foundだった場合の話。

prep_and_validate_updates/6で最後にパターンマッチする関数を見ていきます。couch_key_tree:get_all_leafs/1で更新前のrev_treeのleafを取得しています。

couch_key_tree.erl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
get_all_leafs(Trees) ->
    get_all_leafs(Trees, []).

get_all_leafs([], Acc) ->
    Acc;
get_all_leafs([{Pos, Tree}|Rest], Acc) ->
    get_all_leafs(Rest, get_all_leafs_simple(Pos, [Tree], []) ++ Acc).

get_all_leafs_simple(_Pos, [], _KeyPathAcc) ->
    [];
get_all_leafs_simple(Pos, [{KeyId, Value, []} | RestTree], KeyPathAcc) ->
    [{Value, {Pos, [KeyId | KeyPathAcc]}} | get_all_leafs_simple(Pos, RestTree, KeyPathAcc)];
get_all_leafs_simple(Pos, [{KeyId, _Value, SubTree} | RestTree], KeyPathAcc) ->
    get_all_leafs_simple(Pos + 1, SubTree, [KeyId | KeyPathAcc]) ++ get_all_leafs_simple(Pos, RestTree, KeyPathAcc).

[{Value, {Pos, [KeyId, ...]}}, ...]というleafの集合が返ってきます。それを[{{Pos, RevId}, {Deleted, Sp, Revs}}, ...]というリストに変換した後、dict:from_list/1でdictionaryに変換します。それをFullDocInfoと共にprep_and_validate_update/5に渡してバリデーションチェック等を行い、ドキュメントが更新OKかNGかを判定しているようです。not_foundが指定された場合と比較すると、OldFullDocInfoからleafの値を取り出して、バリデーションチェックを行っているであろうprep_and_validate_update/5に渡しているところが異なります。prep_and_validate_update/5を見てみます。

couch_db.erl

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
prep_and_validate_update(Db, #doc{id=Id,revs={RevStart, Revs}}=Doc,
        OldFullDocInfo, LeafRevsDict, AllowConflict) ->
    case Revs of
    [PrevRev|_] ->
        case dict:find({RevStart, PrevRev}, LeafRevsDict) of
        {ok, {Deleted, DiskSp, DiskRevs}} ->
            case couch_doc:has_stubs(Doc) of
            true ->
                DiskDoc = make_doc(Db, Id, Deleted, DiskSp, DiskRevs),
                Doc2 = couch_doc:merge_stubs(Doc, DiskDoc),
                {validate_doc_update(Db, Doc2, fun() -> DiskDoc end), Doc2};
            false ->
                LoadDiskDoc = fun() -> make_doc(Db,Id,Deleted,DiskSp,DiskRevs) end,
                {validate_doc_update(Db, Doc, LoadDiskDoc), Doc}
            end;
        error when AllowConflict ->
            couch_doc:merge_stubs(Doc, #doc{}), % will generate error if
                                                        % there are stubs
            {validate_doc_update(Db, Doc, fun() -> nil end), Doc};
        error ->
            {conflict, Doc}
        end;
    [] ->
        % new doc, and we have existing revs.
        % reuse existing deleted doc
        if OldFullDocInfo#full_doc_info.deleted orelse AllowConflict ->
            {validate_doc_update(Db, Doc, fun() -> nil end), Doc};
        true ->
            {conflict, Doc}
        end
    end.

DocからRevsを取り出し、要素が1つ以上設定されているかによって処理を分けています。Revsに要素が1つ以上設定されている場合は、その先頭の要素をPrevRevにバインドし、LeafRevsDict{RevStart, PrevRev}で検索します。{RevStart, PrevRev}は、{Pos, RevId(KeyId)}なので、更新対象のDocOldFullDocInfoのleafに含まれているかどうかチェックしているようです。含まれていれば先程と同様にcouch_doc:has_stubs/1を呼び出し、必要があればcouch_doc:merge_stubs/2を呼び出してアタッチメントをマージした上で、validate_doc_update/3の結果を返すようになっています。

couch_db:validate_doc_update/3

couch_db:validate_doc_update/3を見てみます。

couch_db.erl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
validate_doc_update(#db{}=Db, #doc{id= <<"_design/",_/binary>>}, _GetDiskDocFun) ->
    catch check_is_admin(Db);
validate_doc_update(#db{validate_doc_funs=[]}, _Doc, _GetDiskDocFun) ->
    ok;
validate_doc_update(_Db, #doc{id= <<"_local/",_/binary>>}, _GetDiskDocFun) ->
    ok;
validate_doc_update(Db, Doc, GetDiskDocFun) ->
    DiskDoc = GetDiskDocFun(),
    JsonCtx = couch_util:json_user_ctx(Db),
    SecObj = get_security(Db),
    try [case Fun(Doc, DiskDoc, JsonCtx, SecObj) of
            ok -> ok;
            Error -> throw(Error)
        end || Fun <- Db#db.validate_doc_funs],
        ok
    catch
        throw:Error ->
            Error
    end.

更新対象のDocDesignDocの場合はadminかどうかチェックしているようです。#db.validate_doc_funsにvalidation用の関数が設定されていない場合、またはDoc#doc.idがlocalのものであればvalidationは実施されません。

上記以外の場合、まずGetDiskDocFunsを実行してDiskDocを取得します。このGetDiskDocFuns以前みたようにデータファイルからDocのデータを取得する関数です。DiskDocOldFullDocInfoに保存されているポインタから取得したドキュメントデータなので、更新ドキュメントの一つ前のバージョンのドキュメントデータになります。

JsonCtx及びSecObjを取得し、Db#db.validate_doc_funsに設定されているvalidation関数に対して(Doc, DiskDoc, JsonCtx, SecObj)を渡してvalidationを実行し、すべてOKだった場合はokが、そうでない場合はvalidation関数が返すErrorで返るようになっています。

ちなみにvalidate_update_docという関数名は、「CouchDB Source Code Reading Part5#couchdocgetvalidatedocfun1」で触れてますが、あちらはcouch_query_serversモジュールで定義されている同名の関数で、恐らく同じようなことをやっているのだと思います。

strip out any empty buckets

couch_db:validate_doc_update/3を使ってドキュメントのvalidationを行い、DocBucket毎に{PreppedBucket, AccErrors3}に分けられます。PreppedBucketはvalidationした結果OKだったドキュメントのリスト、AccErrrosはNGだったドキュメントのリストです。DocBucketsリストにあるすべてのDocBucketに対してこれらの分類が終わった後、couch_db:update_docs/4では以下のようなコードを通ります。

couch_db.erl

1
2
% strip out any empty buckets
DocBuckets2 = [Bucket || [_|_] = Bucket <- DocBucketsPrepped];                                                                                      

コメントを見ると、空Bucketを取り除く、という意図に読めるのですが、このコードの意味が良く分からなかったので、ここだけ試してみました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Eshell V5.10.3  (abort with ^G)
1> A=[1,[2,3],4].
[1,[2,3],4]
2> B=[X || [_|_]=X <- A].
[[2,3]]
3> B.
[[2,3]]
4> A2=[1,[2],3,4].
[1,[2],3,4]
5> B2=[X || [_|_]=X <- A2].
[[2]]
6> A3=[[1],[],[3],[4]].
[[1],[],[3],[4]]
7> B3=[X || [_|_]=X <- A3].
[[1],[3],[4]]

ABの結果を見ると、リストの中のリスト([2,3])がピックアップされているのが分かります。また、A2B2の結果から、リストの中のリストの要素数は1でも問題ないことが分かります。そしてA3B3の結果から、リストの中のリストの要素数が0のものは取り除かれているのが分かります。これらの結果から、コメントにあるように空Bucketを取り除く、という意図であることが分かりました。

validationを実行した結果、DocBucketの中のすべてのDocがOK、もしくはすべてのDocがNGだった場合、もう一方のバケットは空になる為、それを取り除く意図なのだと思います。Aから14が除去されてBに格納される部分について、X14の場合、パターンマッチのところで数値とリストで型が合わないのでエラーになるのかと思ったのですが、14が自動的に除去されるのがちょっと意外でした。リスト内包表記は、パターンマッチしないものは結果リストに含まれない仕様なんですね。

Conclusion

ドキュメント更新前のvalidationの呼び出し部分を見てきました。データ構造を組み替えたり、DiskDocが入ってくるなど複雑で読みにくい感じがします。また、リスト内包表記は、パターンマッチしないものはエラーにならず結果のリストに含まれない、ということを初めて知りました。