quartz_torrent 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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