Skip to content

Commit

Permalink
Merge pull request esl#44 from eleostech/retry-httpc
Browse files Browse the repository at this point in the history
Retry SimpleDB 503s, honor NextToken when present
  • Loading branch information
gleber committed Aug 16, 2013
2 parents ae7bc26 + e78d3a9 commit 2996040
Show file tree
Hide file tree
Showing 2 changed files with 264 additions and 9 deletions.
116 changes: 107 additions & 9 deletions src/erlcloud_sdb.erl
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,20 @@
delete_attributes/4, delete_attributes/5,
get_attributes/2, get_attributes/3, get_attributes/4, get_attributes/5,
put_attributes/3, put_attributes/4, put_attributes/5,
select/1, select/2, select/3, select/4
select/1, select/2, select/3, select/4,
select_all/1, select_all/2, select_all/3
]).

%% Export all functions for unit tests
-ifdef(TEST).
-compile(export_all).
-endif.

-include_lib("erlcloud/include/erlcloud.hrl").
-include_lib("erlcloud/include/erlcloud_aws.hrl").

-define(API_VERSION, "2009-04-15").
-define(SDB_TIMEOUT, 10000).

-spec(new/2 :: (string(), string()) -> aws_config()).
new(AccessKeyID, SecretAccessKey) ->
Expand Down Expand Up @@ -213,6 +220,9 @@ put_attributes(DomainName, ItemName, Attributes, Conditionals, Config)
attributes_list(Attributes)] ++ conditionals_list(Conditionals),
sdb_simple_request(Config, "PutAttributes", Params).

%% These functions will return the first page of results along with
%% a token to retrieve the next page, if any.

-spec select/1 :: (string()) -> proplist().
select(SelectExpression) -> select(SelectExpression, none).

Expand All @@ -238,11 +248,64 @@ select(SelectExpression, NextToken, ConsistentRead, Config)
when is_list(SelectExpression),
is_list(NextToken) orelse NextToken =:= none,
is_boolean(ConsistentRead) ->
{Doc, Result} = sdb_request(Config, "Select",
[{"SelectExpression", SelectExpression}, {"NextToken", NextToken},
{"ConsistentRead", ConsistentRead}]),
[{items, extract_items(xmerl_xpath:string("/SelectResponse/SelectResult/Item", Doc))}|
Result].
{Items, NewNextToken, Metadata} = sdb_select_request(SelectExpression,
NextToken,
ConsistentRead,
Config),
Metadata2 = case NewNextToken of
done -> Metadata;
Token -> [{next_token, Token}|Metadata]
end,
[{items, Items}|Metadata2].

%% These functions will make multiple requests until all
%% pages of results have been consumed.

-spec select_all/1 :: (string()) -> proplist().
select_all(SelectExpression) ->
select_all(SelectExpression, false).

-spec select_all/2 :: (string(), boolean()) -> proplist().
select_all(SelectExpression, ConsistentRead)
when is_boolean(ConsistentRead) ->
select_all(SelectExpression, ConsistentRead, default_config());
select_all(SelectExpression, Config) ->
select_all(SelectExpression, false, Config).

-spec select_all/3 :: (string(), boolean(), aws_config()) -> proplist().
select_all(SelectExpression, ConsistentRead, Config)
when is_list(SelectExpression),
is_boolean(ConsistentRead) ->
select_all(SelectExpression, none, ConsistentRead, Config, [], []).

-spec select_all/6 :: (string(), string() | none | done, boolean(),
aws_config(), proplist(), proplist()) -> proplist().
select_all(_, done, _, _, Items, Metadata) ->
[{items, Items}|Metadata];
select_all(SelectExpression, NextToken, ConsistentRead, Config, Items, Metadata) ->
{NewItems, NewNextToken, NewMetadata} = sdb_select_request(SelectExpression,
NextToken,
ConsistentRead,
Config),
select_all(SelectExpression, NewNextToken, ConsistentRead,
Config, Items ++ NewItems, Metadata ++ NewMetadata).

sdb_select_request(SelectExpression, NextToken, ConsistentRead, Config) ->
{Doc, Metadata} = sdb_request(Config, "Select",
[{"SelectExpression", SelectExpression},
{"NextToken", NextToken},
{"ConsistentRead", ConsistentRead}]),
NewNextToken = extract_token(Doc),
Items = extract_items(xmerl_xpath:string("/SelectResponse/SelectResult/Item", Doc)),
{Items, NewNextToken, Metadata}.

extract_token(Doc) ->
case xmerl_xpath:string("/SelectResponse/SelectResult/NextToken", Doc) of
[] ->
done;
[Token] ->
erlcloud_xml:get_text(Token)
end.

extract_items(Items) ->
[extract_item(Item) || Item <- Items].
Expand All @@ -254,10 +317,45 @@ extract_item(Item) ->
].

sdb_request(Config, Action, Params) ->
case sdb_request_with_retry(Config, Action, Params, 1, ?SDB_TIMEOUT, erlang:now()) of
{ok, {Doc, Metadata}} ->
{Doc, Metadata};
{error, Error} ->
erlang:error({aws_error, Error})
end.

sdb_request_with_retry(Config, Action, Params, Try, Timeout, StartTime) ->
case sdb_request_safe(Config, Action, Params) of
{ok, Doc, Metadata} ->
{ok, {Doc, Metadata}};
{error, {http_error, 503, _StatusLine, _Body}} ->
%% Convert from microseconds to milliseconds
Waited = timer:now_diff(erlang:now(), StartTime) / 1000.0,
case Waited of
_TooLong when Waited > Timeout ->
{error, retry_timeout};
_ ->
Wait = math:pow(2, Try) * 200.0,
%% Not exactly random since we're relying on the
%% calling process to hold our PRNG state.
FuzzWait = random:uniform(round(Wait)),
timer:sleep(FuzzWait),
sdb_request_with_retry(Config, Action, Params,
Try + 1, Timeout, StartTime)
end;
{error, _} = Error ->
Error
end.

sdb_request_safe(Config, Action, Params) ->
QParams = [{"Action", Action}, {"Version", ?API_VERSION}|Params],
Doc = erlcloud_aws:aws_request_xml(post, Config#aws_config.sdb_host,
"/", QParams, Config),
{Doc, [{box_usage, erlcloud_xml:get_float("/*/ResponseMetadata/BoxUsage", Doc)}]}.
case erlcloud_aws:aws_request_xml2(post, Config#aws_config.sdb_host,
"/", QParams, Config) of
{ok, Doc} ->
{ok, Doc, [{box_usage, erlcloud_xml:get_float("/*/ResponseMetadata/BoxUsage", Doc)}]};
{error, Error} ->
{error, Error}
end.

sdb_simple_request(Config, Action, Params) ->
{_Doc, Result} = sdb_request(Config, Action, Params),
Expand Down
157 changes: 157 additions & 0 deletions test/erlcloud_sdb_tests.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
-module(erlcloud_sdb_tests).

-ifdef(TEST).
-compile(export_all).

-include_lib("eunit/include/eunit.hrl").

setup() ->
erlcloud_sdb:configure("fake", "fake-secret"),
meck:new(httpc, [unstick]).

cleanup(_) ->
meck:unload(httpc).

%% Helpers

expect_chain([Response | Chain]) ->
meck:expect(httpc, request,
fun(_, _, _, _) ->
expect_chain(Chain),
Response
end);
expect_chain([]) ->
ok.

parse_document(XML) ->
element(1, xmerl_scan:string(XML)).

%% Fixtures

next_token() ->
"rO0ABXNyACdjb20uYW1hem9uLnNkcy5RdWVyeVByb2Nlc3Nvci5Nb3JlVG9rZW7racXLnINNqwMA\nC0kAFGluaXRpYWxDb25qdW5jdEluZGV4WgAOaXNQYWdlQm91bmRhcnlKAAxsYXN0RW50aXR5SURa\nAApscnFFbmFibGVkSQAPcXVlcnlDb21wbGV4aXR5SgATcXVlcnlTdHJpbmdDaGVja3N1bUkACnVu\naW9uSW5kZXhaAA11c2VRdWVyeUluZGV4TAANY29uc2lzdGVudExTTnQAEkxqYXZhL2xhbmcvU3Ry\naW5nO0wAEmxhc3RBdHRyaWJ1dGVWYWx1ZXEAfgABTAAJc29ydE9yZGVydAAvTGNvbS9hbWF6b24v\nc2RzL1F1ZXJ5UHJvY2Vzc29yL1F1ZXJ5JFNvcnRPcmRlcjt4cAAAAAAB//////////8AAAAAAAAA\nAADl8izPAAAAAAB0ABZbMTM1NTM1MzAzNTk2NywyOTE5OTldcH5yAC1jb20uYW1hem9uLnNkcy5R\ndWVyeVByb2Nlc3Nvci5RdWVyeSRTb3J0T3JkZXIAAAAAAAAAABIAAHhyAA5qYXZhLmxhbmcuRW51\nbQAAAAAAAAAAEgAAeHB0AAlBU0NFTkRJTkd4".

single_result_response_body(Name) ->
"<SelectResponse>
<SelectResult>
<Item>
<Name>" ++ Name ++ "</Name>
<Attribute><Name>Color</Name><Value>Black</Value></Attribute>
</Item>
</SelectResult>
<ResponseMetadata>
<RequestId>b1e8f1f7-42e9-494c-ad09-2674e557526d</RequestId>
<BoxUsage>0.1</BoxUsage>
</ResponseMetadata>
</SelectResponse>".

only_token_response_body() ->
"<?xml version=\"1.0\"?><SelectResponse xmlns=\"http://sdb.amazonaws.com/doc/2009-04-15/\"><SelectResult><NextToken>rO0ABXNyACdjb20uYW1hem9uLnNkcy5RdWVyeVByb2Nlc3Nvci5Nb3JlVG9rZW7racXLnINNqwMA\nC0kAFGluaXRpYWxDb25qdW5jdEluZGV4WgAOaXNQYWdlQm91bmRhcnlKAAxsYXN0RW50aXR5SURa\nAApscnFFbmFibGVkSQAPcXVlcnlDb21wbGV4aXR5SgATcXVlcnlTdHJpbmdDaGVja3N1bUkACnVu\naW9uSW5kZXhaAA11c2VRdWVyeUluZGV4TAANY29uc2lzdGVudExTTnQAEkxqYXZhL2xhbmcvU3Ry\naW5nO0wAEmxhc3RBdHRyaWJ1dGVWYWx1ZXEAfgABTAAJc29ydE9yZGVydAAvTGNvbS9hbWF6b24v\nc2RzL1F1ZXJ5UHJvY2Vzc29yL1F1ZXJ5JFNvcnRPcmRlcjt4cAAAAAAB//////////8AAAAAAAAA\nAADl8izPAAAAAAB0ABZbMTM1NTM1MzAzNTk2NywyOTE5OTldcH5yAC1jb20uYW1hem9uLnNkcy5R\ndWVyeVByb2Nlc3Nvci5RdWVyeSRTb3J0T3JkZXIAAAAAAAAAABIAAHhyAA5qYXZhLmxhbmcuRW51\nbQAAAAAAAAAAEgAAeHB0AAlBU0NFTkRJTkd4</NextToken></SelectResult><ResponseMetadata><RequestId>f20b398c-9744-0f0c-5b8a-41c436e41987</RequestId><BoxUsage>0.0000137200</BoxUsage></ResponseMetadata></SelectResponse>".

single_result_and_token_response_body() ->
"<?xml version=\"1.0\"?><SelectResponse xmlns=\"http://sdb.amazonaws.com/doc/2009-04-15/\"><SelectResult><Item><Name>item0</Name><Attribute><Name>Color</Name><Value>Black</Value></Attribute></Item><NextToken>rO0ABXNyACdjb20uYW1hem9uLnNkcy5RdWVyeVByb2Nlc3Nvci5Nb3JlVG9rZW7racXLnINNqwMA\nC0kAFGluaXRpYWxDb25qdW5jdEluZGV4WgAOaXNQYWdlQm91bmRhcnlKAAxsYXN0RW50aXR5SURa\nAApscnFFbmFibGVkSQAPcXVlcnlDb21wbGV4aXR5SgATcXVlcnlTdHJpbmdDaGVja3N1bUkACnVu\naW9uSW5kZXhaAA11c2VRdWVyeUluZGV4TAANY29uc2lzdGVudExTTnQAEkxqYXZhL2xhbmcvU3Ry\naW5nO0wAEmxhc3RBdHRyaWJ1dGVWYWx1ZXEAfgABTAAJc29ydE9yZGVydAAvTGNvbS9hbWF6b24v\nc2RzL1F1ZXJ5UHJvY2Vzc29yL1F1ZXJ5JFNvcnRPcmRlcjt4cAAAAAAB//////////8AAAAAAAAA\nAADl8izPAAAAAAB0ABZbMTM1NTM1MzAzNTk2NywyOTE5OTldcH5yAC1jb20uYW1hem9uLnNkcy5R\ndWVyeVByb2Nlc3Nvci5RdWVyeSRTb3J0T3JkZXIAAAAAAAAAABIAAHhyAA5qYXZhLmxhbmcuRW51\nbQAAAAAAAAAAEgAAeHB0AAlBU0NFTkRJTkd4</NextToken></SelectResult><ResponseMetadata><RequestId>f20b398c-9744-0f0c-5b8a-41c436e41987</RequestId><BoxUsage>0.0000137200</BoxUsage></ResponseMetadata></SelectResponse>".

single_result_response() ->
single_result_response("item0").

single_result_response(Name) ->
{ok, {{0, 200, ""}, [], single_result_response_body(Name)}}.

only_token_response() ->
{ok, {{0, 200, ""}, [], only_token_response_body()}}.

single_result_and_token_response() ->
{ok, {{0, 200, ""}, [], single_result_and_token_response_body()}}.

unavailable_response() ->
{ok, {{0, 503, "Unavailable"}, [], ""}}.

%% Tests - select
select_test_() ->
{foreach, local,
fun setup/0,
fun cleanup/1,
[{test, ?MODULE, select_single_response},
{test, ?MODULE, select_next_token}
]}.

select_single_response() ->
expect_chain([single_result_response()]),

Result = erlcloud_sdb:select("select"),
?assertEqual(undefined, proplists:get_value(next_token, Result)).

select_next_token() ->
expect_chain([only_token_response(),
single_result_response()]),

Result = erlcloud_sdb:select("select"),
?assertEqual(next_token(), proplists:get_value(next_token, Result)).

%% Tests - select_all

select_all_test_() ->
{foreach, local,
fun setup/0,
fun cleanup/1,
[{test, ?MODULE, select_all_single_response},
{test, ?MODULE, select_all_failure},
{test, ?MODULE, select_all_503},
{test, ?MODULE, select_all_next_token},
{test, ?MODULE, select_all_next_and_failure},
{test, ?MODULE, select_all_two_results},
{test, ?MODULE, extract_token_test}
]}.

select_all_single_response() ->
expect_chain([single_result_response()]),

Result = erlcloud_sdb:select_all("select"),
Items = proplists:get_value(items, Result),
?assertEqual(1, length(Items)).

select_all_failure() ->
expect_chain([{error, {conn_failed,{error,ssl_not_started}}}]),

{'EXIT', {Error, _Stack}} = (catch erlcloud_sdb:select_all("select")),
?assertEqual({aws_error,{socket_error,{conn_failed,{error,ssl_not_started}}}}, Error).

select_all_503() ->
expect_chain([unavailable_response(),
single_result_response()]),

Result = erlcloud_sdb:select_all("select"),
Items = proplists:get_value(items, Result),
?assertEqual(1, length(Items)).

select_all_next_token() ->
expect_chain([only_token_response(),
single_result_response()]),

Result = erlcloud_sdb:select_all("select"),
Items = proplists:get_value(items, Result),
?assertEqual(1, length(Items)).

select_all_next_and_failure() ->
expect_chain([only_token_response(),
unavailable_response(),
single_result_response()]),

Result = erlcloud_sdb:select_all("select"),
Items = proplists:get_value(items, Result),
?assertEqual(1, length(Items)).

select_all_two_results() ->
expect_chain([single_result_and_token_response(),
single_result_response("item1")]),

Result = erlcloud_sdb:select_all("select"),
Items = proplists:get_value(items, Result),
?assertEqual(["item0", "item1"], [proplists:get_value(name, Item) || Item <- Items]).

extract_token_test() ->
?assertEqual(next_token(), erlcloud_sdb:extract_token(parse_document(only_token_response_body()))),
?assertEqual(next_token(), erlcloud_sdb:extract_token(parse_document(single_result_and_token_response_body()))),
?assertEqual(done, erlcloud_sdb:extract_token(parse_document(single_result_response_body("item0")))).

-endif.

0 comments on commit 2996040

Please sign in to comment.