quartz_torrent 0.0.1

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.
Files changed (35) hide show
  1. data/bin/quartztorrent_download +127 -0
  2. data/bin/quartztorrent_download_curses +841 -0
  3. data/bin/quartztorrent_magnet_from_torrent +32 -0
  4. data/bin/quartztorrent_show_info +62 -0
  5. data/lib/quartz_torrent.rb +2 -0
  6. data/lib/quartz_torrent/bitfield.rb +314 -0
  7. data/lib/quartz_torrent/blockstate.rb +354 -0
  8. data/lib/quartz_torrent/classifiedpeers.rb +95 -0
  9. data/lib/quartz_torrent/extension.rb +37 -0
  10. data/lib/quartz_torrent/filemanager.rb +543 -0
  11. data/lib/quartz_torrent/formatter.rb +92 -0
  12. data/lib/quartz_torrent/httptrackerclient.rb +121 -0
  13. data/lib/quartz_torrent/interruptiblesleep.rb +27 -0
  14. data/lib/quartz_torrent/log.rb +132 -0
  15. data/lib/quartz_torrent/magnet.rb +92 -0
  16. data/lib/quartz_torrent/memprofiler.rb +27 -0
  17. data/lib/quartz_torrent/metainfo.rb +221 -0
  18. data/lib/quartz_torrent/metainfopiecestate.rb +265 -0
  19. data/lib/quartz_torrent/peer.rb +145 -0
  20. data/lib/quartz_torrent/peerclient.rb +1627 -0
  21. data/lib/quartz_torrent/peerholder.rb +123 -0
  22. data/lib/quartz_torrent/peermanager.rb +170 -0
  23. data/lib/quartz_torrent/peermsg.rb +502 -0
  24. data/lib/quartz_torrent/peermsgserialization.rb +102 -0
  25. data/lib/quartz_torrent/piecemanagerrequestmetadata.rb +12 -0
  26. data/lib/quartz_torrent/rate.rb +58 -0
  27. data/lib/quartz_torrent/ratelimit.rb +48 -0
  28. data/lib/quartz_torrent/reactor.rb +949 -0
  29. data/lib/quartz_torrent/regionmap.rb +124 -0
  30. data/lib/quartz_torrent/semaphore.rb +43 -0
  31. data/lib/quartz_torrent/trackerclient.rb +271 -0
  32. data/lib/quartz_torrent/udptrackerclient.rb +70 -0
  33. data/lib/quartz_torrent/udptrackermsg.rb +250 -0
  34. data/lib/quartz_torrent/util.rb +100 -0
  35. metadata +195 -0
@@ -0,0 +1,102 @@
1
+ require 'bencode'
2
+ require 'quartz_torrent/peermsg'
3
+ require 'quartz_torrent/extension'
4
+
5
+ module QuartzTorrent
6
+ class PeerWireMessageSerializer
7
+ @@classForMessage = nil
8
+ # The mapping of our extended message ids to extensions. This is different than @extendedMessageIdToClass which is
9
+ # the mapping of peer message ids to extensions, which is different for every peer.
10
+ @@classForExtendedMessage = nil
11
+
12
+ def initialize
13
+ # extendedMessageIdToClass is the mapping of extended message ids that the peer has sent to extensions.
14
+ @extendedMessageIdToClass = [ExtendedHandshake]
15
+ @logger = LogManager.getLogger("peermsg_serializer")
16
+ end
17
+
18
+ def unserializeFrom(io)
19
+ packedLength = io.read(4)
20
+ raise EOFError.new if packedLength.length == 0
21
+
22
+ length = packedLength.unpack("N")[0]
23
+ @logger.debug "unserializeFrom: read that length of message is #{length}"
24
+ raise "Received peer message with length #{length}. All messages must have length >= 0" if length < 0
25
+ return KeepAlive.new if length == 0
26
+
27
+ id = io.read(1).unpack("C")[0]
28
+ @logger.debug "unserializeFrom: read message id #{id}"
29
+ payload = io.read(length-1)
30
+
31
+ #raise "Unsupported peer message id #{id}" if id >= self.classForMessage.length
32
+ clazz = classForMessage(id, payload)
33
+ raise "Unsupported peer message id #{id}" if ! clazz
34
+
35
+ result = clazz.new
36
+ result.unserialize(payload)
37
+ updateExtendedMessageIdsFromHandshake(result)
38
+ result
39
+ end
40
+
41
+ def serializeTo(msg, io)
42
+ if msg.is_a?(Extended)
43
+ # Set the extended message id
44
+ extendedMsgId = @extendedMessageIdToClass.index msg.class
45
+ raise "Unsupported extended peer message #{msg.class}" if ! extendedMsgId
46
+ msg.extendedMessageId = extendedMsgId
47
+ end
48
+ msg.serializeTo(io)
49
+ end
50
+
51
+ private
52
+ # Determine the class associated with the message type passed.
53
+ def classForMessage(id, payload)
54
+ if @@classForMessage.nil?
55
+ @@classForMessage = [Choke, Unchoke, Interested, Uninterested, Have, BitfieldMessage, Request, Piece, Cancel]
56
+ @@classForMessage[20] = Extended
57
+ end
58
+
59
+ if @@classForExtendedMessage.nil?
60
+ @@classForExtendedMessage = []
61
+ @@classForExtendedMessage[Extension::MetadataExtensionId] = ExtendedMetaInfo
62
+ end
63
+
64
+ result = @@classForMessage[id]
65
+
66
+ if result == Extended && payload
67
+ # Extended messages have further subtypes.
68
+ extendedMsgId = payload.unpack("C")[0]
69
+ if extendedMsgId == 0
70
+ result = ExtendedHandshake
71
+ else
72
+ # In this case the extended message number is the one we told our peers to use, not the one the peer told us.
73
+ result = @@classForExtendedMessage[extendedMsgId]
74
+ raise "Unsupported extended peer message id '#{extendedMsgId}'" if ! result
75
+ end
76
+
77
+ end
78
+
79
+ result
80
+ end
81
+
82
+ def updateExtendedMessageIdsFromHandshake(msg)
83
+ if msg.is_a?(ExtendedHandshake)
84
+ if msg.dict && msg.dict["m"]
85
+ msg.dict["m"].each do |extName, extId|
86
+ # Update the list here.
87
+ clazz = Extension.peerMsgClassForExtensionName(extName)
88
+ if clazz
89
+ @logger.debug "Peer supports extension #{extName} using id '#{extId}'."
90
+ @extendedMessageIdToClass[extId] = clazz
91
+ else
92
+ @logger.warn "Peer supports extension #{extName} using id '#{extId}', but I don't know what class to use for that extension."
93
+ end
94
+ end
95
+ else
96
+ @logger.warn "Peer sent extended handshake without the 'm' key."
97
+ end
98
+ end
99
+ end
100
+
101
+ end
102
+ end
@@ -0,0 +1,12 @@
1
+ module QuartzTorrent
2
+
3
+ # Metadata associated with outstanding requests to the PieceManager (asynchronous IO management).
4
+ class PieceManagerRequestMetadata
5
+ def initialize(type, data)
6
+ @type = type
7
+ @data = data
8
+ end
9
+ attr_accessor :type
10
+ attr_accessor :data
11
+ end
12
+ end
@@ -0,0 +1,58 @@
1
+ module QuartzTorrent
2
+ # Class that keeps track of a rate, for example a download or upload rate.
3
+ # The class is used by calling 'update' with samples (numbers) representing
4
+ # the amount of units being measured accrued since the last call, and value
5
+ # returns the rate in units/second.
6
+ #
7
+ # This is implemented as an exponential moving average. The weight of the
8
+ # current sample is based on an exponention function of the time since the
9
+ # last sample. To reduce CPU usage, when update is called with a new sample
10
+ # the new average is not calculated immediately, but instead the samples are
11
+ # summed until 1 second has elapsed before recalculating the average.
12
+ class Rate
13
+ # Create a new Rate that measures the rate using samples.
14
+ # avgPeriod specifies the duration over which the samples are averaged.
15
+ def initialize(avgPeriod = 4.0)
16
+ reset
17
+ @avgPeriod = avgPeriod.to_f
18
+ end
19
+
20
+ # Get the current rate. If there are too few samples, 0 is returned.
21
+ def value
22
+ update 0
23
+ @value ? @value : 0.0
24
+ end
25
+
26
+ # Update the rate by passing another sample (number) representing units accrued since the last
27
+ # call to update.
28
+ def update(sample)
29
+ now = Time.new
30
+ elapsed = now - @time
31
+ @sum += sample
32
+ if elapsed > 1.0
33
+ @value = newValue elapsed, @sum/elapsed
34
+ @time = now
35
+ @sum = 0.0
36
+ end
37
+ end
38
+
39
+ # Reset the rate to empty.
40
+ def reset
41
+ @value = nil
42
+ @time = Time.new
43
+ @sum = 0.0
44
+ end
45
+
46
+ private
47
+ def newValue(elapsed, sample)
48
+ return sample if ! @value
49
+ a = alpha elapsed
50
+ a*sample + (1-a)*@value
51
+ end
52
+
53
+ # See http://en.wikipedia.org/wiki/Moving_average#Application_to_measuring_computer_performance
54
+ def alpha(elapsed)
55
+ 1 - Math.exp(-elapsed/@avgPeriod)
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,48 @@
1
+ module QuartzTorrent
2
+
3
+ # This class can be used to limit the rate at which work is done.
4
+ class RateLimit
5
+ # unitsPerSecond: Each second this many units are added to a pool. At any time
6
+ # up to the number of units in the pool may be withdrawn.
7
+ # upperLimit: The maximum size of the pool. This controls how bursty the rate is.
8
+ # For example, if the rate is 1/s and the limit is 5, then if there was no withdrawals
9
+ # for 5 seconds, then the max pool size is reached, and the next withdrawal may be 5, meaning
10
+ # 5 units could be used instantaneously. However the average for the last 5 seconds is still 1/s.
11
+ # initialValue: Initial size of the pool.
12
+ def initialize(unitsPerSecond, upperLimit, initialValue)
13
+ @unitsPerSecond = unitsPerSecond.to_f
14
+ @upperLimit = upperLimit.to_f
15
+ @initialValue = initialValue.to_f
16
+ @pool = @initialValue
17
+ @time = Time.new
18
+ end
19
+
20
+ # Return the limit in units per second
21
+ attr_reader :unitsPerSecond
22
+
23
+ # Set the limit in units per second.
24
+ def unitsPerSecond=(v)
25
+ @unitsPerSecond = v.to_f
26
+ end
27
+
28
+ # How much is in the pool.
29
+ def avail
30
+ updatePool
31
+ @pool
32
+ end
33
+
34
+ # Withdraw this much from the pool.
35
+ def withdraw(n)
36
+ updatePool
37
+ @pool -= n
38
+ end
39
+
40
+ private
41
+ def updatePool
42
+ now = Time.new
43
+ @pool = @pool + (now - @time)*@unitsPerSecond
44
+ @pool = @upperLimit if @pool > @upperLimit
45
+ @time = now
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,949 @@
1
+ require 'socket'
2
+ require 'pqueue'
3
+ require 'fiber'
4
+ require 'thread'
5
+ require 'quartz_torrent/ratelimit'
6
+ include Socket::Constants
7
+
8
+ module QuartzTorrent
9
+
10
+ # Callers must subclass this class to use the reactor. The event handler methods should be
11
+ # overridden. For data-oriented event handler methods, the functions write and read are available
12
+ # to access the current io, as well as the method currentIo. Close can be called from
13
+ # event handlers to close the current io.
14
+ class Handler
15
+ # Event handler. An IO object has been successfully initialized.
16
+ # For example, a connect call has completed
17
+ def clientInit(metainfo)
18
+ end
19
+
20
+ # Event handler. A peer has connected to the listening socket
21
+ def serverInit(metainfo, addr, port)
22
+ end
23
+
24
+ # Event handler. The current io is ready for reading
25
+ # If you will write to the same io from both this handler and the timerExpired handler,
26
+ # you must make sure to perform all writing at once in this handler. If not then
27
+ # the writes from the timer handler may be interleaved.
28
+ #
29
+ # For example if the recvData handler performs:
30
+ #
31
+ # 1. read 5 bytes
32
+ # 2. write 5 bytes
33
+ # 3. read 5 bytes
34
+ # 4. write 5 bytes
35
+ #
36
+ # and the writes in 2 and 4 are meant to be one message (say mesage 2 is the length, and message 4 is the body)
37
+ # then this can occur:
38
+ # recvData reads 5 bytes, writes 5 bytes, tries to read 5 more bytes and is blocked
39
+ # timerExpired writes 5 bytes
40
+ # recvData continues; reads the 5 bytes and writes 5 bytes.
41
+ #
42
+ # Now the timerExpired data was written interleaved.
43
+ def recvData(metainfo)
44
+ end
45
+
46
+ # Event handler. A timer has expired
47
+ def timerExpired(metainfo)
48
+ end
49
+
50
+ # Event handler. Error during read, or write. Connection errors are reported separately in connectError
51
+ def error(metainfo, details)
52
+ end
53
+
54
+ # Event handler. Error during connection, or connection timed out.
55
+ def connectError(metainfo, details)
56
+ end
57
+
58
+ # Event handler. This is called for events added using addUserEvent to the reactor.
59
+ def userEvent(event)
60
+ end
61
+
62
+ ### Methods not meant to be overridden
63
+ attr_accessor :reactor
64
+
65
+ # Schedule a timer.
66
+ def scheduleTimer(duration, metainfo = nil, recurring = true, immed = false)
67
+ @reactor.scheduleTimer(duration, metainfo, recurring, immed) if @reactor
68
+ end
69
+
70
+ # Cancel a timer scheduled with scheduleTimer.
71
+ def cancelTimer(timerInfo)
72
+ return if ! timerInfo
73
+ @reactor.cancelTimer(timerInfo) if @reactor
74
+ end
75
+
76
+ def connect(addr, port, metainfo, timeout = nil)
77
+ @reactor.connect(addr, port, metainfo, timeout) if @reactor
78
+ end
79
+
80
+ # Write data to the current io
81
+ def write(data)
82
+ @reactor.write(data) if @reactor
83
+ end
84
+
85
+ # Read len bytes from the current io
86
+ def read(len)
87
+ result = ''
88
+ result = @reactor.read(len) if @reactor
89
+ result
90
+ end
91
+
92
+ # Shutdown the reactor
93
+ def stopReactor
94
+ @reactor.stop if @reactor
95
+ end
96
+
97
+ # Check if stop has been called on the reactor
98
+ def stopped?
99
+ @stopped
100
+ end
101
+
102
+ # Close the current io
103
+ def close(io = nil)
104
+ @reactor.close(io) if @reactor
105
+ end
106
+
107
+ def currentIo
108
+ result = nil
109
+ result = @reactor.currentIo if @reactor
110
+ result
111
+ end
112
+
113
+ # Find an io by metainfo
114
+ def findIoByMetainfo(metainfo)
115
+ @reactor.findIoByMetainfo metainfo if metainfo && @reactor
116
+ end
117
+
118
+ def setMetaInfo(metainfo)
119
+ @reactor.setMetaInfo metainfo if @reactor
120
+ end
121
+
122
+ # Set the max rate at which the current IO can be read. The parameter should be a RateLimit object.
123
+ def setReadRateLimit(rateLimit)
124
+ @reactor.setReadRateLimit rateLimit if @reactor
125
+ end
126
+
127
+ # Set the max rate at which the current IO can be written to. The parameter should be a RateLimit object.
128
+ def setWriteRateLimit(rateLimit)
129
+ @reactor.setWriteRateLimit rateLimit if @reactor
130
+ end
131
+ end
132
+
133
+ class OutputBuffer
134
+ # Create a new OutputBuffer for the specified IO. The parameter seekable should be
135
+ # true or false. If true, then this output buffer will support seek
136
+ # at the cost of performance.
137
+ def initialize(io, seekable = false)
138
+ @io = io
139
+ @seekable = seekable
140
+ if seekable
141
+ @buffer = []
142
+ else
143
+ @buffer = ''
144
+ end
145
+ end
146
+
147
+ def empty?
148
+ @buffer.length == 0
149
+ end
150
+
151
+ def size
152
+ @buffer.length
153
+ end
154
+
155
+ def append(data)
156
+ if ! @seekable
157
+ @buffer << data
158
+ else
159
+ @buffer.push [@io.tell, data]
160
+ end
161
+ end
162
+
163
+ # Write the contents of the output buffer to the io. This method throws all of the exceptions that write would throw
164
+ # (EAGAIN, EWOULDBLOCK, etc)
165
+ # If max is specified and this is a non-seekable io, at most that many bytes are written.
166
+ def flush(max = nil)
167
+ if ! @seekable
168
+ toWrite = @buffer.length
169
+ toWrite = max if max && max < @buffer.length
170
+ numWritten = 0
171
+ while toWrite > 0
172
+ numWritten = @io.write_nonblock(@buffer[0,toWrite])
173
+ raise Errno::EAGAIN if numWritten == 0
174
+ @buffer = @buffer[numWritten,@buffer.length]
175
+ toWrite -= numWritten
176
+ end
177
+ else
178
+ while @buffer.length > 0
179
+ @io.seek @buffer.first[0], IO::SEEK_SET
180
+ while @buffer.first[1].length > 0
181
+ numWritten = @io.write_nonblock(@buffer.first[1])
182
+ raise Errno::EAGAIN if numWritten == 0
183
+ @buffer.first[1] = @buffer.first[1][numWritten,@buffer.first[1].length]
184
+ end
185
+ # This chunk has been fully written. Remove it
186
+ @buffer.shift
187
+ end
188
+ end
189
+ end
190
+ end
191
+
192
+ # This class provides a facade for an IO object. The read method
193
+ # on this object acts as a blocking read. Internally it reads
194
+ # nonblockingly and passes processing to other ready sockets
195
+ # if this socket would block.
196
+ class IoFacade
197
+ def initialize(ioInfo, logger = nil)
198
+ @ioInfo = ioInfo
199
+ @io = ioInfo.io
200
+ @logger = logger
201
+ end
202
+
203
+ attr_accessor :logger
204
+
205
+ # Method needed for disposeIo to work without breaking encapsulation of
206
+ # WriteOnlyIoFacade.
207
+ def removeFromIOHash(hash)
208
+ hash.delete @io
209
+ end
210
+
211
+ def read(length)
212
+ data = ''
213
+ while data.length < length
214
+ begin
215
+ toRead = length-data.length
216
+ rateLimited = false
217
+ if @ioInfo.readRateLimit
218
+ avail = @ioInfo.readRateLimit.avail.to_i
219
+ if avail < toRead
220
+ toRead = avail
221
+ rateLimited = true
222
+ end
223
+ @ioInfo.readRateLimit.withdraw toRead
224
+ end
225
+ @logger.debug "IoFacade: must read: #{length} have read: #{data.length}. Reading #{toRead} bytes now" if @logger
226
+ data << @io.read_nonblock(toRead) if toRead > 0
227
+ # If we tried to read more than we are allowed to by rate limiting, yield.
228
+ Fiber.yield if rateLimited
229
+ rescue Errno::EWOULDBLOCK
230
+ # Wait for more data.
231
+ @logger.debug "IoFacade: read would block" if @logger
232
+ Fiber.yield
233
+ rescue Errno::EAGAIN, Errno::EINTR
234
+ # Wait for more data.
235
+ @logger.debug "IoFacade: read was interrupted" if @logger
236
+ Fiber.yield
237
+ rescue
238
+ @logger.debug "IoFacade: read error: #{$!}" if @logger
239
+ # Read failure occurred
240
+ @ioInfo.lastReadError = $!
241
+ if @ioInfo.useErrorhandler
242
+ @ioInfo.state = :error
243
+ Fiber.yield
244
+ else
245
+ raise $!
246
+ end
247
+ end
248
+ end
249
+ data
250
+ end
251
+
252
+ def write(data)
253
+ # Issue: what about write, read, read on files opened for read/write? Write should happen at offset X, but reads moved to offset N. Since writes
254
+ # are buffered, write may happen after read which means write will happen at the wrong offset N. Can fix by always seeking (if needed) before writes to the
255
+ # position where the write was queued, but this should only be done for files since other fds don't support seek. This is satisfactory for files opened
256
+ # for write only since the file position will be where we expect so we won't need to seek. Same for read only.
257
+ @ioInfo.outputBuffer.append data
258
+ @logger.debug "wrote #{data.length} bytes to the output buffer of IO metainfo=#{@ioInfo.metainfo}" if @logger
259
+ data.length
260
+ end
261
+
262
+ def seek(amount, whence)
263
+ @io.seek amount, whence if @ioInfo.seekable?
264
+ end
265
+
266
+ def flush
267
+ @io.flush
268
+ end
269
+
270
+ def close
271
+ @io.close
272
+ end
273
+
274
+ def closed?
275
+ @io.closed?
276
+ end
277
+ end
278
+
279
+ # An IoFacade that doesn't allow reading.
280
+ class WriteOnlyIoFacade < IoFacade
281
+ def initialize(ioInfo, logger = nil, readError = "Reading is not allowed for this IO")
282
+ super(ioInfo, logger)
283
+ @readError = readError
284
+ end
285
+
286
+ def read(length)
287
+ raise @readError
288
+ end
289
+ end
290
+
291
+ # An IO and associated meta-information used by the Reactor.
292
+ class IOInfo
293
+ def initialize(io, metainfo, seekable = false)
294
+ @io = io
295
+ @metainfo = metainfo
296
+ @readFiber = nil
297
+ @readFiberIoFacade = IoFacade.new(self)
298
+ @lastReadError = nil
299
+ @connectTimer = nil
300
+ @seekable = seekable
301
+ @outputBuffer = OutputBuffer.new(@io, seekable)
302
+ @useErrorhandler = true
303
+ @readRateLimit = nil
304
+ @writeRateLimit = nil
305
+ end
306
+ attr_accessor :io
307
+ attr_accessor :metainfo
308
+ attr_accessor :state
309
+ attr_accessor :lastReadError
310
+ attr_accessor :connectTimeout
311
+ attr_accessor :outputBuffer
312
+ attr_accessor :readFiber
313
+ attr_accessor :readFiberIoFacade
314
+ attr_accessor :connectTimer
315
+ attr_accessor :useErrorhandler
316
+ attr_accessor :readRateLimit
317
+ attr_accessor :writeRateLimit
318
+ def seekable?
319
+ @seekable
320
+ end
321
+ end
322
+
323
+ # Class used to manage timers.
324
+ class TimerManager
325
+ class TimerInfo
326
+ def initialize(duration, recurring, metainfo)
327
+ @duration = duration
328
+ @recurring = recurring
329
+ @metainfo = metainfo
330
+ @cancelled = false
331
+ refresh
332
+ end
333
+ attr_accessor :recurring
334
+ attr_accessor :duration
335
+ attr_accessor :expiry
336
+ attr_accessor :metainfo
337
+ # Since pqueue doesn't allow removal of anything but the head
338
+ # we flag deleted items so they are deleted when they are pulled
339
+ attr_accessor :cancelled
340
+
341
+ def secondsUntilExpiry
342
+ @expiry - Time.new
343
+ end
344
+
345
+ def refresh
346
+ @expiry = Time.new + @duration
347
+ end
348
+ end
349
+
350
+ def initialize(logger = nil)
351
+ @queue = PQueue.new { |a,b| b.expiry <=> a.expiry }
352
+ @mutex = Mutex.new
353
+ @logger = logger
354
+ end
355
+
356
+ # Add a timer. Parameter 'duration' specifies the timer duration in seconds,
357
+ # 'metainfo' is caller information passed to the handler when the timer expires,
358
+ # 'recurring' should be true if the timer will repeat, or false if it will only
359
+ # expire once, and 'immed' when true specifies that the timer should expire immediately
360
+ # (and again each duration if recurring) while false specifies that the timer will only
361
+ # expire the first time after it's duration elapses.
362
+ def add(duration, metainfo = nil, recurring = true, immed = false)
363
+ raise "TimerManager.add: Timer duration may not be nil" if duration.nil?
364
+ info = TimerInfo.new(duration, recurring, metainfo)
365
+ info.expiry = Time.new if immed
366
+ @mutex.synchronize{ @queue.push info }
367
+ info
368
+ end
369
+
370
+ # Cancel a timer.
371
+ def cancel(timerInfo)
372
+ timerInfo.cancelled = true
373
+ end
374
+
375
+ # Return the next timer event from the queue, but don't remove it from the queue.
376
+ def peek
377
+ clearCancelled
378
+ @queue.top
379
+ end
380
+
381
+ # Remove the next timer event from the queue and return it as a TimerHandler::TimerInfo object.
382
+ def next
383
+ clearCancelled
384
+ result = nil
385
+ @mutex.synchronize{ result = @queue.pop }
386
+ if result && result.recurring
387
+ result.refresh
388
+ @mutex.synchronize{ @queue.push result }
389
+ end
390
+ result
391
+ end
392
+
393
+ private
394
+
395
+ def clearCancelled
396
+ while @queue.top && @queue.top.cancelled
397
+ info = @queue.pop
398
+ end
399
+ end
400
+ end
401
+
402
+ # This class implements the Reactor pattern. The Reactor listens for activity on IO objects and calls methods on
403
+ # an associated Handler object when activity is detected. Callers can use listen, connect or open to register IO
404
+ # objects with the reactor.
405
+ #
406
+ # This Reactor is implemented using Fibers in such a way that when activity is defected on an IO, the handler
407
+ # can perform reads of N bytes without blocking and without needing to buffer. For example, the handler
408
+ # may call:
409
+ #
410
+ # msg = io.read(300)
411
+ #
412
+ # when it knows it must read 300 bytes. If only 100 are available, the handler is cooperatively preempted and
413
+ # later resumed when more bytes are available, so that the read seems atomic while also not blocking.
414
+ class Reactor
415
+ class InternalTimerInfo
416
+ def initialize(type, data)
417
+ @type = type
418
+ @data = data
419
+ end
420
+ attr_accessor :type
421
+ attr_accessor :data
422
+ end
423
+
424
+ # Create a new reactor that uses the passed hander.
425
+ def initialize(handler, logger = nil)
426
+ raise "Reactor.new called with nil handler. Handler can't be nil" if handler.nil?
427
+
428
+ @stopped = false
429
+ @handler = handler
430
+ @handler.reactor = self
431
+
432
+ # Hash of IOInfo objects, keyed by io.
433
+ @ioInfo = {}
434
+ @timerManager = TimerManager.new(logger)
435
+ @currentIoInfo = nil
436
+ @logger = logger
437
+ @listenBacklog = 10
438
+ @eventRead, @eventWrite = IO.pipe
439
+ @currentEventPipeChars = 0
440
+ @currentHandlerCallback = nil
441
+ @userEvents = []
442
+ end
443
+
444
+ attr_accessor :listenBacklog
445
+
446
+ # Returns true if the reactor is stopped
447
+ def stopped?
448
+ @stopped
449
+ end
450
+
451
+ # Create a TCP connection to the specified host
452
+ def connect(addr, port, metainfo, timeout = nil)
453
+ ioInfo = startConnection(port, addr, metainfo)
454
+ @ioInfo[ioInfo.io] = ioInfo
455
+ if timeout && ioInfo.state == :connecting
456
+ ioInfo.connectTimeout = timeout
457
+ ioInfo.connectTimer = scheduleTimer(timeout, InternalTimerInfo.new(:connect_timeout, ioInfo), false)
458
+ end
459
+ end
460
+
461
+ # Create a TCP server that listens for connections on the specified
462
+ # port
463
+ def listen(addr, port, metainfo)
464
+ listener = Socket.new( AF_INET, SOCK_STREAM, 0 )
465
+ sockaddr = Socket.pack_sockaddr_in( port, "0.0.0.0" )
466
+ listener.setsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR, true)
467
+ listener.bind( sockaddr )
468
+ @logger.debug "listening on port #{port}" if @logger
469
+ listener.listen( @listenBacklog )
470
+
471
+ info = IOInfo.new(listener, metainfo)
472
+ info.readFiberIoFacade.logger = @logger if @logger
473
+ info.state = :listening
474
+ @ioInfo[info.io] = info
475
+ end
476
+
477
+ # Open the specified file for the specified mode.
478
+ def open(path, mode, metainfo, useErrorhandler = true)
479
+ file = File.open(path, mode)
480
+
481
+ info = IOInfo.new(file, metainfo, true)
482
+ info.useErrorhandler = useErrorhandler
483
+ info.readFiberIoFacade.logger = @logger if @logger
484
+ info.state = :connected
485
+ @ioInfo[info.io] = info
486
+ end
487
+
488
+ # Add a generic event. This event will be processed the next pass through the
489
+ # event loop
490
+ def addUserEvent(event)
491
+ @userEvents.push event
492
+ end
493
+
494
+ # Run event loop
495
+ def start
496
+ while true
497
+ begin
498
+ break if eventLoopBody == :halt
499
+ rescue
500
+ @logger.error "Unexpected exception in reactor event loop: #{$!}" if @logger
501
+ @logger.error $!.backtrace.join "\n" if @logger
502
+ end
503
+ end
504
+
505
+ @logger.info "Reactor shutting down" if @logger
506
+
507
+ # Event loop finished
508
+ @ioInfo.each do |k,v|
509
+ k.close
510
+ end
511
+
512
+ end
513
+
514
+ # Stop the event loop.
515
+ def stop
516
+ @stopped = true
517
+ return if @currentEventPipeChars > 0
518
+ @eventWrite.write 'x'
519
+ @currentEventPipeChars += 1
520
+ @eventWrite.flush
521
+ end
522
+
523
+ # Schedule a timer. Parameter 'duration' specifies the timer duration in seconds,
524
+ # 'metainfo' is caller information passed to the handler when the timer expires,
525
+ # 'recurring' should be true if the timer will repeat, or false if it will only
526
+ # expire once, and 'immed' when true specifies that the timer should expire immediately
527
+ # (and again each duration if recurring) while false specifies that the timer will only
528
+ # expire the first time after it's duration elapses.
529
+ def scheduleTimer(duration, metainfo = nil, recurring = true, immed = false)
530
+ timerInfo = @timerManager.add(duration, metainfo, recurring, immed)
531
+ # This timer may expire sooner than the current sleep we are doing in select(). To make
532
+ # sure we will write to the event pipe to break out of select().
533
+ if @currentEventPipeChars == 0
534
+ @eventWrite.write 'x'
535
+ @currentEventPipeChars += 1
536
+ @eventWrite.flush
537
+ end
538
+ timerInfo
539
+ end
540
+
541
+ # Meant to be called from the handler. Cancel the timer scheduled with scheduleTimer
542
+ def cancelTimer(timerInfo)
543
+ @timerManager.cancel timerInfo
544
+ end
545
+
546
+ # Meant to be called from the handler. Adds the specified data to the outgoing queue for the current io
547
+ def write(data)
548
+ if @currentIoInfo
549
+ # This is meant to be called from inside a fiber. Should add a check to confirm that here.
550
+ @currentIoInfo.readFiberIoFacade.write(data)
551
+ else
552
+ raise "Reactor.write called with no current io. Was it called from a timer handler?"
553
+ end
554
+ end
555
+
556
+ # Meant to be called from the handler. Read 'len' bytes from the current IO.
557
+ def read(len)
558
+ if @currentIoInfo
559
+ # This is meant to be called from inside a fiber. Should add a check to confirm that here.
560
+ @currentIoInfo.readFiberIoFacade.read(len)
561
+ else
562
+ raise "Reactor.read called with no current io. Was it called from a timer handler?"
563
+ end
564
+ end
565
+
566
+ # Meant to be called from the handler. Closes the passed io, or if it's nil, closes the current io
567
+ def close(io = nil)
568
+ if ! io
569
+ disposeIo @currentIoInfo if @currentIoInfo
570
+ else
571
+ disposeIo io
572
+ end
573
+ end
574
+
575
+ # Meant to be called from the handler. Returns the current io
576
+ def currentIo
577
+ @currentIoInfo.readFiberIoFacade
578
+ end
579
+
580
+ # Meant to be called from the handler. Sets the meta info for the current io
581
+ def setMetaInfo(metainfo)
582
+ @currentIoInfo.metainfo = metainfo
583
+ end
584
+
585
+ # Meant to be called from the handler. Sets the max rate at which the current io can read.
586
+ def setReadRateLimit(rateLimit)
587
+ @currentIoInfo.readRateLimit = rateLimit
588
+ end
589
+
590
+ # Meant to be called from the handler. Sets the max rate at which the current io can be written to.
591
+ def setWriteRateLimit(rateLimit)
592
+ @currentIoInfo.writeRateLimit = rateLimit
593
+ end
594
+
595
+ # Meant to be called from the handler. Find an IO by metainfo. The == operator is used to
596
+ # match the metainfo.
597
+ def findIoByMetainfo(metainfo)
598
+ @ioInfo.each_value do |info|
599
+ if info.metainfo == metainfo
600
+ io = info.readFiberIoFacade
601
+ # Don't allow read calls from timer handlers. This is to prevent a complex situation.
602
+ # See the processTimer call in eventLoopBody for more info
603
+ io = WriteOnlyIoFacade.new(info) if @currentHandlerCallback == :timer
604
+ return io
605
+ end
606
+ end
607
+ nil
608
+ end
609
+
610
+ private
611
+
612
+ # Returns :continue or :halt to the caller, specifying whether to continue the event loop or halt.
613
+ def eventLoopBody
614
+ # 1. Check timers
615
+ timer, selectTimeout = processTimers
616
+
617
+ readset = []
618
+ writeset = []
619
+ outputBufferNotEmptyCount = 0
620
+ ioToRemove = []
621
+ @ioInfo.each do |k,v|
622
+ if k.closed?
623
+ ioToRemove.push k
624
+ next
625
+ end
626
+ readset.push k if v.state != :connecting && ! @stopped && (v.readRateLimit.nil? || v.readRateLimit.avail >= 1.0)
627
+ @logger.debug "eventloop: IO metainfo=#{v.metainfo} added to read set" if @logger
628
+ writeset.push k if (!v.outputBuffer.empty? || v.state == :connecting) && v.state != :listening && (v.writeRateLimit.nil? || v.writeRateLimit.avail >= 1.0)
629
+ @logger.debug "eventloop: IO metainfo=#{v.metainfo} added to write set" if @logger
630
+ outputBufferNotEmptyCount += 1 if !v.outputBuffer.empty?
631
+ end
632
+ readset.push @eventRead
633
+
634
+ # Only exit the event loop once we've written all pending data.
635
+ return :halt if @stopped && outputBufferNotEmptyCount == 0
636
+
637
+ # 2. Check user events
638
+ @userEvents.each{ |event| @handler.userEvent event } if ! @stopped
639
+
640
+ # 3. Call Select. Ignore exception set: apparently this is for OOB data, or terminal things.
641
+ selectResult = nil
642
+ while true
643
+ begin
644
+ if readset.length > 1024 || writeset.length > 1024
645
+ @logger.error "Too many file descriptors to pass to select! Trimming them. Some fds may starve!" if @logger
646
+ readset = readset.first(1024)
647
+ writeset = writeset.first(1024)
648
+ end
649
+ @logger.debug "eventloop: Calling select" if @logger
650
+ selectResult = IO.select(readset, writeset, nil, selectTimeout)
651
+ @logger.debug "eventloop: select done. result: #{selectResult.inspect}" if @logger
652
+ break
653
+ rescue
654
+ # Exception occurred. Probably EINTR.
655
+ @logger.warn "Select raised exception; will retry. Reason: #{$!}" if @logger
656
+ @logger.warn "IOs at time of failure:"
657
+ @logger.warn "Readset:"
658
+ readset.each do |io|
659
+ ioInfo = @ioInfo[io]
660
+ puts " #{ioInfo.metainfo}: #{(io.closed?) ? "closed" : "not closed"}"
661
+ end
662
+ @logger.warn "Writeset:"
663
+ writeset.each do |io|
664
+ ioInfo = @ioInfo[io]
665
+ puts " #{ioInfo.metainfo}: #{(io.closed?) ? "closed" : "not closed"}"
666
+ end
667
+
668
+ end
669
+ end
670
+
671
+ if selectResult.nil?
672
+ # Process timer
673
+ @logger.debug "eventloop: processing timer" if @logger
674
+ # Calling processTimer in withReadFiber here is not correct. What if at this point the fiber was already paused in a read, and we
675
+ # want to process a timer? In that case we will resume the read and it will possibly finish, but we'll never
676
+ # call the timer handler. For this reason we must prevent read calls in timerHandlers.
677
+ processTimer(timer) if timer
678
+ else
679
+ readable, writeable = selectResult
680
+
681
+ # If we are stopped, then ignore reads; we only care about completing our writes that were pending when we were stopped.
682
+ readable = [] if @stopped
683
+
684
+ readable.each do |io|
685
+ # This could be the eventRead pipe, which we use to signal shutdown or to reloop.
686
+ if io == @eventRead
687
+ @logger.debug "Event received on the eventRead pipe." if @logger
688
+ @eventRead.read 1
689
+ @currentEventPipeChars -= 1
690
+ next
691
+ end
692
+
693
+ @currentIoInfo = @ioInfo[io]
694
+ if @currentIoInfo.state == :listening
695
+ @logger.debug "eventloop: calling handleAccept for IO metainfo=#{@currentIoInfo.metainfo}" if @logger
696
+ # Replace the currentIoInfo with the accepted socket
697
+ listenerMetainfo = @currentIoInfo.metainfo
698
+ @currentIoInfo, addr, port = handleAccept(@currentIoInfo)
699
+ withReadFiber(@currentIoInfo) do
700
+ @currentHandlerCallback = :serverinit
701
+ @handler.serverInit(listenerMetainfo, addr, port)
702
+ end
703
+ else
704
+ @logger.debug "eventloop: calling handleRead for IO metainfo=#{@currentIoInfo.metainfo}" if @logger
705
+ #handleRead(@currentIoInfo)
706
+ withReadFiber(@currentIoInfo) do
707
+ @currentHandlerCallback = :recv_data
708
+ @handler.recvData @currentIoInfo.metainfo
709
+ end
710
+ end
711
+ end
712
+
713
+ writeable.each do |io|
714
+ @currentIoInfo = @ioInfo[io]
715
+ # Check if there is still ioInfo for this io. This can happen if this io was also ready for read, and
716
+ # the read had an error (for example connection failure) and the ioinfo was removed when handling the error.
717
+ next if ! @currentIoInfo
718
+ if @currentIoInfo.state == :connecting
719
+ @logger.debug "eventloop: calling finishConnection for IO metainfo=#{@currentIoInfo.metainfo}" if @logger
720
+ finishConnection(@currentIoInfo)
721
+ else
722
+ @logger.debug "eventloop: calling writeOutputBuffer for IO metainfo=#{@currentIoInfo.metainfo}" if @logger
723
+ writeOutputBuffer(@currentIoInfo)
724
+ end
725
+ end
726
+ end
727
+
728
+ ioToRemove.each do |io|
729
+ ioInfo = @ioInfo.delete io
730
+ @logger.warn "Detected an IO that was closed but still in the list of selectable IO. Metadata = #{ioInfo.metainfo}"
731
+ end
732
+
733
+ :continue
734
+ end
735
+
736
+ def processTimers
737
+ selectTimeout = nil
738
+ timer = nil
739
+ while true && ! @stopped
740
+ timer = @timerManager.peek
741
+ break if ! timer
742
+ secondsUntilExpiry = timer.secondsUntilExpiry
743
+ if secondsUntilExpiry > 0
744
+ selectTimeout = secondsUntilExpiry
745
+ break
746
+ end
747
+ # Process timer now; it's firing time has already passed.
748
+ processTimer(timer)
749
+ end
750
+ [timer, selectTimeout]
751
+ end
752
+
753
+ def startConnection(port, addr, metainfo)
754
+ socket = Socket.new(AF_INET, SOCK_STREAM, 0)
755
+ addr = Socket.pack_sockaddr_in(port, addr)
756
+
757
+ info = IOInfo.new(socket, metainfo)
758
+ info.readFiberIoFacade.logger = @logger if @logger
759
+
760
+ begin
761
+ socket.connect_nonblock(addr)
762
+ info.state = :connected
763
+ @currentHandlerCallback = :client_init
764
+ @handler.clientInit(ioInfo.metainfo)
765
+ rescue Errno::EINPROGRESS
766
+ # Connection is ongoing.
767
+ info.state = :connecting
768
+ end
769
+
770
+ info
771
+ end
772
+
773
+ def finishConnection(ioInfo)
774
+ # Socket was connecting and is now writable. Check if there was a connection failure
775
+ # This uses the getpeername method. See http://cr.yp.to/docs/connect.html
776
+ begin
777
+ ioInfo.io.getpeername
778
+ ioInfo.state = :connected
779
+ if ioInfo.connectTimer
780
+ @logger.debug "cancelling connect timer for IO metainfo=#{@currentIoInfo.metainfo}" if @logger
781
+ @timerManager.cancel ioInfo.connectTimer
782
+ end
783
+ @currentHandlerCallback = :client_init
784
+ @handler.clientInit(ioInfo.metainfo)
785
+ rescue
786
+ # Connection failed.
787
+ @logger.debug "connection failed for IO metainfo=#{@currentIoInfo.metainfo}: #{$!}" if @logger
788
+ @currentHandlerCallback = :connect_error
789
+ @handler.connectError(ioInfo.metainfo, $!)
790
+ disposeIo(ioInfo)
791
+ end
792
+ end
793
+
794
+ def processTimer(timer)
795
+ begin
796
+ # Check for internal timers first.
797
+ if timer.metainfo && timer.metainfo.is_a?(InternalTimerInfo)
798
+ if timer.metainfo.type == :connect_timeout
799
+ @currentHandlerCallback = :error
800
+ @handler.error(timer.metainfo.data.metainfo, "Connection timed out")
801
+ disposeIo(timer.metainfo.data)
802
+ end
803
+ else
804
+ @currentHandlerCallback = :timer
805
+ @handler.timerExpired(timer.metainfo)
806
+ end
807
+ rescue
808
+ @logger.error "Exception in timer event handler: #{$!}" if @logger
809
+ @logger.error $!.backtrace.join "\n" if @logger
810
+ end
811
+ @timerManager.next
812
+ end
813
+
814
+ def disposeIo(io)
815
+ # Inner function in disposeIo.
816
+ def closeIo(io)
817
+ begin
818
+ io.close if !io.closed?
819
+ rescue
820
+ @logger.warn "Closing IO failed with exception #{$!}"
821
+ @logger.debug $!.backtrace.join("\n")
822
+ end
823
+ end
824
+
825
+ if io.is_a?(IOInfo)
826
+ # Flush any output
827
+ begin
828
+ @logger.debug "disposeIo: flushing data" if @logger
829
+ io.outputBuffer.flush
830
+ rescue
831
+ end
832
+
833
+ closeIo(io.io)
834
+ @ioInfo.delete io.io
835
+ elsif io.is_a?(IoFacade)
836
+ closeIo(io)
837
+ io.removeFromIOHash(@ioInfo)
838
+ else
839
+ closeIo(io)
840
+ @ioInfo.delete io
841
+ end
842
+ end
843
+
844
+ # Given the ioInfo for a listening socket, call accept and return the new ioInfo for the
845
+ # client's socket
846
+ def handleAccept(ioInfo)
847
+ socket, clientAddr = ioInfo.io.accept
848
+ info = IOInfo.new(socket, ioInfo.metainfo)
849
+ info.readFiberIoFacade.logger = @logger if @logger
850
+ info.state = :connected
851
+ @ioInfo[info.io] = info
852
+ if @logger
853
+ port, addr = Socket.unpack_sockaddr_in(clientAddr)
854
+ @logger.debug "Accepted connection from #{addr}:#{port}" if @logger
855
+ end
856
+
857
+ [info, addr, port]
858
+ end
859
+
860
+ def handleRead(ioInfo)
861
+ if ioInfo.readFiber.nil? || ! ioInfo.readFiber.alive?
862
+ ioInfo.readFiber = Fiber.new do |ioInfo|
863
+ @currentHandlerCallback = :recv_data
864
+ @handler.recvData ioInfo.metainfo
865
+ end
866
+ end
867
+
868
+ # Allow handler to read some data.
869
+ # This call will return either if:
870
+ # 1. the handler needs more data but it isn't available yet,
871
+ # 2. if it's read all the data it wanted to read for the current message it's building
872
+ # 3. if a read error occurred.
873
+ #
874
+ # In case 2 the latter case the fiber will be dead. In cases 1 and 2, we should select on the socket
875
+ # until data is ready. For case 3, the state of the ioInfo is set to error and the io should be
876
+ # removed.
877
+ ioInfo.readFiber.resume(ioInfo)
878
+ if ioInfo.state == :error
879
+ @currentHandlerCallback = :error
880
+ @handler.error(ioInfo.metainfo, ioInfo.lastReadError)
881
+ disposeIo(ioInfo)
882
+ end
883
+ end
884
+
885
+ # Call the passed block in the context of the read Fiber. Basically the
886
+ # passed block is run as normal, but if the block performs a read from an io and that
887
+ # read would block, the block is paused, and withReadFiber returns. The next time withReadFiber
888
+ # is called the block will be resumed at the point of the read.
889
+ def withReadFiber(ioInfo)
890
+ if ioInfo.readFiber.nil? || ! ioInfo.readFiber.alive?
891
+ ioInfo.readFiber = Fiber.new do |ioInfo|
892
+ yield ioInfo.readFiberIoFacade
893
+ end
894
+ end
895
+
896
+ # Allow handler to read some data.
897
+ # This call will return either if:
898
+ # 1. the handler needs more data but it isn't available yet,
899
+ # 2. if it's read all the data it wanted to read for the current message it's building
900
+ # 3. if a read error occurred.
901
+ #
902
+ # In case 2 the latter case the fiber will be dead. In cases 1 and 2, we should select on the socket
903
+ # until data is ready. For case 3, the state of the ioInfo is set to error and the io should be
904
+ # removed.
905
+ ioInfo.readFiber.resume(ioInfo)
906
+ if ioInfo.state == :error
907
+ @currentHandlerCallback = :error
908
+ @handler.error(ioInfo.metainfo, ioInfo.lastReadError)
909
+ disposeIo(ioInfo)
910
+ end
911
+
912
+ end
913
+
914
+ def writeOutputBuffer(ioInfo)
915
+ begin
916
+ @logger.debug "writeOutputBuffer: flushing data" if @logger
917
+ if !ioInfo.writeRateLimit
918
+ ioInfo.outputBuffer.flush
919
+ else
920
+ avail = ioInfo.writeRateLimit.avail
921
+ if avail < ioInfo.outputBuffer.size
922
+ if avail > 0
923
+ ioInfo.writeRateLimit.withdraw avail
924
+ ioInfo.outputBuffer.flush avail
925
+ end
926
+ else
927
+ ioInfo.writeRateLimit.withdraw ioInfo.outputBuffer.size
928
+ ioInfo.outputBuffer.flush
929
+ end
930
+ end
931
+ rescue Errno::EWOULDBLOCK, Errno::EAGAIN, Errno::EINTR
932
+ # Need to wait to write more.
933
+ @logger.debug "writeOutputBuffer: write failed with retryable exception #{$!}" if @logger
934
+ rescue
935
+ # Write failure occurred
936
+ @logger.debug "writeOutputBuffer: write failed with unexpected exception #{$!}" if @logger
937
+ if ioInfo.useErrorhandler
938
+ @currentHandlerCallback = :error
939
+ @handler.error(ioInfo.metainfo, "Write error: #{$!}")
940
+ else
941
+ raise $!
942
+ end
943
+ end
944
+
945
+ end
946
+
947
+ end
948
+
949
+ end