Current section
Files
Jump to
Current section
Files
src/eredis_parser.erl
%%
%% Parser of the Redis protocol, see http://redis.io/topics/protocol
%%
%% The idea behind this parser is that we accept any binary data
%% available on the socket. If there is not enough data to parse a
%% complete response, we ask the caller to call us later when there is
%% more data. If there is too much data, we only parse the first
%% response and let the caller call us again with the rest.
%%
%% This approach lets us write a "pure" parser that does not depend on
%% manipulating the socket, which erldis and redis-erl is
%% doing. Instead, we may ask the socket to send us data as fast as
%% possible and parse it continously. The overhead of manipulating the
%% socket when parsing multibulk responses is killing the performance
%% of erldis.
%%
%% Future improvements:
%% * When we return a bulk continuation, we also include the size of
%% the bulk. The caller may use this to explicitly call
%% gen_tcp:recv/2 with the desired size.
-module(eredis_parser).
-include("eredis.hrl").
-include_lib("eunit/include/eunit.hrl").
-export([init/0, parse/2]).
%% Exported for testing
-export([parse_bulk/1, parse_bulk/2,
parse_multibulk/1, parse_multibulk/2, buffer_create/0, buffer_create/1]).
%%
%% API
%%
%% @doc: Initialize the parser
init() ->
#pstate{}.
-spec parse(State::#pstate{}, Data::binary()) ->
{ok, return_value(), NewState::#pstate{}} |
{ok, return_value(), Rest::binary(), NewState::#pstate{}} |
{error, ErrString::binary(), NewState::#pstate{}} |
{error, ErrString::binary(), Rest::binary(), NewState::#pstate{}} |
{continue, NewState::#pstate{}}.
%% @doc: Parses the (possibly partial) response from Redis. Returns
%% either {ok, Value, NewState}, {ok, Value, Rest, NewState} or
%% {continue, NewState}. External entry point for parsing.
%%
%% In case {ok, Value, NewState} is returned, Value contains the value
%% returned by Redis. NewState will be an empty parser state.
%%
%% In case {ok, Value, Rest, NewState} is returned, Value contains the
%% most recent value returned by Redis, while Rest contains any extra
%% data that was given, but was not part of the same response. In this
%% case you should immeditely call parse again with Rest as the Data
%% argument and NewState as the State argument.
%%
%% In case {continue, NewState} is returned, more data is needed
%% before a complete value can be returned. As soon as you have more
%% data, call parse again with NewState as the State argument and any
%% new binary data as the Data argument.
%% Parser in initial state, the data we receive will be the beginning
%% of a response
parse(#pstate{state = undefined} = State, NewData) ->
%% Look at the first byte to get the type of reply
case NewData of
%% Status
<<$+, Data/binary>> ->
return_result(parse_simple(Data), State, status_continue);
%% Error
<<$-, Data/binary>> ->
return_error(parse_simple(Data), State, status_continue);
%% Integer reply
<<$:, Data/binary>> ->
return_result(parse_simple(Data), State, status_continue);
%% Multibulk
<<$*, _Rest/binary>> ->
return_result(parse_multibulk(NewData), State, multibulk_continue);
%% Bulk
<<$$, _Rest/binary>> ->
return_result(parse_bulk(NewData), State, bulk_continue);
_ ->
%% TODO: Handle the case where we start parsing a new
%% response, but cannot make any sense of it
{error, unknown_response}
end;
%% The following clauses all match on different continuation states
parse(#pstate{state = bulk_continue,
continuation_data = ContinuationData} = State, NewData) ->
return_result(parse_bulk(ContinuationData, NewData), State, bulk_continue);
parse(#pstate{state = multibulk_continue,
continuation_data = ContinuationData} = State, NewData) ->
return_result(parse_multibulk(ContinuationData, NewData), State, multibulk_continue);
parse(#pstate{state = status_continue,
continuation_data = ContinuationData} = State, NewData) ->
return_result(parse_simple(ContinuationData, NewData), State, status_continue).
%%
%% MULTIBULK
%%
parse_multibulk(Data) when is_binary(Data) -> parse_multibulk(buffer_create(Data));
parse_multibulk(Buffer) ->
case get_newline_pos(Buffer) of
undefined ->
{continue, {incomplete_size, Buffer}};
NewlinePos ->
OffsetNewlinePos = NewlinePos - 1,
<<$*, Size:OffsetNewlinePos/binary, ?NL, Bulk/binary>> = buffer_to_binary(Buffer),
IntSize = list_to_integer(binary_to_list(Size)),
do_parse_multibulk(IntSize, buffer_create(Bulk))
end.
%% Size of multibulk was incomplete, try again
parse_multibulk({incomplete_size, Buffer}, NewData0) ->
NewBuffer = buffer_append(Buffer, NewData0),
parse_multibulk(NewBuffer);
%% Ran out of data inside do_parse_multibulk in parse_bulk, must
%% continue traversing the bulks
parse_multibulk({in_parsing_bulks, Count, Buffer, Acc},
NewData0) ->
NewBuffer = buffer_append(Buffer, NewData0),
%% Continue where we left off
do_parse_multibulk(Count, NewBuffer, Acc).
%% @doc: Parses the given number of bulks from Data. If Data does not
%% contain enough bulks, {continue, ContinuationData} is returned with
%% enough information to start parsing with the correct count and
%% accumulated data.
do_parse_multibulk(Count, Buffer) ->
do_parse_multibulk(Count, Buffer, []).
do_parse_multibulk(-1, Buffer, []) ->
{ok, undefined, buffer_to_binary(Buffer)};
do_parse_multibulk(0, Buffer, Acc) ->
{ok, lists:reverse(Acc), buffer_to_binary(Buffer)};
do_parse_multibulk(Count, Buffer, Acc) ->
case buffer_size(Buffer) == 0 of
true -> {continue, {in_parsing_bulks, Count, buffer_create(), Acc}};
false ->
%% Try parsing the first bulk in Data, if it works, we get the
%% extra data back that was not part of the bulk which we can
%% recurse on. If the bulk does not contain enough data, we
%% return with a continuation and enough data to pick up where we
%% left off. In the continuation we will get more data
%% automagically in Data, so parsing the bulk might work.
case parse_bulk(Buffer) of
{ok, Value, Rest} ->
do_parse_multibulk(Count - 1, buffer_create(Rest), [Value | Acc]);
{continue, _} ->
{continue, {in_parsing_bulks, Count, Buffer, Acc}}
end
end.
%%
%% BULK
%%
parse_bulk(Data) when is_binary(Data) -> parse_bulk(buffer_create(Data));
parse_bulk(Buffer) ->
case buffer_hd(Buffer) of
[$*] -> parse_multibulk(Buffer);
[$+] -> parse_simple(buffer_tl(Buffer));
[$-] -> parse_simple(buffer_tl(Buffer));
[$:] -> parse_simple(buffer_tl(Buffer));
[$$] -> do_parse_bulk(Buffer)
end.
%% Bulk, at beginning of response
do_parse_bulk(Buffer) ->
%% Find the position of the first terminator, everything up until
%% this point contains the size specifier. If we cannot find it,
%% we received a partial response and need more data
case get_newline_pos(Buffer) of
undefined ->
{continue, {incomplete_size, Buffer}};
NewlinePos ->
OffsetNewlinePos = NewlinePos - 1, % Take into account the first $
<<$$, Size:OffsetNewlinePos/binary, Bulk/binary>> = buffer_to_binary(Buffer),
IntSize = list_to_integer(binary_to_list(Size)),
if
%% Nil response from redis
IntSize =:= -1 ->
<<?NL, Rest/binary>> = Bulk,
{ok, undefined, Rest};
%% We have enough data for the entire bulk
size(Bulk) - (size(<<?NL>>) * 2) >= IntSize ->
<<?NL, Value:IntSize/binary, ?NL, Rest/binary>> = Bulk,
{ok, Value, Rest};
true ->
%% Need more data, so we send the bulk without the
%% size specifier to our future self
{continue, {IntSize, buffer_create(Bulk)}}
end
end.
%% Bulk, continuation from partial bulk size
parse_bulk({incomplete_size, Buffer}, NewData0) ->
NewBuffer = buffer_append(Buffer, NewData0),
parse_bulk(NewBuffer);
%% Bulk, continuation from partial bulk value
parse_bulk({IntSize, Buffer0}, Data) ->
Buffer = buffer_append(Buffer0, Data),
case buffer_size(Buffer) - (size(<<?NL>>) * 2) >= IntSize of
true ->
<<?NL, Value:IntSize/binary, ?NL, Rest/binary>> = buffer_to_binary(Buffer),
{ok, Value, Rest};
false ->
{continue, {IntSize, Buffer}}
end.
%%
%% SIMPLE REPLIES
%%
%% Handles replies on the following format:
%% TData\r\n
%% Where T is a type byte, like '+', '-', ':'. Data is terminated by \r\n
%% @doc: Parse simple replies. Data must not contain type
%% identifier. Type must be handled by the caller.
parse_simple(Data) when is_binary(Data) -> parse_simple(buffer_create(Data));
parse_simple(Buffer) ->
case get_newline_pos(Buffer) of
undefined ->
{continue, {incomplete_simple, Buffer}};
NewlinePos ->
<<Value:NewlinePos/binary, ?NL, Rest/binary>> = buffer_to_binary(Buffer),
{ok, Value, Rest}
end.
parse_simple({incomplete_simple, Buffer}, NewData0) ->
NewBuffer = buffer_append(Buffer, NewData0),
parse_simple(NewBuffer).
%%
%% INTERNAL HELPERS
%%
get_newline_pos({B, _}) ->
case re:run(B, ?NL) of
{match, [{Pos, _}]} -> Pos;
nomatch -> undefined
end.
buffer_create() ->
{[], 0}.
buffer_create(Data) ->
{[Data], byte_size(Data)}.
buffer_append({List, Size}, Binary) ->
NewList = case List of
[] -> [Binary];
[Head | Tail] -> [Head, Tail, Binary]
end,
{NewList, Size + byte_size(Binary)}.
buffer_hd({[<<Char, _/binary>> | _], _}) -> [Char];
buffer_hd({[], _}) -> [].
buffer_tl({[<<_, RestBin/binary>> | Rest], Size}) -> {[RestBin | Rest], Size - 1}.
buffer_to_binary({List, _}) -> iolist_to_binary(List).
buffer_size({_, Size}) -> Size.
%% @doc: Helper for handling the result of parsing. Will update the
%% parser state with the continuation of given name if necessary.
return_result({ok, Value, <<>>}, _State, _StateName) ->
{ok, Value, init()};
return_result({ok, Value, Rest}, _State, _StateName) ->
{ok, Value, Rest, init()};
return_result({continue, ContinuationData}, State, StateName) ->
{continue, State#pstate{state = StateName, continuation_data = ContinuationData}}.
%% @doc: Helper for returning an error. Uses return_result/3 and just transforms the {ok, ...} tuple into an error tuple
return_error(Result, State, StateName) ->
case return_result(Result, State, StateName) of
{ok, Value, ParserState} ->
{error, Value, ParserState};
{ok, Value, Rest, ParserState} ->
{error, Value, Rest, ParserState};
Res ->
Res
end.