前回、アタッチメントをファイルに書き込む箇所を読んだのですが、書き込んだデータがアタッチメントかどうかをどのように判別できるようにしているのか分からなかったので、アタッチメントをファイルから読み出す部分を見てみたいと思います。
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...
rev
やopen_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/5
→couch_d:read_doc/2
→couch_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/2
はcouch_db:flush_att/2
の戻り値を#doc.atts
に設定しているので、couch_db:flush_att/2
が返す#att.data
に、アタッチメントデータをファイルに書き込んだ際のポインタが設定されることになります。
Write attachement data to file
前回と同様に、couch_db:flush_att/2
のwhen 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}
を設定しています。このStreamInfo
はcouch_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は各データを以下の形式でファイルに読み書きしているようです。
1ブロックは4KBで、データのポインタはブロック内に存在しています。先頭1bitはフォーマットレイアウト、その次の31ビットはデータサイズ、残りはtermをシリアライズしたバイナリデータです。
Conclusion
前回読んだアタッチメントのデータが、データファイル上でどのように識別されるか見てみました。
アタッチメントデータを表す識別子はデータファイルには書き込まれず、書き込んだデータファイルのファイルポインタを#att.data
に設定しているだけでした。これはファイルに書き込まれたデータそのものにはデータ種別を表す識別子が無くて、データ種別を表現するオブジェクトがそのファイルポインタを持つ、という管理をしていることになります。