前回、アタッチメントをファイルに書き込む箇所を読んだのですが、書き込んだデータがアタッチメントかどうかをどのように判別できるようにしているのか分からなかったので、アタッチメントをファイルから読み出す部分を見てみたいと思います。

Read attachement data from file

まずHTTPレイヤをエントリとし、GETメソッドを扱う関数を探してみたところ、couch_httpd_db:db_doc_req/3を見つけたので、ここから追っていきます。

couch_httpd_db.erl:

 1db_doc_req(#httpd{method = 'GET', mochi_req = MochiReq} = Req, Db, DocId) ->
 2    #doc_query_args{
 3        rev = Rev,
 4        open_revs = Revs,
 5        options = Options1,
 6        atts_since = AttsSince
 7    } = parse_doc_query(Req),
 8    Options = case AttsSince of
 9    nil ->
10        Options1;
11    RevList when is_list(RevList) ->
12        [{atts_since, RevList}, attachments | Options1]
13    end,
14    case Revs of
15    [] ->
16        Doc = couch_doc_open(Db, DocId, Rev, Options),
17        send_doc(Req, Doc, Options);
18...

revopen_revsは指定しないケースで読み進めてみます。Revs\[\]の場合はcouch_doc_open/4を呼び出してドキュメントを取得しています。この関数を見ていきます。

couch_httpd_db.erl:

 1couch_doc_open(Db, DocId, Rev, Options) ->
 2    case Rev of
 3    nil -> % open most recent rev
 4        case couch_db:open_doc(Db, DocId, Options) of
 5        {ok, Doc} ->
 6            Doc;
 7         Error ->
 8             throw(Error)
 9         end;
10  _ -> % open a specific rev (deletions come back as stubs)
11      case couch_db:open_doc_revs(Db, DocId, [Rev], Options) of
12          {ok, [{ok, Doc}]} ->
13              Doc;
14          {ok, [{{not_found, missing}, Rev}]} ->
15              throw(not_found);
16          {ok, [Else]} ->
17              throw(Else)
18      end
19  end.

Revの指定がない(nil)場合はcouch_db:open_doc/3を呼び出しています。

couch_db.erl:

 1open_doc(Db, Id, Options) ->
 2    increment_stat(Db, {couchdb, database_reads}),
 3    case open_doc_int(Db, Id, Options) of
 4    {ok, #doc{deleted=true}=Doc} ->
 5        case lists:member(deleted, Options) of
 6        true ->
 7            apply_open_options({ok, Doc},Options);
 8        false ->
 9            {not_found, deleted}
10        end;
11    Else ->
12        apply_open_options(Else,Options)
13    end.

couch_db:increment_stat/2以前読んでいるので簡単に書くと、{couchdb, database_reads}というkeyでETSにカウンタが登録されているので、それを更新しています。couch_db:open_doc_int/3以前読んでいます。

couch_db:make_doc/5の内容から、couch_db:read_doc/5couch_d:read_doc/2couch_file:pread_term/2という流れでファイルからデータを読み出し、couch_db:read_doc/2{ok, {BodyData0, Atts00}}という戻り値を返すことから、ドキュメント自体のデータとアタッチメントは{BodyData0, Atts00}としてシリアライズして保存されていることになります。

また、make_doc/5で取得したアタッチメントのリストをドキュメントの形式に直す際、#att.dataには{Fd, Sp}が設定されていることからcouch_db:make_doc/5でファイルから読み出したアタッチメントの情報には、実データではなくポインタしか保存されていないことが分かります。

そうなると、前回couch_db:doc_flush_atts/2にて、ファイルに書き込んだ際のポインタを#att.dataに設定する箇所があるはずです。couch_db:doc_flush_atts/2couch_db:flush_att/2の戻り値を#doc.attsに設定しているので、couch_db:flush_att/2が返す#att.dataに、アタッチメントデータをファイルに書き込んだ際のポインタが設定されることになります。

Write attachement data to file

前回と同様に、couch_db:flush_att/2when is_binary(Data)というガードがある関数を再度見てみます。

couch_db.erl:

1flush_att(Fd, #att{data=Data}=Att) when is_binary(Data) ->
2    with_stream(Fd, Att, fun(OutputStream) ->
3        couch_stream:write(OutputStream, Data)
4    end);

この関数の戻り値の#att.dataに書き込んだデータのポインタが設定されていることになります。もう一度couch_db:with_stream/3を見てみます。

couch_db.erl:

 1with_stream(Fd, #att{md5=InMd5,type=Type,encoding=Enc}=Att, Fun) ->
 2...
 3    {StreamInfo, Len, IdentityLen, Md5, IdentityMd5} =
 4        couch_stream:close(OutputStream),
 5    check_md5(IdentityMd5, ReqMd5),
 6    {AttLen, DiskLen, NewEnc} = case Enc of
 7    identity ->
 8        case {Md5, IdentityMd5} of
 9        {Same, Same} ->
10            {Len, IdentityLen, identity};
11        _ ->
12            {Len, IdentityLen, gzip}
13        end;
14    gzip ->
15        case {Att#att.att_len, Att#att.disk_len} of
16        {AL, DL} when AL =:= undefined orelse DL =:= undefined ->
17            % Compressed attachment uploaded through the standalone API.
18            {Len, Len, gzip};
19        {AL, DL} ->
20            % This case is used for efficient push-replication, where a
21            % compressed attachment is located in the body of multipart
22            % content-type request.
23            {AL, DL, gzip}
24        end
25    end,
26    Att#att{
27        data={Fd,StreamInfo},
28        att_len=AttLen,
29        disk_len=DiskLen,
30        md5=Md5,
31        encoding=NewEnc
32    }.

前回couch_stream:close/1までしか読みませんでしたが、今回はその先を読んでみます。

この関数の最後のところで#att.dataに対して、{Fd, StreamInfo}を設定しています。このStreamInfocouch_stream:close(OutputStream)を呼び出した戻り値に含まれる値です。この関数は、アタッチメントのデータバッファに残ったデータをファイルに書き込む、という処理で、StreamInfoは書き込んだデータの開始位置とサイズを

[{Pos, BinSize}, {Pos, BinSize}, ...]

という形式で保持しています。

このStreamInfo#att.dataに設定されていることによって、ドキュメントからアタッチメントのデータが取得できるようになっています。

Data block

一度見ていますが、改めてファイルからデータを読み込んでいる部分を見てみます。

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;

最初にcouch_file:read_raw_iolist_int/3で約8KB(2ブロック分)を読み込み、NextPosを取得しています。この関数と、この関数の中で呼び出しているcouch_file:calculate_total_read_len/2を見てみます。

couch_file.erl:

 1-spec read_raw_iolist_int(#file{}, Pos::non_neg_integer(), Len::non_neg_integer()) ->
 2    {Data::iolist(), CurPos::non_neg_integer()}.
 3read_raw_iolist_int(Fd, {Pos, _Size}, Len) -> % 0110 UPGRADE CODE
 4    read_raw_iolist_int(Fd, Pos, Len);
 5read_raw_iolist_int(#file{fd = Fd}, Pos, Len) ->
 6    BlockOffset = Pos rem ?SIZE_BLOCK,
 7    TotalBytes = calculate_total_read_len(BlockOffset, Len),
 8    {ok, <<RawBin:TotalBytes/binary>>} = file:pread(Fd, Pos, TotalBytes),
 9    {remove_block_prefixes(BlockOffset, RawBin), Pos + TotalBytes}.
10...
11calculate_total_read_len(0, FinalLen) ->
12    calculate_total_read_len(1, FinalLen) + 1;
13calculate_total_read_len(BlockOffset, FinalLen) ->
14    case ?SIZE_BLOCK - BlockOffset of
15    BlockLeft when BlockLeft >= FinalLen ->
16        FinalLen;
17    BlockLeft ->
18        FinalLen + ((FinalLen - BlockLeft) div (?SIZE_BLOCK -1)) +
19            if ((FinalLen - BlockLeft) rem (?SIZE_BLOCK -1)) =:= 0 -> 0;
20                true -> 1 end
21    end.

couch_file:read_raw_iolist_int/3の呼び出しでは、Lenは約8KB(2ブロック)、BlockOffsetはデータファイルのファイルポインタ(Pos)に対して、ブロックの先頭からのオフセットを求めています。これらをcouch_file:calculate_total_read_len/2に渡してファイルから読み込むサイズを決定し、ファイルの読み込みを開始したポイント(Pos)からそのサイズ分加算した数値を返します。これがNextPosになっています。

couch_file:calculate_total_read_len/2では、渡されたBlockOffsetから、そのオフセットからブロック終端までのサイズと、FinalLenで指定した読み出すサイズを比較しています。読み出すサイズ(FinalLen)がブロックオフセットからブロック終端までのサイズ、つまりそのブロック内に収まるのであればFinalLenを返します。収まらない場合はこの関数の最後の式になるのですが、(?SIZE_BLOCK -1)という値を使っている理由が良くわかりません。そこで前回追っていったブロック単位で書き込む箇所を再度確認します。

couch_file.erl:

 1make_blocks(_BlockOffset, []) ->
 2    [];
 3make_blocks(0, IoList) ->
 4    [<<0>> | make_blocks(1, IoList)];
 5make_blocks(BlockOffset, IoList) ->
 6    case split_iolist(IoList, (?SIZE_BLOCK - BlockOffset), []) of
 7    {Begin, End} ->
 8        [Begin | make_blocks(0, End)];
 9    _SplitRemaining ->
10        IoList
11    end.

この関数を見ると、ブロックの先頭に<<0>>を設定しています。(?SIZE_BLOCK -1)の-1はこの分を意図しているようです。

最後にcouch_file:read_raw_iolist_int/3で約8KB読み込んだ後に呼び出すcouch_file:maybe_read_more_iolist/4を見てみます。

couch_file.erl:

1maybe_read_more_iolist(Buffer, DataSize, _, _)
2    when DataSize =< byte_size(Buffer) ->
3    <<Data:DataSize/binary, _/binary>> = Buffer,
4    [Data];
5maybe_read_more_iolist(Buffer, DataSize, NextPos, File) ->
6    {Missing, _} =
7        read_raw_iolist_int(File, NextPos, DataSize - byte_size(Buffer)),
8    [Buffer, Missing].

Bufferは読み込んだ8KBの中の実データ部(RestRawData)、Lenは読み込んだ8KBの中で31bitで表現されるデータサイズです。RestRawDataのサイズ>=Len、つまり読み込み済みのデータにすべて収まっている場合は、RestRawDataの先頭からLenの長さ分だけ取って、Dataにバインドします。収まらない場合は、couch_file:read_raw_iolist_int/3を呼び出して収まらない分を読み込みます。

以上より、CouchDBは各データを以下の形式でファイルに読み書きしているようです。

couchdb_block

1ブロックは4KBで、データのポインタはブロック内に存在しています。先頭1bitはフォーマットレイアウト、その次の31ビットはデータサイズ、残りはtermをシリアライズしたバイナリデータです。

Conclusion

前回読んだアタッチメントのデータが、データファイル上でどのように識別されるか見てみました。

アタッチメントデータを表す識別子はデータファイルには書き込まれず、書き込んだデータファイルのファイルポインタを#att.dataに設定しているだけでした。これはファイルに書き込まれたデータそのものにはデータ種別を表す識別子が無くて、データ種別を表現するオブジェクトがそのファイルポインタを持つ、という管理をしていることになります。