act-act about projects rss

CouchDB Source Code Reading part6

couch_httpd_db:db_req/2

前回までドキュメントの取得の流れを見てきたので、次に更新まわりを見ていこうと思います。

couch_httpd_db.erl

1
2
3
4
5
6
7
8
9
10
11
12
db_req(#httpd{method='POST',path_parts=[_DbName]}=Req, Db) ->
    couch_httpd:validate_ctype(Req, "application/json"),
    Doc = couch_doc:from_json_obj(couch_httpd:json_body(Req)),
    validate_attachment_names(Doc),
    Doc2 = case Doc#doc.id of
        <<"">> ->
            Doc#doc{id=couch_uuids:new(), revs={0, []}};
        _ ->
            Doc
    end,OA
    DocId = Doc2#doc.id,
    update_doc(Req, Db, DocId, Doc2);

実際に確かめたわけではないのですが、ドキュメントの更新APIの入り口はこのあたりかな、と。couch_doc:from_json_obj/1couch_httpd:json_body/1でJSONNからドキュメントに変換しているようなので、このあたりのコードを見ています。まずはcouch_httpd:json_body/1から。

couch_httpd_db.erl

1
2
json_body(Httpd) ->
    ?JSON_DECODE(maybe_decompress(Httpd, body(Httpd))).

?JSON_DECODEというマクロが指定されています。マクロの定義を見てみます。

couch_db.hrl

1
-define(JSON_DECODE(V), ejson:decode(V)).

?JSON_DECODEマクロの実体はejson:decode/1でした。JSONのエンコード、デコードを変更しやすいようにマクロにしているようです。ejsonのREADMEを見ると、ejson:decode/1{[{<<"foo">>,true}]}といったように、JSONをErlangのタプルとリストに変換する関数です。次にcouch_httpd:body/1を見てみます。

couch_httpd.erl

1
2
3
4
5
6
body(#httpd{mochi_req=MochiReq, req_body=undefined}) ->
    MaxSize = list_to_integer(
        couch_config:get("couchdb", "max_document_size", "4294967296")),
    MochiReq:recv_body(MaxSize);
body(#httpd{req_body=ReqBody}) ->
    ReqBody.

Bodyのサイズの上限は4GBとなってました。変数の名前から、HTTPの部分はmochiwebが使われているようです。

ejsonにより、JSONがタプルとリストの組み合わせに変換されるので、couch_doc:from_json_obj/1はそれをドキュメントに変換する関数になります。コードを見てみます。

couch_doc.erl

1
2
3
4
5
from_json_obj({Props}) ->
    transfer_fields(Props, #doc{body=[]});

from_json_obj(_Other) ->
    throw({bad_request, "Document must be a JSON object"}).

指定されたJSONオブジェクトがタプルでなければエラーとしています。そのままtransfer_fields/2を見てみます。

couch_doc.erl

1
2
3
4
5
6
7
8
9
10
11
12
13
transfer_fields([], #doc{body=Fields}=Doc) ->
    % convert fields back to json object
    Doc#doc{body={lists:reverse(Fields)}};

transfer_fields([{<<"_id">>, Id} | Rest], Doc) ->
    validate_docid(Id),
    transfer_fields(Rest, Doc#doc{id=Id});

transfer_fields([{<<"_rev">>, Rev} | Rest], #doc{revs={0, []}}=Doc) ->
    {Pos, RevId} = parse_rev(Rev),
    transfer_fields(Rest,
            Doc#doc{revs={Pos, [RevId]}});
...

couch_doc:transer_fieldsは18個ほど定義されており、各々のパターンによって#docの構築の仕方が定義されています。サスガにひとつひとつ見るのはツラいので今回は追いませんが、この関数を実行するとJSONオブジェクトが#docに変換されて返る、ということは分かりました。指定したJSONが思った形でDBに保存されないようであれば、このあたりを見てみると良いかもしれません。

couch_httpd_db:db_req/2に戻って、validate_attachment_names/1。これはattachementのnameがUTF-8かどうかチェックしています。Attachementsが何かについてはこちらを参照。

#doc.idが設定されていなければUUIDを生成して#doc.idに設定してrevを初期化し、couch_httpd_db:update_doc/4を呼び出します。

couch_httpd_db.erl

1
2
3
4
5
update_doc(Req, Db, DocId, #doc{deleted=false}=Doc) ->
    Loc = absolute_uri(Req, "/" ++ ?b2l(Db#db.name) ++ "/" ++ ?b2l(DocId)),
    update_doc(Req, Db, DocId, Doc, [{"Location", Loc}]);
update_doc(Req, Db, DocId, Doc) ->
    update_doc(Req, Db, DocId, Doc, []).

#doc.deletedfalseの場合はドキュメントのURIを生成して"Location"とし、update_doc/5を呼び出します。

couch_httpd_db.erl

1
2
3
4
5
update_doc(Req, Db, DocId, Doc, Headers) ->
    #doc_query_args{
        update_type = UpdateType
    } = parse_doc_query(Req),
    update_doc(Req, Db, DocId, Doc, Headers, UpdateType).

Reqを指定してparse_doc_query/1を呼び出し、UpdateTypeを取得しています。この値を取得する過程を追ってみます。

couch_httpd_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
parse_doc_query(Req) ->
    lists:foldl(fun({Key,Value}, Args) ->
        case {Key, Value} of
        {"attachments", "true"} ->
            Options = [attachments | Args#doc_query_args.options],
            Args#doc_query_args{options=Options};
        {"meta", "true"} ->
            Options = [revs_info, conflicts, deleted_conflicts | Args#doc_query_args.options],
            Args#doc_query_args{options=Options};
        {"revs", "true"} ->
            Options = [revs | Args#doc_query_args.options],
            Args#doc_query_args{options=Options};
        {"local_seq", "true"} ->
            Options = [local_seq | Args#doc_query_args.options],
            Args#doc_query_args{options=Options};
        {"revs_info", "true"} ->
            Options = [revs_info | Args#doc_query_args.options],
            Args#doc_query_args{options=Options};
        {"conflicts", "true"} ->
            Options = [conflicts | Args#doc_query_args.options],
            Args#doc_query_args{options=Options};
        {"deleted_conflicts", "true"} ->
            Options = [deleted_conflicts | Args#doc_query_args.options],
            Args#doc_query_args{options=Options};
        {"rev", Rev} ->
            Args#doc_query_args{rev=couch_doc:parse_rev(Rev)};
        {"open_revs", "all"} ->
            Args#doc_query_args{open_revs=all};
        {"open_revs", RevsJsonStr} ->
            JsonArray = ?JSON_DECODE(RevsJsonStr),
            Args#doc_query_args{open_revs=couch_doc:parse_revs(JsonArray)};
        {"latest", "true"} ->
            Options = [latest | Args#doc_query_args.options],
            Args#doc_query_args{options=Options};
        {"atts_since", RevsJsonStr} ->
            JsonArray = ?JSON_DECODE(RevsJsonStr),
            Args#doc_query_args{atts_since = couch_doc:parse_revs(JsonArray)};
        {"new_edits", "false"} ->
            Args#doc_query_args{update_type=replicated_changes};
        {"new_edits", "true"} ->
            Args#doc_query_args{update_type=interactive_edit};
        {"att_encoding_info", "true"} ->
            Options = [att_encoding_info | Args#doc_query_args.options],
            Args#doc_query_args{options=Options};
        _Else -> % unknown key value pair, ignore.
            Args
        end
    end, #doc_query_args{}, couch_httpd:qs(Req)).

クエリパラメータからオプションに変換しているようですが、この関数内ではupdate_typeが出てこないので_Elseに落ち、このパラメータは#doc_query_args.optionsには設定されないようです。ちょっと変な感じもするけど、あってるのかな…。update_typeが取得できなかったと想定して、update_doc/6を見ていきます。

couch_httpd_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
update_doc(Req, Db, DocId, #doc{deleted=Deleted}=Doc, Headers, UpdateType) ->
    case couch_httpd:header_value(Req, "X-Couch-Full-Commit") of
    "true" ->
        Options = [full_commit];
    "false" ->
        Options = [delay_commit];
    _ ->
        Options = []
    end,
    case couch_httpd:qs_value(Req, "batch") of
    "ok" ->
        % async batching
        spawn(fun() ->
                case catch(couch_db:update_doc(Db, Doc, Options, UpdateType)) of
                {ok, _} -> ok;
                Error ->
                    ?LOG_INFO("Batch doc error (~s): ~p",[DocId, Error])
                end
            end),
        send_json(Req, 202, Headers, {[
            {ok, true},
            {id, DocId}
        ]});
    _Normal ->
        % normal
        {ok, NewRev} = couch_db:update_doc(Db, Doc, Options, UpdateType),
        NewRevStr = couch_doc:rev_to_str(NewRev),
        ResponseHeaders = [{"ETag", <<"\"", NewRevStr/binary, "\"">>}] ++ Headers,
        send_json(Req,
            if Deleted orelse Req#httpd.method == 'DELETE' -> 200;
            true -> 201 end,
            ResponseHeaders, {[
                {ok, true},
                {id, DocId},
                {rev, NewRevStr}]})
    end.

まず、ヘッダにX-Couch-Full-Commitが指定されている場合はfull_commitを、指定されていない場合はdelay_commitをオプションとしています。クエリパラメータにbatch=okが付いていると非同期で、付いていない場合は同期でcouch_db:update_doc/4を呼び出します。

Conclusion

HTTP経由でドキュメントを更新する流れを見てきました。肝心のcouch_db:update_doc/4は次回読んでみる予定です。