Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added a timeout value for the take_group_member #72

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 23 additions & 7 deletions src/pooler.erl
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
take_member/1,
take_member/2,
take_group_member/1,
take_group_member/2,
return_group_member/2,
return_group_member/3,
return_member/2,
Expand Down Expand Up @@ -202,13 +203,21 @@ take_member(PoolName) when is_atom(PoolName) orelse is_pid(PoolName) ->
take_member(PoolName, Timeout) when is_atom(PoolName) orelse is_pid(PoolName) ->
gen_server:call(PoolName, {take_member, time_as_millis(Timeout)}, infinity).


%% @doc Take a member from a randomly selected member of the group
%% `GroupName'. Returns `MemberPid' or `error_no_members'. If no
%% members are available in the randomly chosen pool, all other pools
%% in the group are tried in order.
-spec take_group_member(atom()) -> pid() | error_no_members | {error_no_group, atom()}.
take_group_member(GroupName) ->
take_group_member(GroupName) -> take_group_member(GroupName, 0).

%% @doc Take a member from a randomly selected member of the group
%% `GroupName'. Returns `MemberPid' or `error_no_members'. If no
%% members are available in the randomly chosen pool, all other pools
%% in the group are tried in order. If no member
%% is available within the specified timeout, error_no_members is returned.
%% `Timeout' can be either milliseconds as integer or `{duration, time_unit}'
-spec take_group_member(atom(), non_neg_integer()) -> pid() | error_no_members | {error_no_group, atom()}.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spec here for Timeout doesn't include {duration(), time_unit()} -- if we're going to support that, the spec should allow it as well.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes. I will add that when I have some free time this weekend.

take_group_member(GroupName, Timeout) ->
case pg2:get_local_members(GroupName) of
{error, {no_such_group, GroupName}} ->
{error_no_group, GroupName};
Expand All @@ -220,18 +229,25 @@ take_group_member(GroupName) ->
{_, _, X} = os:timestamp(),
Idx = (X rem length(Pools)) + 1,
{PoolPid, Rest} = extract_nth(Idx, Pools),
take_first_pool([PoolPid | Rest])
case {take_first_pool([PoolPid | Rest], 0), Timeout} of
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why a first case with hard-coded zero timeout?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not have a lot of time to truly dig into the internals of how pooler works so if any of my assumptions are wrong please let me know:

From the looks of the code when you call the "take_first_pool" it would basically call "take_member" with a zero timeout. If you pass in a zero timeout it will not grow the pool if it is a capacity (or if it does grow the pool, it will not wait around for a response). The reason I do the 0 timeout first it to make sure no one pool gets too big.

Example:

Lets assume I have two pools each one connected to a different Riak server with an initial capacity of 1 and a max capacity of 100: If I passed in the non-zero timeout first and the sudo random number being used (via the os:timestamp call) always landed on 1, I would end up with one pool with 100 connections and 1 pool with 1 connection. In such a case this would unbalance the load placed on the the 2 riak servers.

That said the big issue I have with this change is if you have a lot of pools (like 1000), you would be making 2000 gen_server calls just to find out there are no pools left with any space.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also I completely acknowledge that my argument maybe purely academic and in reality this is just not the case (or so rare that it is not worth handling) :P

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I see what you're concerned about. I think that random selection of the pool makes a single-large-pool issue a non-problem. Are you concerned that the selection isn't random enough?

How many pools are you using? 1000 strikes me as a large number -- at least in the sense that I didn't consider such as a use case to solve for in the initial design.

It would be possible to build a small experiment to model out how the pools in the group would grow.

On balance, I'd prefer the simpler code path and think that the random selection makes that OK.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 1000 pools was just a crazy max case kind of thing, in practice we run one pool per Riak DB (so typically 5) and configure them with a init capacity of 10 and a max of 100. When I first ran the change without the first pass of 0 through our load simulator I was seeing pools sizes roughly round 25, 20, 15, 10, 10 and after the change it evened out (around 14 per pool give or take one). In the end nothing that was so critical that the system became unstable but I did find that typically a few servers received more load than others.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also wonder if a solution like what dispcount(https://github.com/ferd/dispcount) does maybe better. He uses an ets table with counters to determine which pool is the best to pull from. However that has a disadvantage of doing more ets calls which would be much more expensive then just getting the timestamp ...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ferd also keeps all the processes in an ets table so there are no gen_server calls to pull a process however not sure if that would work good for riak given vector clock issue you cite in the Readme.md.

{error_no_members, 0} ->
error_no_members;
{error_no_members, _} ->
take_first_pool([PoolPid | Rest], Timeout);
{Member, _} ->
Member
end
end.

take_first_pool([PoolPid | Rest]) ->
case take_member(PoolPid) of
take_first_pool([PoolPid | Rest], Timeout) ->
case take_member(PoolPid, Timeout) of
error_no_members ->
take_first_pool(Rest);
take_first_pool(Rest, Timeout);
Member ->
ets:insert(?POOLER_GROUP_TABLE, {Member, PoolPid}),
Member
end;
take_first_pool([]) ->
take_first_pool([], _) ->
error_no_members.

%% this helper function returns `{Nth_Elt, Rest}' where `Nth_Elt' is
Expand Down