158 lines
5.8 KiB
Python
Executable File
158 lines
5.8 KiB
Python
Executable File
# The contents of this file are subject to the BitTorrent Open Source License
|
|
# Version 1.1 (the License). You may not copy or use this file, in either
|
|
# source code or executable form, except in compliance with the License. You
|
|
# may obtain a copy of the License at http://www.bittorrent.com/license/.
|
|
#
|
|
# Software distributed under the License is distributed on an AS IS basis,
|
|
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
# for the specific language governing rights and limitations under the
|
|
# License.
|
|
|
|
# Written by Bram Cohen
|
|
|
|
from random import randrange
|
|
from math import sqrt
|
|
|
|
class Choker(object):
|
|
|
|
def __init__(self, config, schedule, done = lambda: False):
|
|
self.config = config
|
|
self.schedule = schedule
|
|
self.connections = []
|
|
self.count = 0
|
|
self.done = done
|
|
self.unchokes_since_last = 0
|
|
schedule(self._round_robin, 10)
|
|
|
|
def _round_robin(self):
|
|
self.schedule(self._round_robin, 10)
|
|
self.count += 1
|
|
if self.done():
|
|
self._rechoke_seed(True)
|
|
return
|
|
if self.count % 3 == 0:
|
|
for i in xrange(len(self.connections)):
|
|
u = self.connections[i].upload
|
|
if u.choked and u.interested:
|
|
self.connections = self.connections[i:] + self.connections[:i]
|
|
break
|
|
self._rechoke()
|
|
|
|
def _rechoke(self):
|
|
if self.done():
|
|
self._rechoke_seed()
|
|
return
|
|
preferred = []
|
|
for i in xrange(len(self.connections)):
|
|
c = self.connections[i]
|
|
if c.upload.interested and not c.download.is_snubbed() and c.download.have.numfalse:
|
|
preferred.append((-c.download.get_rate(), i))
|
|
preferred.sort()
|
|
prefcount = min(len(preferred), self._max_uploads() -1)
|
|
mask = [0] * len(self.connections)
|
|
for _, i in preferred[:prefcount]:
|
|
mask[i] = 1
|
|
count = max(1, self.config['min_uploads'] - prefcount)
|
|
for i in xrange(len(self.connections)):
|
|
c = self.connections[i]
|
|
u = c.upload
|
|
if mask[i]:
|
|
u.unchoke(self.count)
|
|
elif count > 0 and c.download.have.numfalse:
|
|
u.unchoke(self.count)
|
|
if u.interested:
|
|
count -= 1
|
|
else:
|
|
u.choke()
|
|
|
|
def _rechoke_seed(self, force_new_unchokes = False):
|
|
if force_new_unchokes:
|
|
# number of unchokes per 30 second period
|
|
i = (self._max_uploads() + 2) // 3
|
|
# this is called 3 times in 30 seconds, if i==4 then unchoke 1+1+2
|
|
# and so on; substract unchokes recently triggered by disconnects
|
|
num_force_unchokes = max(0, (i + self.count % 3) // 3 - \
|
|
self.unchokes_since_last)
|
|
else:
|
|
num_force_unchokes = 0
|
|
preferred = []
|
|
new_limit = self.count - 3
|
|
for i in xrange(len(self.connections)):
|
|
c = self.connections[i]
|
|
u = c.upload
|
|
if not u.choked and u.interested and c.download.have.numfalse:
|
|
if u.unchoke_time > new_limit or (
|
|
u.buffer and c.connection.is_flushed()):
|
|
preferred.append((-u.unchoke_time, -u.get_rate(), i))
|
|
else:
|
|
preferred.append((1, -u.get_rate(), i))
|
|
num_kept = self._max_uploads() - num_force_unchokes
|
|
assert num_kept >= 0
|
|
preferred.sort()
|
|
preferred = preferred[:num_kept]
|
|
mask = [0] * len(self.connections)
|
|
for _, _, i in preferred:
|
|
mask[i] = 1
|
|
num_nonpref = self._max_uploads() - len(preferred)
|
|
if force_new_unchokes:
|
|
self.unchokes_since_last = 0
|
|
else:
|
|
self.unchokes_since_last += num_nonpref
|
|
last_unchoked = None
|
|
for i in xrange(len(self.connections)):
|
|
c = self.connections[i]
|
|
u = c.upload
|
|
if not mask[i]:
|
|
if not u.interested:
|
|
u.choke()
|
|
elif u.choked:
|
|
if num_nonpref > 0 and c.connection.is_flushed() and c.download.have.numfalse:
|
|
u.unchoke(self.count)
|
|
num_nonpref -= 1
|
|
if num_nonpref == 0:
|
|
last_unchoked = i
|
|
else:
|
|
if num_nonpref == 0 or not c.download.have.numfalse:
|
|
u.choke()
|
|
else:
|
|
num_nonpref -= 1
|
|
if num_nonpref == 0:
|
|
last_unchoked = i
|
|
if last_unchoked is not None:
|
|
self.connections = self.connections[last_unchoked + 1:] + \
|
|
self.connections[:last_unchoked + 1]
|
|
|
|
def connection_made(self, connection):
|
|
p = randrange(len(self.connections) + 1)
|
|
self.connections.insert(p, connection)
|
|
|
|
def connection_lost(self, connection):
|
|
self.connections.remove(connection)
|
|
if connection.upload.interested and not connection.upload.choked:
|
|
self._rechoke()
|
|
|
|
def interested(self, connection):
|
|
if not connection.upload.choked:
|
|
self._rechoke()
|
|
|
|
def not_interested(self, connection):
|
|
if not connection.upload.choked:
|
|
self._rechoke()
|
|
|
|
def _max_uploads(self):
|
|
uploads = self.config['max_uploads']
|
|
rate = self.config['max_upload_rate']
|
|
if uploads > 0:
|
|
pass
|
|
elif rate <= 0:
|
|
uploads = 7 # unlimited, just guess something here...
|
|
elif rate < 9:
|
|
uploads = 2
|
|
elif rate < 15:
|
|
uploads = 3
|
|
elif rate < 42:
|
|
uploads = 4
|
|
else:
|
|
uploads = int(sqrt(rate * .6))
|
|
return uploads
|