You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

htb.py 9.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. # -*- test-case-name: twisted.test.test_htb -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. """
  5. Hierarchical Token Bucket traffic shaping.
  6. Patterned after U{Martin Devera's Hierarchical Token Bucket traffic
  7. shaper for the Linux kernel<http://luxik.cdi.cz/~devik/qos/htb/>}.
  8. @seealso: U{HTB Linux queuing discipline manual - user guide
  9. <http://luxik.cdi.cz/~devik/qos/htb/manual/userg.htm>}
  10. @seealso: U{Token Bucket Filter in Linux Advanced Routing & Traffic Control
  11. HOWTO<http://lartc.org/howto/lartc.qdisc.classless.html#AEN682>}
  12. """
  13. # TODO: Investigate whether we should be using os.times()[-1] instead of
  14. # time.time. time.time, it has been pointed out, can go backwards. Is
  15. # the same true of os.times?
  16. from time import time
  17. from zope.interface import implementer, Interface
  18. from twisted.protocols import pcp
  19. class Bucket:
  20. """
  21. Implementation of a Token bucket.
  22. A bucket can hold a certain number of tokens and it drains over time.
  23. @cvar maxburst: The maximum number of tokens that the bucket can
  24. hold at any given time. If this is L{None}, the bucket has
  25. an infinite size.
  26. @type maxburst: C{int}
  27. @cvar rate: The rate at which the bucket drains, in number
  28. of tokens per second. If the rate is L{None}, the bucket
  29. drains instantaneously.
  30. @type rate: C{int}
  31. """
  32. maxburst = None
  33. rate = None
  34. _refcount = 0
  35. def __init__(self, parentBucket=None):
  36. """
  37. Create a L{Bucket} that may have a parent L{Bucket}.
  38. @param parentBucket: If a parent Bucket is specified,
  39. all L{add} and L{drip} operations on this L{Bucket}
  40. will be applied on the parent L{Bucket} as well.
  41. @type parentBucket: L{Bucket}
  42. """
  43. self.content = 0
  44. self.parentBucket = parentBucket
  45. self.lastDrip = time()
  46. def add(self, amount):
  47. """
  48. Adds tokens to the L{Bucket} and its C{parentBucket}.
  49. This will add as many of the C{amount} tokens as will fit into both
  50. this L{Bucket} and its C{parentBucket}.
  51. @param amount: The number of tokens to try to add.
  52. @type amount: C{int}
  53. @returns: The number of tokens that actually fit.
  54. @returntype: C{int}
  55. """
  56. self.drip()
  57. if self.maxburst is None:
  58. allowable = amount
  59. else:
  60. allowable = min(amount, self.maxburst - self.content)
  61. if self.parentBucket is not None:
  62. allowable = self.parentBucket.add(allowable)
  63. self.content += allowable
  64. return allowable
  65. def drip(self):
  66. """
  67. Let some of the bucket drain.
  68. The L{Bucket} drains at the rate specified by the class
  69. variable C{rate}.
  70. @returns: C{True} if the bucket is empty after this drip.
  71. @returntype: C{bool}
  72. """
  73. if self.parentBucket is not None:
  74. self.parentBucket.drip()
  75. if self.rate is None:
  76. self.content = 0
  77. else:
  78. now = time()
  79. deltaTime = now - self.lastDrip
  80. deltaTokens = deltaTime * self.rate
  81. self.content = max(0, self.content - deltaTokens)
  82. self.lastDrip = now
  83. return self.content == 0
  84. class IBucketFilter(Interface):
  85. def getBucketFor(*somethings, **some_kw):
  86. """
  87. Return a L{Bucket} corresponding to the provided parameters.
  88. @returntype: L{Bucket}
  89. """
  90. @implementer(IBucketFilter)
  91. class HierarchicalBucketFilter:
  92. """
  93. Filter things into buckets that can be nested.
  94. @cvar bucketFactory: Class of buckets to make.
  95. @type bucketFactory: L{Bucket}
  96. @cvar sweepInterval: Seconds between sweeping out the bucket cache.
  97. @type sweepInterval: C{int}
  98. """
  99. bucketFactory = Bucket
  100. sweepInterval = None
  101. def __init__(self, parentFilter=None):
  102. self.buckets = {}
  103. self.parentFilter = parentFilter
  104. self.lastSweep = time()
  105. def getBucketFor(self, *a, **kw):
  106. """
  107. Find or create a L{Bucket} corresponding to the provided parameters.
  108. Any parameters are passed on to L{getBucketKey}, from them it
  109. decides which bucket you get.
  110. @returntype: L{Bucket}
  111. """
  112. if ((self.sweepInterval is not None)
  113. and ((time() - self.lastSweep) > self.sweepInterval)):
  114. self.sweep()
  115. if self.parentFilter:
  116. parentBucket = self.parentFilter.getBucketFor(self, *a, **kw)
  117. else:
  118. parentBucket = None
  119. key = self.getBucketKey(*a, **kw)
  120. bucket = self.buckets.get(key)
  121. if bucket is None:
  122. bucket = self.bucketFactory(parentBucket)
  123. self.buckets[key] = bucket
  124. return bucket
  125. def getBucketKey(self, *a, **kw):
  126. """
  127. Construct a key based on the input parameters to choose a L{Bucket}.
  128. The default implementation returns the same key for all
  129. arguments. Override this method to provide L{Bucket} selection.
  130. @returns: Something to be used as a key in the bucket cache.
  131. """
  132. return None
  133. def sweep(self):
  134. """
  135. Remove empty buckets.
  136. """
  137. for key, bucket in self.buckets.items():
  138. bucket_is_empty = bucket.drip()
  139. if (bucket._refcount == 0) and bucket_is_empty:
  140. del self.buckets[key]
  141. self.lastSweep = time()
  142. class FilterByHost(HierarchicalBucketFilter):
  143. """
  144. A Hierarchical Bucket filter with a L{Bucket} for each host.
  145. """
  146. sweepInterval = 60 * 20
  147. def getBucketKey(self, transport):
  148. return transport.getPeer()[1]
  149. class FilterByServer(HierarchicalBucketFilter):
  150. """
  151. A Hierarchical Bucket filter with a L{Bucket} for each service.
  152. """
  153. sweepInterval = None
  154. def getBucketKey(self, transport):
  155. return transport.getHost()[2]
  156. class ShapedConsumer(pcp.ProducerConsumerProxy):
  157. """
  158. Wraps a C{Consumer} and shapes the rate at which it receives data.
  159. """
  160. # Providing a Pull interface means I don't have to try to schedule
  161. # traffic with callLaters.
  162. iAmStreaming = False
  163. def __init__(self, consumer, bucket):
  164. pcp.ProducerConsumerProxy.__init__(self, consumer)
  165. self.bucket = bucket
  166. self.bucket._refcount += 1
  167. def _writeSomeData(self, data):
  168. # In practice, this actually results in obscene amounts of
  169. # overhead, as a result of generating lots and lots of packets
  170. # with twelve-byte payloads. We may need to do a version of
  171. # this with scheduled writes after all.
  172. amount = self.bucket.add(len(data))
  173. return pcp.ProducerConsumerProxy._writeSomeData(self, data[:amount])
  174. def stopProducing(self):
  175. pcp.ProducerConsumerProxy.stopProducing(self)
  176. self.bucket._refcount -= 1
  177. class ShapedTransport(ShapedConsumer):
  178. """
  179. Wraps a C{Transport} and shapes the rate at which it receives data.
  180. This is a L{ShapedConsumer} with a little bit of magic to provide for
  181. the case where the consumer it wraps is also a C{Transport} and people
  182. will be attempting to access attributes this does not proxy as a
  183. C{Consumer} (e.g. C{loseConnection}).
  184. """
  185. # Ugh. We only wanted to filter IConsumer, not ITransport.
  186. iAmStreaming = False
  187. def __getattr__(self, name):
  188. # Because people will be doing things like .getPeer and
  189. # .loseConnection on me.
  190. return getattr(self.consumer, name)
  191. class ShapedProtocolFactory:
  192. """
  193. Dispense C{Protocols} with traffic shaping on their transports.
  194. Usage::
  195. myserver = SomeFactory()
  196. myserver.protocol = ShapedProtocolFactory(myserver.protocol,
  197. bucketFilter)
  198. Where C{SomeServerFactory} is a L{twisted.internet.protocol.Factory}, and
  199. C{bucketFilter} is an instance of L{HierarchicalBucketFilter}.
  200. """
  201. def __init__(self, protoClass, bucketFilter):
  202. """
  203. Tell me what to wrap and where to get buckets.
  204. @param protoClass: The class of C{Protocol} this will generate
  205. wrapped instances of.
  206. @type protoClass: L{Protocol<twisted.internet.interfaces.IProtocol>}
  207. class
  208. @param bucketFilter: The filter which will determine how
  209. traffic is shaped.
  210. @type bucketFilter: L{HierarchicalBucketFilter}.
  211. """
  212. # More precisely, protoClass can be any callable that will return
  213. # instances of something that implements IProtocol.
  214. self.protocol = protoClass
  215. self.bucketFilter = bucketFilter
  216. def __call__(self, *a, **kw):
  217. """
  218. Make a C{Protocol} instance with a shaped transport.
  219. Any parameters will be passed on to the protocol's initializer.
  220. @returns: A C{Protocol} instance with a L{ShapedTransport}.
  221. """
  222. proto = self.protocol(*a, **kw)
  223. origMakeConnection = proto.makeConnection
  224. def makeConnection(transport):
  225. bucket = self.bucketFilter.getBucketFor(transport)
  226. shapedTransport = ShapedTransport(transport, bucket)
  227. return origMakeConnection(shapedTransport)
  228. proto.makeConnection = makeConnection
  229. return proto