quartz_torrent 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -89,7 +89,7 @@ module QuartzTorrent
89
89
  @logger.debug "Peer supports extension #{extName} using id '#{extId}'."
90
90
  @extendedMessageIdToClass[extId] = clazz
91
91
  else
92
- @logger.warn "Peer supports extension #{extName} using id '#{extId}', but I don't know what class to use for that extension."
92
+ @logger.debug "Peer supports extension #{extName} using id '#{extId}', but I don't know what class to use for that extension."
93
93
  end
94
94
  end
95
95
  else
@@ -37,6 +37,12 @@ module QuartzTorrent
37
37
  @pool -= n
38
38
  end
39
39
 
40
+ def to_s
41
+ s = ""
42
+ s << "units/sec: #{@unitsPerSecond}, avail: #{avail}, upperlim: #{@upperLimit}"
43
+ s
44
+ end
45
+
40
46
  private
41
47
  def updatePool
42
48
  now = Time.new
@@ -1,8 +1,8 @@
1
1
  require 'socket'
2
- require 'pqueue'
3
2
  require 'fiber'
4
3
  require 'thread'
5
4
  require 'quartz_torrent/ratelimit'
5
+ require 'quartz_torrent/timermanager'
6
6
  include Socket::Constants
7
7
 
8
8
  module QuartzTorrent
@@ -311,7 +311,6 @@ module QuartzTorrent
311
311
  @ioInfo.writeRateLimit = rate
312
312
  end
313
313
 
314
- attr_accessor :writeRateLimit
315
314
  end
316
315
 
317
316
  # An IoFacade that doesn't allow reading. This is not part of the public API.
@@ -346,6 +345,7 @@ module QuartzTorrent
346
345
  @useErrorhandler = true
347
346
  @readRateLimit = nil
348
347
  @writeRateLimit = nil
348
+ raise "IO passed to IOInfo initialize may not be nil" if io.nil?
349
349
  end
350
350
  attr_accessor :io
351
351
  attr_accessor :metainfo
@@ -366,84 +366,6 @@ module QuartzTorrent
366
366
  end
367
367
  end
368
368
 
369
- # Class used to manage timers.
370
- class TimerManager
371
- class TimerInfo
372
- def initialize(duration, recurring, metainfo)
373
- @duration = duration
374
- @recurring = recurring
375
- @metainfo = metainfo
376
- @cancelled = false
377
- refresh
378
- end
379
- attr_accessor :recurring
380
- attr_accessor :duration
381
- attr_accessor :expiry
382
- attr_accessor :metainfo
383
- # Since pqueue doesn't allow removal of anything but the head
384
- # we flag deleted items so they are deleted when they are pulled
385
- attr_accessor :cancelled
386
-
387
- def secondsUntilExpiry
388
- @expiry - Time.new
389
- end
390
-
391
- def refresh
392
- @expiry = Time.new + @duration
393
- end
394
- end
395
-
396
- def initialize(logger = nil)
397
- @queue = PQueue.new { |a,b| b.expiry <=> a.expiry }
398
- @mutex = Mutex.new
399
- @logger = logger
400
- end
401
-
402
- # Add a timer. Parameter 'duration' specifies the timer duration in seconds,
403
- # 'metainfo' is caller information passed to the handler when the timer expires,
404
- # 'recurring' should be true if the timer will repeat, or false if it will only
405
- # expire once, and 'immed' when true specifies that the timer should expire immediately
406
- # (and again each duration if recurring) while false specifies that the timer will only
407
- # expire the first time after it's duration elapses.
408
- def add(duration, metainfo = nil, recurring = true, immed = false)
409
- raise "TimerManager.add: Timer duration may not be nil" if duration.nil?
410
- info = TimerInfo.new(duration, recurring, metainfo)
411
- info.expiry = Time.new if immed
412
- @mutex.synchronize{ @queue.push info }
413
- info
414
- end
415
-
416
- # Cancel a timer.
417
- def cancel(timerInfo)
418
- timerInfo.cancelled = true
419
- end
420
-
421
- # Return the next timer event from the queue, but don't remove it from the queue.
422
- def peek
423
- clearCancelled
424
- @queue.top
425
- end
426
-
427
- # Remove the next timer event from the queue and return it as a TimerHandler::TimerInfo object.
428
- def next
429
- clearCancelled
430
- result = nil
431
- @mutex.synchronize{ result = @queue.pop }
432
- if result && result.recurring
433
- result.refresh
434
- @mutex.synchronize{ @queue.push result }
435
- end
436
- result
437
- end
438
-
439
- private
440
-
441
- def clearCancelled
442
- while @queue.top && @queue.top.cancelled
443
- info = @queue.pop
444
- end
445
- end
446
- end
447
369
 
448
370
  # This class implements the Reactor pattern. The Reactor listens for activity on IO objects and calls methods on
449
371
  # an associated Handler object when activity is detected. Callers can use listen, connect or open to register IO
@@ -494,7 +416,9 @@ module QuartzTorrent
494
416
  @stopped
495
417
  end
496
418
 
497
- # Create a TCP connection to the specified host
419
+ # Create a TCP connection to the specified host.
420
+ # Note that this method may raise exceptions. For example 'Too many open files' might be raised if
421
+ # the process is using too many file descriptors
498
422
  def connect(addr, port, metainfo, timeout = nil)
499
423
  ioInfo = startConnection(port, addr, metainfo)
500
424
  @ioInfo[ioInfo.io] = ioInfo
@@ -704,14 +628,16 @@ module QuartzTorrent
704
628
  end
705
629
  end
706
630
 
707
- if selectResult.nil?
708
- # Process timer
709
- @logger.debug "eventloop: processing timer" if @logger
631
+ if timer
710
632
  # Calling processTimer in withReadFiber here is not correct. What if at this point the fiber was already paused in a read, and we
711
633
  # want to process a timer? In that case we will resume the read and it will possibly finish, but we'll never
712
634
  # call the timer handler. For this reason we must prevent read calls in timerHandlers.
713
- processTimer(timer) if timer
714
- else
635
+ # Process timer
636
+ @logger.debug "eventloop: processing timer" if @logger
637
+ processTimer(timer)
638
+ end
639
+
640
+ if ! selectResult.nil?
715
641
  readable, writeable = selectResult
716
642
 
717
643
  # If we are stopped, then ignore reads; we only care about completing our writes that were pending when we were stopped.
@@ -727,6 +653,10 @@ module QuartzTorrent
727
653
  end
728
654
 
729
655
  @currentIoInfo = @ioInfo[io]
656
+
657
+ # The IOInfo associated with this io could have been closed by the timer handler processed above.
658
+ next if @currentIoInfo.nil?
659
+
730
660
  if @currentIoInfo.state == :listening
731
661
  @logger.debug "eventloop: calling handleAccept for IO metainfo=#{@currentIoInfo.metainfo}" if @logger
732
662
  # Replace the currentIoInfo with the accepted socket
@@ -773,9 +703,9 @@ module QuartzTorrent
773
703
  selectTimeout = nil
774
704
  timer = nil
775
705
  while true && ! @stopped
776
- timer = @timerManager.peek
706
+ secondsUntilExpiry = nil
707
+ timer = @timerManager.next{ |s| secondsUntilExpiry = s }
777
708
  break if ! timer
778
- secondsUntilExpiry = timer.secondsUntilExpiry
779
709
  if secondsUntilExpiry > 0
780
710
  selectTimeout = secondsUntilExpiry
781
711
  break
@@ -844,7 +774,6 @@ module QuartzTorrent
844
774
  @logger.error "Exception in timer event handler: #{$!}" if @logger
845
775
  @logger.error $!.backtrace.join "\n" if @logger
846
776
  end
847
- @timerManager.next
848
777
  end
849
778
 
850
779
  def disposeIo(io)
@@ -9,19 +9,31 @@ class Semaphore
9
9
  end
10
10
 
11
11
  # Wait on the semaphore. If the count zero or below, the calling thread blocks.
12
- def wait
12
+ # Optionally a timeout in seconds can be specified. This method returns true if the wait
13
+ # ended because of a signal, and false if it ended because of a timeout.
14
+ def wait(timeout = nil)
15
+ result = true
13
16
  c = nil
14
17
  @mutex.synchronize do
15
18
  @count -= 1
16
- c = @count
19
+ if @count < 0
20
+ @sleeping.push Thread.current
21
+ @mutex.sleep(timeout)
22
+ end
17
23
  end
18
-
19
- if c < 0
24
+ if timeout
25
+ # If we had a timeout we may have woken due to it expiring rather than
26
+ # due to signal being called. In that case we need to remove ourself from the sleepers.
20
27
  @mutex.synchronize do
21
- @sleeping.push Thread.current
28
+ i = @sleeping.index(Thread.current)
29
+ if i
30
+ @count += 1
31
+ @sleeping.delete_at(i)
32
+ result = false
33
+ end
22
34
  end
23
- Thread.stop
24
35
  end
36
+ result
25
37
  end
26
38
 
27
39
  # Signal the semaphore. If the count is below zero the waiting threads are woken.
@@ -30,14 +42,16 @@ class Semaphore
30
42
  @mutex.synchronize do
31
43
  c = @count
32
44
  @count += 1
33
- end
34
- if c < 0
35
- t = nil
36
- @mutex.synchronize do
45
+ if c < 0
37
46
  t = @sleeping.shift
47
+ t.wakeup if t
38
48
  end
39
- t.wakeup if t
40
49
  end
41
50
  end
51
+
52
+ # Testing method.
53
+ def count
54
+ @count
55
+ end
42
56
  end
43
57
 
@@ -0,0 +1,120 @@
1
+ require 'pqueue'
2
+
3
+ module QuartzTorrent
4
+ # Class used to manage timers.
5
+ class TimerManager
6
+ class TimerInfo
7
+ def initialize(duration, recurring, metainfo)
8
+ @duration = duration
9
+ @recurring = recurring
10
+ @metainfo = metainfo
11
+ @cancelled = false
12
+ refresh
13
+ end
14
+ attr_accessor :recurring
15
+ attr_accessor :duration
16
+ attr_accessor :expiry
17
+ attr_accessor :metainfo
18
+ # Since pqueue doesn't allow removal of anything but the head
19
+ # we flag deleted items so they are deleted when they are pulled
20
+ attr_accessor :cancelled
21
+
22
+ def secondsUntilExpiry
23
+ @expiry - Time.new
24
+ end
25
+
26
+ def refresh
27
+ @expiry = Time.new + @duration
28
+ end
29
+ end
30
+
31
+ def initialize(logger = nil)
32
+ @queue = PQueue.new { |a,b| b.expiry <=> a.expiry }
33
+ @mutex = Mutex.new
34
+ @logger = logger
35
+ end
36
+
37
+ # Add a timer. Parameter 'duration' specifies the timer duration in seconds,
38
+ # 'metainfo' is caller information passed to the handler when the timer expires,
39
+ # 'recurring' should be true if the timer will repeat, or false if it will only
40
+ # expire once, and 'immed' when true specifies that the timer should expire immediately
41
+ # (and again each duration if recurring) while false specifies that the timer will only
42
+ # expire the first time after it's duration elapses.
43
+ def add(duration, metainfo = nil, recurring = true, immed = false)
44
+ raise "TimerManager.add: Timer duration may not be nil" if duration.nil?
45
+ info = TimerInfo.new(duration, recurring, metainfo)
46
+ info.expiry = Time.new if immed
47
+ @mutex.synchronize{ @queue.push info }
48
+ info
49
+ end
50
+
51
+ # For testing. Add a cancelled timer.
52
+ def add_cancelled(duration, metainfo = nil, recurring = true, immed = false)
53
+ raise "TimerManager.add: Timer duration may not be nil" if duration.nil?
54
+ info = TimerInfo.new(duration, recurring, metainfo)
55
+ info.expiry = Time.new if immed
56
+ info.cancelled = true
57
+ @mutex.synchronize{ @queue.push info }
58
+ info
59
+ end
60
+
61
+ # Cancel a timer.
62
+ def cancel(timerInfo)
63
+ timerInfo.cancelled = true
64
+ end
65
+
66
+ # Return the next timer event from the queue, but don't remove it from the queue.
67
+ def peek
68
+ result = nil
69
+ @mutex.synchronize do
70
+ clearCancelled
71
+ result = @queue.top
72
+ end
73
+ result
74
+ end
75
+
76
+ # Remove the next timer event from the queue and return it as a TimerHandler::TimerInfo object.
77
+ # Warning: if the timer is a recurring timer, the secondsUntilExpiry will be set to the NEXT time
78
+ # the timer would expire, instead of this time. If the original secondsUntilExpiry is needed,
79
+ # pass a block to this method, and the block will be called with the original secondsUntilExpiry.
80
+ def next
81
+ result = nil
82
+ @mutex.synchronize do
83
+ clearCancelled
84
+ result = @queue.pop
85
+ end
86
+ if result
87
+ yield result.secondsUntilExpiry if block_given?
88
+ if result.recurring
89
+ result.refresh
90
+ @mutex.synchronize{ @queue.push result }
91
+ end
92
+ end
93
+ result
94
+ end
95
+
96
+ def to_s
97
+ arr = nil
98
+ @mutex.synchronize do
99
+ arr = @queue.to_a
100
+ end
101
+ s = "now = #{Time.new}. Queue = ["
102
+ arr.each do |e|
103
+ s << "(#{e.object_id};#{e.expiry};#{e.metainfo[0]};#{e.secondsUntilExpiry}),"
104
+ end
105
+ s << "]"
106
+ end
107
+
108
+ def empty?
109
+ @queue.empty?
110
+ end
111
+
112
+ private
113
+
114
+ def clearCancelled
115
+ while @queue.top && @queue.top.cancelled
116
+ @queue.pop
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,91 @@
1
+ require "quartz_torrent/log.rb"
2
+
3
+ module QuartzTorrent
4
+ class TorrentQueue
5
+ # The maxIncomplete and maxActive parameters specify how many torrents may be unpaused and unqueued at once.
6
+ # Parameter maxActive is the total maximum number of active torrents (unpaused, unqueued), and maxIncomplete is a subset of
7
+ # maxActive that are incomplete and thus downloading (as opposed to only uploading). An exception is thrown if
8
+ # maxIncomplete > maxActive.
9
+ #
10
+ def initialize(maxIncomplete, maxActive)
11
+ raise "The maximum number of running torrents may not be larger than the maximum number of unqueued torrents" if maxIncomplete > maxActive
12
+ @maxIncomplete = maxIncomplete
13
+ @maxActive = maxActive
14
+ @queue = []
15
+ @logger = LogManager.getLogger("queue")
16
+ end
17
+
18
+ # Compute which torrents can now be unqueued based on the state of running torrents.
19
+ # Parameter torrentDatas should be an array of TorrentData that the decision will be based off. At a minimum
20
+ # these items should respond to 'paused', 'queued', 'state', 'queued='
21
+ def dequeue(torrentDatas)
22
+ numActive = 0
23
+ numIncomplete = 0
24
+ torrentDatas.each do |torrentData|
25
+ next if torrentData.paused || torrentData.queued
26
+ numIncomplete += 1 if incomplete?(torrentData)
27
+ numActive += 1
28
+ end
29
+ @logger.debug "incomplete: #{numIncomplete}/#{@maxIncomplete} active: #{numActive}/#{@maxActive}"
30
+ @logger.debug "Queue contains #{size} torrents"
31
+
32
+ torrents = []
33
+
34
+ while numActive < @maxActive
35
+ torrentData = nil
36
+ if numIncomplete < @maxIncomplete
37
+ # Unqueue first incomplete torrent from queue
38
+ torrentData = dequeueFirstMatching{ |torrentData| incomplete?(torrentData)}
39
+ @logger.debug "#{torrentData ? "dequeued" : "failed to dequeue"} an incomplete torrent"
40
+ numIncomplete += 1 if torrentData
41
+ end
42
+ if ! torrentData
43
+ # Unqueue first complete (uploading) torrent from queue
44
+ torrentData = dequeueFirstMatching{ |torrentData| !incomplete?(torrentData)}
45
+ @logger.debug "#{torrentData ? "dequeued" : "failed to dequeue"} a complete torrent"
46
+ end
47
+ numActive += 1 if torrentData
48
+
49
+ if torrentData
50
+ torrentData.queued = false
51
+ torrents.push torrentData
52
+ else
53
+ break
54
+ end
55
+ end
56
+
57
+ @logger.debug "Will dequeue #{torrents.size}"
58
+
59
+ torrents
60
+ end
61
+
62
+ def push(torrentData)
63
+ torrentData.queued = true
64
+ @queue.push torrentData
65
+ end
66
+
67
+ def unshift(torrentData)
68
+ torrentData.queued = true
69
+ @queue.unshift torrentData
70
+ end
71
+
72
+ def size
73
+ @queue.size
74
+ end
75
+
76
+ private
77
+
78
+ def incomplete?(torrentData)
79
+ torrentData.state != :uploading
80
+ end
81
+
82
+ def dequeueFirstMatching
83
+ index = @queue.index{ |torrentData| yield(torrentData) }
84
+ if index
85
+ @queue.delete_at index
86
+ else
87
+ nil
88
+ end
89
+ end
90
+ end
91
+ end