1 %%%-------------------------------------------------------------------
2 %%% File : etorrent_t_peer_group.erl
3 %%% Author : Jesper Louis Andersen <jesper.louis.andersen@gmail.com>
4 %%% License : See COPYING
5 %%% Description : Master process for a number of peers.
7 %%% Created : 18 Jul 2007 by
8 %%% Jesper Louis Andersen <jesper.louis.andersen@gmail.com>
9 %%%-------------------------------------------------------------------
10 -module(etorrent_choker
).
12 -behaviour(gen_server
).
14 -include("rate_mgr.hrl").
15 -include("peer_state.hrl").
17 -include("etorrent_mnesia_table.hrl").
20 -export([start_link
/1, perform_rechoke
/0, monitor
/1]).
22 %% gen_server callbacks
23 -export([init
/1, handle_call
/3, handle_cast
/2, handle_info
/2,
24 terminate
/2, code_change
/3]).
26 -record(state
, {our_peer_id
= none
,
32 optimistic_unchoke_pid
= none
,
33 opt_unchoke_chain
= []}).
35 -record(rechoke_info
, {pid :: pid(),
36 kind
:: 'seeding' | 'leeching',
37 state
:: 'seeding' | 'leeching' ,
39 r_interest_state
:: 'interested' | 'not_interested',
40 r_choke_state
:: 'choked' | 'unchoked' ,
44 -define(SERVER
, ?MODULE
).
46 -define(ROUND_TIME
, 10000).
48 %%====================================================================
50 %%====================================================================
51 start_link(OurPeerId
) ->
52 gen_server:start_link({local
, ?SERVER
}, ?MODULE
, [OurPeerId
], []).
55 gen_server:cast(?SERVER
, rechoke
).
58 gen_server:call(?SERVER
, {monitor
, Pid
}).
60 %%====================================================================
61 %% gen_server callbacks
62 %%====================================================================
65 process_flag(trap_exit
, true
),
66 {ok
, Tref
} = timer:send_interval(?ROUND_TIME
, self(), round_tick
),
67 {ok
, #state
{ our_peer_id
= OurPeerId
,
70 handle_call({monitor
, Pid
}, _From
, S
) ->
71 _Tref
= erlang:monitor(process, Pid
),
72 NewChain
= insert_new_peer_into_chain(Pid
, S#state
.opt_unchoke_chain
),
74 {reply
, ok
, S#state
{ opt_unchoke_chain
= NewChain
}};
75 handle_call(Request
, _From
, State
) ->
76 error_logger:error_report([unknown_peer_group_call
, Request
]),
78 {reply
, Reply
, State
}.
79 handle_cast(rechoke
, S
) ->
82 handle_cast(_Msg
, State
) ->
85 handle_info(round_tick
, S
) ->
88 {ok
, NS
} = advance_optimistic_unchoke(S
),
90 {noreply
, NS#state
{ round = 2}};
91 N
when is_integer(N
) ->
93 {noreply
, S#state
{round = S#state
.round - 1}}
95 handle_info({'DOWN', _Ref
, process, Pid
, Reason
}, S
)
96 when (Reason
=:= normal
) or (Reason
=:= shutdown
) ->
97 % The peer shut down normally. Hence we just remove him and start up
98 % other peers. Eventually the tracker will re-add him to the peer list
100 % XXX: We might have to do something else
103 NewChain
= lists:delete(Pid
, S#state
.opt_unchoke_chain
),
104 {noreply
, S#state
{ opt_unchoke_chain
= NewChain
}};
105 handle_info({'DOWN', _Ref
, process, Pid
, _Reason
}, S
) ->
106 % The peer shut down unexpectedly re-add him to the queue in the *back*
107 NS
= case etorrent_peer:select(Pid
) of
109 % XXX: We might have to check that remote is intersted and we were choking
114 NewChain
= lists:delete(Pid
, NS#state
.opt_unchoke_chain
),
115 {noreply
, NS#state
{opt_unchoke_chain
= NewChain
}};
116 handle_info(Info
, State
) ->
117 error_logger:error_report([unknown_info_peer_group
, Info
]),
120 terminate(Reason
, _S
) ->
121 error_logger:info_report([peer_group_mgr_term
, Reason
]),
124 code_change(_OldVsn
, State
, _Extra
) ->
127 %%--------------------------------------------------------------------
128 %%% Internal functions
129 %%--------------------------------------------------------------------
131 %%--------------------------------------------------------------------
132 %% Function: rechoke(State) -> ok
133 %% Description: Recalculate the choke/unchoke state of peers
134 %%--------------------------------------------------------------------
136 Peers
= build_rechoke_info(S#state
.opt_unchoke_chain
),
137 {PreferredDown
, PreferredSeed
} = split_preferred(Peers
),
138 PreferredSet
= prune_preferred_peers(PreferredDown
, PreferredSeed
),
139 ToChoke
= rechoke_unchoke(Peers
, PreferredSet
),
140 rechoke_choke(ToChoke
, 0, optimistics(PreferredSet
)).
142 build_rechoke_info(Peers
) ->
143 SeederSet
= sets:from_list(seeding_torrents()),
144 build_rechoke_info(SeederSet
, Peers
).
146 build_rechoke_info(_Seeding
, []) ->
148 build_rechoke_info(Seeding
, [Pid
| Next
]) ->
149 case etorrent_peer:select(Pid
) of
150 [] -> build_rechoke_info(Seeding
, Next
);
152 Kind
= Peer#peer
.state
,
153 Snubbed
= etorrent_rate_mgr:snubbed(Peer#peer
.torrent_id
, Pid
),
154 PeerState
= etorrent_rate_mgr:select_state(Peer#peer
.torrent_id
, Pid
),
155 case sets:is_element(Peer#peer
.torrent_id
, Seeding
) of
157 case etorrent_rate_mgr:fetch_send_rate(
158 Peer#peer
.torrent_id
,
160 none
-> build_rechoke_info(Seeding
, Next
);
162 [#rechoke_info
{ pid = Pid
,
167 PeerState#peer_state
.interest_state
,
169 PeerState#peer_state
.choke_state
,
171 PeerState#peer_state
.local_choke
,
172 snubbed
= Snubbed
} |
173 build_rechoke_info(Seeding
, Next
)]
176 case etorrent_rate_mgr:fetch_recv_rate(
177 Peer#peer
.torrent_id
,
179 none
-> build_rechoke_info(Seeding
, Next
);
181 [#rechoke_info
{ pid = Pid
,
184 rate
= -Rate
, % Inverted for later sorting!
186 PeerState#peer_state
.interest_state
,
188 PeerState#peer_state
.choke_state
,
190 PeerState#peer_state
.local_choke
,
191 snubbed
= Snubbed
} |
192 build_rechoke_info(Seeding
, Next
)]
197 advance_optimistic_unchoke(S
) ->
198 NewChain
= move_cyclic_chain(S#state
.opt_unchoke_chain
),
201 {ok
, S
}; %% No peers yet
203 etorrent_t_peer_recv:unchoke(H
),
204 {ok
, S#state
{ opt_unchoke_chain
= NewChain
,
205 optimistic_unchoke_pid
= H
}}
208 move_cyclic_chain([]) -> [];
209 move_cyclic_chain(Chain
) ->
211 case etorrent_peer:select(Pid
) of
213 [P
] -> T
= etorrent_rate_mgr:select_state(
216 not (T#peer_state
.interest_state
=:= interested
217 andalso T#peer_state
.choke_state
=:= choked
)
220 {Front
, Back
} = lists:splitwith(F
, Chain
),
224 insert_new_peer_into_chain(Pid
, Chain
) ->
225 Length
= length(Chain
),
226 Index
= lists:max([0, crypto:rand_uniform(0, Length
)]),
227 {Front
, Back
} = lists:split(Index
, Chain
),
228 Front
++ [Pid
| Back
].
231 case application:get_env(etorrent
, max_upload_slots
) of
233 {ok
, Rate
} = application:get_env(etorrent
, max_upload_rate
),
235 N
when N
=< 0 -> 7; %% Educated guess
240 round(math:sqrt(N
* 0.6))
242 {ok
, N
} when is_integer(N
) ->
246 split_preferred(Peers
) ->
247 {Downs
, Leechs
} = split_preferred_peers(Peers
, [], []),
248 {lists:keysort(#rechoke_info
.rate
, Downs
),
249 lists:keysort(#rechoke_info
.rate
, Leechs
)}.
251 prune_preferred_peers(SDowns
, SLeechs
) ->
252 MaxUploads
= upload_slots(),
253 DUploads
= lists:max([1, round(MaxUploads
* 0.7)]),
254 SUploads
= lists:max([1, round(MaxUploads
* 0.3)]),
256 case lists:max([0, DUploads
- length(SDowns
)]) of
257 0 -> {SUploads
, DUploads
};
258 N
-> {SUploads
+ N
, DUploads
- N
}
261 case lists:max([0, SUP2
- length(SLeechs
)]) of
264 {SUP2
- K
, lists:min([DUP2
+ K
, length(SDowns
)])}
266 {TSDowns
, TSLeechs
} = {lists:sublist(SDowns
, DUP3
),
267 lists:sublist(SLeechs
, SUP3
)},
268 sets:union(sets:from_list(TSDowns
), sets:from_list(TSLeechs
)).
270 rechoke_unchoke([], _PS
) ->
272 rechoke_unchoke([P
| Next
], PSet
) ->
273 case sets:is_element(P
, PSet
) of
275 etorrent_t_peer_recv:unchoke(P#rechoke_info
.pid),
276 rechoke_unchoke(Next
, PSet
);
278 [P
| rechoke_unchoke(Next
, PSet
)]
282 MinUp
= case application:get_env(etorrent
, min_uploads
) of
286 lists:max([MinUp
, upload_slots() - sets:size(PSet
)]).
288 rechoke_choke([], _Count
, _Optimistics
) ->
290 rechoke_choke([P
| Next
], Count
, Optimistics
) when Count
>= Optimistics
->
291 etorrent_t_peer_recv:choke(P#rechoke_info
.pid),
292 rechoke_choke(Next
, Count
, Optimistics
);
293 rechoke_choke([P
| Next
], Count
, Optimistics
) ->
294 case P#rechoke_info
.kind
=:= seeding
of
296 etorrent_t_peer_recv:choke(P#rechoke_info
.pid),
297 rechoke_choke(Next
, Count
, Optimistics
);
299 etorrent_t_peer_recv:unchoke(P#rechoke_info
.pid),
300 case P#rechoke_info
.r_interest_state
=:= interested
of
302 rechoke_choke(Next
, Count
+1, Optimistics
);
304 rechoke_choke(Next
, Count
, Optimistics
)
308 split_preferred_peers([], Downs
, Leechs
) ->
310 split_preferred_peers([P
| Next
], Downs
, Leechs
) ->
311 case P#rechoke_info
.kind
=:= seeding
312 orelse P#rechoke_info
.r_interest_state
=:= not_interested
of
314 split_preferred_peers(Next
, Downs
, Leechs
);
315 false
when P#rechoke_info
.state
=:= seeding
->
316 split_preferred_peers(Next
, Downs
, [P
| Leechs
]);
317 false
when P#rechoke_info
.snubbed
=:= true
->
318 split_preferred_peers(Next
, Downs
, Leechs
);
320 split_preferred_peers(Next
, [P
| Downs
], Leechs
)
323 seeding_torrents() ->
324 {atomic
, Torrents
} = etorrent_torrent:all(),
325 [T#torrent
.id
|| T
<- Torrents
,
326 T#torrent
.state
=:= seeding
].