distribustream 0.5.0 → 0.5.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.
data/CHANGES CHANGED
@@ -1,3 +1,11 @@
1
+ Version 0.5.1
2
+
3
+ * Add #score method to peers to compare ideal transfer candidates
4
+
5
+ * Convert PDTP::Server::ChunkInfo to store chunk data in RangeMaps
6
+
7
+ * Add daemonization support
8
+
1
9
  Version 0.5.0
2
10
 
3
11
  * Factor traffic routing code into PDTP::Server::TransferManager
@@ -21,6 +21,7 @@
21
21
  # See http://distribustream.org/
22
22
  #++
23
23
 
24
+ require 'rubygems'
24
25
  require 'optparse'
25
26
  require File.dirname(__FILE__) + '/../lib/pdtp/server'
26
27
 
@@ -28,6 +29,7 @@ program_name = File.basename($0)
28
29
  config_filename = nil
29
30
  listen_addr = nil
30
31
  listen_port = nil
32
+ daemonize = true
31
33
 
32
34
  OptionParser.new do |opts|
33
35
  opts.banner = "Usage: #{program_name} [options]"
@@ -40,11 +42,14 @@ OptionParser.new do |opts|
40
42
  opts.on("--port PORT", "Listen on the specified port number.") do |p|
41
43
  listen_port = p
42
44
  end
45
+ opts.on("--foreground", "Run in the foreground instead of daemonizing.") do
46
+ daemonize = false
47
+ end
43
48
  opts.on("--help", "Prints this usage info.") do
44
49
  puts opts
45
50
  exit
46
51
  end
47
- opts.on("--version", "Print version information.") do
52
+ opts.on("--version", "Print version information.") do
48
53
  puts "#{program_name} #{PDTP::VERSION} - DistribuStream server application"
49
54
  puts "Copyright (C) 2006-07 ClickCaster, Inc. (info@clickcaster.com)"
50
55
  exit
@@ -57,7 +62,9 @@ config = {
57
62
  :listen_port => 8000, #client listen port
58
63
  :file_root => '.',
59
64
  :chunk_size => 5000,
60
- :quiet => true
65
+ :quiet => true,
66
+ :logfile => nil,
67
+ :daemonize => true
61
68
  }
62
69
 
63
70
  if config_filename.nil?
@@ -96,4 +103,10 @@ logger.level = Logger::INFO if config[:quiet]
96
103
  server.enable_logging logger
97
104
  server.enable_status_page
98
105
  server.enable_file_service config[:file_root], :chunk_size => config[:chunk_size]
99
- server.run
106
+
107
+ if daemonize and config[:daemonize]
108
+ require 'daemons/daemonize'
109
+ Daemonize.daemonize config[:logfile], program_name
110
+ end
111
+
112
+ server.run
@@ -15,4 +15,10 @@
15
15
  :file_root: /Users/tony/dstest/files
16
16
 
17
17
  # Size of segments to be distributed (in bytes)
18
- :chunk_size: 500000
18
+ :chunk_size: 512000
19
+
20
+ # Path to logfile
21
+ :logfile: dstream.log
22
+
23
+ # Daemonize or run in foreground
24
+ :daemonize: true
@@ -16,3 +16,9 @@
16
16
 
17
17
  # Size of segments to be distributed (in bytes)
18
18
  :chunk_size: 100000
19
+
20
+ # Path to logfile
21
+ :logfile: dstream.log
22
+
23
+ # Daemonize or run in foreground
24
+ :daemonize: true
@@ -16,3 +16,9 @@
16
16
 
17
17
  # Size of segments to be distributed (in bytes)
18
18
  :chunk_size: 100000
19
+
20
+ # Path to logfile
21
+ :logfile: dstream.log
22
+
23
+ # Daemonize or run in foreground
24
+ :daemonize: true
@@ -2,10 +2,10 @@ require 'rubygems'
2
2
 
3
3
  GEMSPEC = Gem::Specification.new do |s|
4
4
  s.name = "distribustream"
5
- s.version = "0.5.0"
5
+ s.version = "0.5.1"
6
6
  s.authors = ["Tony Arcieri", "Ashvin Mysore", "Galen Pahlke", "James Sanders", "Tom Stapleton"]
7
7
  s.email = "tony@clickcaster.com"
8
- s.date = "2008-11-27"
8
+ s.date = "2008-12-04"
9
9
  s.summary = "DistribuStream is a fully open peercasting system allowing on-demand or live streaming media to be delivered at a fraction of the normal cost"
10
10
  s.platform = Gem::Platform::RUBY
11
11
 
@@ -17,6 +17,7 @@ GEMSPEC = Gem::Specification.new do |s|
17
17
  s.add_dependency("eventmachine", ">= 0.9.0")
18
18
  s.add_dependency("mongrel", ">= 1.0.2")
19
19
  s.add_dependency("json", ">= 1.1.0")
20
+ s.add_dependency("activesupport", ">= 1.4.0")
20
21
 
21
22
  # RubyForge info
22
23
  s.homepage = "http://distribustream.org"
@@ -20,9 +20,12 @@
20
20
  # See http://distribustream.org/
21
21
  #++
22
22
 
23
+ require 'rubygems'
24
+ require 'active_support'
25
+
23
26
  # Namespace for all PDTP components
24
27
  module PDTP
25
- PDTP::VERSION = '0.5.0' unless defined? PDTP::VERSION
28
+ PDTP::VERSION = '0.5.1' unless defined? PDTP::VERSION
26
29
  def self.version() VERSION end
27
30
 
28
31
  PDTP::DEFAULT_PORT = 6086 unless defined? PDTP::DEFAULT_PORT
@@ -50,10 +50,10 @@ module PDTP
50
50
  new_data = data[0..(toread - 1)]
51
51
 
52
52
  @data << new_data
53
- @read += new_data.length
53
+ @read += new_data.size
54
54
  return nil unless @read == @size
55
55
 
56
- @length = @data.unpack(@size == 2 ? 'n' : 'N').first
56
+ @payload_length = @data.unpack(@size == 2 ? 'n' : 'N').first
57
57
  result = data[toread..data.length]
58
58
 
59
59
  return nil if result.nil? or result.empty?
@@ -61,13 +61,13 @@ module PDTP
61
61
  end
62
62
 
63
63
  # Length of the payload extracted from the prefix
64
- def length
65
- raise RuntimeError, 'length called before prefix extracted' unless read?
66
- @length
64
+ def payload_length
65
+ raise RuntimeError, 'payload_length called before prefix extracted' unless read?
66
+ @payload_length
67
67
  end
68
68
 
69
69
  def reset!
70
- @length = nil
70
+ @payload_length = nil
71
71
  @read = 0
72
72
  @data = ''
73
73
  end
@@ -97,13 +97,13 @@ module PDTP
97
97
  @buffer << data
98
98
 
99
99
  # Don't do anything until we receive the specified amount of data
100
- return unless @buffer.length >= @prefix.length
100
+ return unless @buffer.size >= @prefix.payload_length
101
101
 
102
102
  # Extract the specified amount of data and process it
103
- data = @buffer[0..(@prefix.length - 1)]
103
+ data = @buffer[0..(@prefix.payload_length - 1)]
104
104
 
105
105
  # Store any remaining data
106
- remainder = @buffer[@prefix.length..@buffer.length]
106
+ remainder = @buffer[@prefix.payload_length..@buffer.length]
107
107
 
108
108
  # Invoke receive_packet and allow the user to process the data
109
109
  receive_packet data
@@ -127,4 +127,4 @@ module PDTP
127
127
  send_data [data.size].pack(@prefix.size == 2 ? 'n' : 'N') << data
128
128
  end
129
129
  end
130
- end
130
+ end
@@ -0,0 +1,138 @@
1
+ #--
2
+ # Copyright (C) 2006-07 ClickCaster, Inc. (info@clickcaster.com)
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+ #
17
+ # This source file is distributed as part of the
18
+ # DistribuStream file transfer system.
19
+ #
20
+ # See http://distribustream.org/
21
+ #++
22
+
23
+ module PDTP
24
+ # Structure which maps non-overlapping ranges to objects
25
+ class RangeMap
26
+ include Enumerable
27
+
28
+ def initialize
29
+ @ranges = []
30
+ end
31
+
32
+ # Insert a range into the RangeMap
33
+ def []=(range, obj)
34
+ range = range..range if range.is_a?(Integer) or range.is_a?(Float)
35
+ raise ArgumentError, 'key must be a number or range' unless range.is_a?(Range)
36
+
37
+ index = binary_search range.begin
38
+
39
+ # Correct total overlap
40
+ [index, index + 1].each do |i|
41
+ while @ranges[i] and @ranges[i][0].begin >= range.begin and @ranges[i][0].end <= range.end
42
+ @ranges.delete_at i
43
+ end
44
+ end
45
+
46
+ # Correct overlap with leftmost member
47
+ if @ranges[index] and @ranges[index][0].begin <= range.begin
48
+ if range.end < @ranges[index][0].end
49
+ @ranges[index][0] = (range.end + 1)..(@ranges[index][0].end)
50
+ else
51
+ @ranges[index][0] = (@ranges[index][0].begin)..(range.begin - 1)
52
+ index += 1
53
+ end
54
+ end
55
+
56
+ # Correct overlap with rightmost member
57
+ if @ranges[index] and @ranges[index][0].begin <= range.end
58
+ @ranges[index][0] = (range.end + 1)..(@ranges[index][0].end)
59
+ end
60
+
61
+ @ranges.insert(index, [range, obj])
62
+ obj
63
+ end
64
+
65
+ # Find a value in the RangeMap
66
+ def [](value)
67
+ return nil if empty?
68
+ range, obj = @ranges[binary_search(value)]
69
+ return nil if range.nil?
70
+
71
+ case value
72
+ when Integer, Float
73
+ return nil unless range.include?(value)
74
+ else raise ArgumentError, 'key must be a number'
75
+ end
76
+
77
+ obj
78
+ end
79
+
80
+ # Iterate over all ranges and objects
81
+ def each(&block)
82
+ @ranges.each(&block)
83
+ end
84
+
85
+ # Number of entries in the RangeMap
86
+ def size
87
+ @ranges.size
88
+ end
89
+
90
+ # First range
91
+ def first
92
+ @ranges.first[0]
93
+ end
94
+
95
+ # Last range
96
+ def last
97
+ @ranges.last[0]
98
+ end
99
+
100
+ # Is the RangeMap empty?
101
+ def empty?
102
+ @ranges.empty?
103
+ end
104
+
105
+ # Remove all elements from the RangeMap
106
+ def clear
107
+ @ranges.clear
108
+ self
109
+ end
110
+
111
+ # Inspect the RangeMap
112
+ def inspect
113
+ "#<PDTP::RangeMap {#{@ranges.map { |r| "#{r.first}=>#{r.last.inspect}" }.join(", ")}}>"
114
+ end
115
+
116
+ #########
117
+ protected
118
+ #########
119
+
120
+ # Find the index of the range nearest the given value
121
+ def binary_search(value, a = 0, b = @ranges.size)
122
+ pivot = (a + b) / 2
123
+ range, _ = @ranges[pivot]
124
+
125
+ return b if range.nil?
126
+
127
+ if value < range.begin
128
+ return a if a == pivot
129
+ binary_search(value, a, pivot)
130
+ elsif value > range.end
131
+ return b if b == pivot
132
+ binary_search(value, pivot + 1, b)
133
+ else
134
+ pivot
135
+ end
136
+ end
137
+ end
138
+ end
@@ -20,6 +20,8 @@
20
20
  # See http://distribustream.org/
21
21
  #++
22
22
 
23
+ require File.dirname(__FILE__) + '/../common'
24
+
23
25
  module PDTP
24
26
  class Server
25
27
  # BandwidthEstimator logs the durations of chunk transfers over time,
@@ -76,7 +78,7 @@ module PDTP
76
78
  return 0 if pruned.size.zero?
77
79
 
78
80
  # Calculate the mean of the pruned estimates
79
- @estimate = pruned.inject(0) { |a, v| a + v } / pruned.size
81
+ @estimate = pruned.sum / pruned.size
80
82
  end
81
83
 
82
84
  #########
@@ -95,12 +97,12 @@ module PDTP
95
97
  end
96
98
 
97
99
  # Sum the overlapping transfer rates and store them as estimates
98
- @estimates ||= overlapping_transfers.map { |t| t.inject(0) { |a,v| a + v } }
100
+ @estimates ||= overlapping_transfers.sum
99
101
  end
100
102
 
101
103
  # Compute the mean of the bandwidth estimates
102
104
  def estimates_mean
103
- @mean ||= estimates.inject(0) { |a, v| a + v } / estimates.size
105
+ @mean ||= estimates.sum / estimates.size
104
106
  end
105
107
 
106
108
  # Compute the variance of the bandwidth estimates
@@ -20,77 +20,84 @@
20
20
  # See http://distribustream.org/
21
21
  #++
22
22
 
23
+ require File.dirname(__FILE__) + '/../common/range_map'
24
+
23
25
  module PDTP
24
26
  class Server
25
27
  # Stores information about the chunks requested or provided by a client
26
- class ChunkInfo
28
+ # FIXME this entire class is in need of a rewrite
29
+ class ChunkInfo
27
30
  def initialize
28
31
  @files = {}
29
32
  end
30
33
 
31
- #each chunk can either be provided, requested, transfer, or none
32
- #FIXME some metaprogramming is probably in order here
33
- def provide(filename,range); set(filename, range, :provided); end
34
- def unprovide(filename,range); set(filename,range, :none); end
35
- def request(filename,range); set(filename,range, :requested); end
36
- def unrequest(filename,range); set(filename,range, :none); end
37
- def transfer(filename,range); set(filename,range, :transfer); end
34
+ # Set the state for a given chunk range
35
+ def set(state, filename, range)
36
+ chunks = @files[filename] ||= RangeMap.new
37
+ chunks[range] = state
38
+ end
38
39
 
39
- def provided?(filename,chunk); get(filename,chunk) == :provided; end
40
- def requested?(filename,chunk); get(filename,chunk) == :requested; end
40
+ def provided?(filename, chunk)
41
+ get(filename, chunk) == :provided
42
+ end
43
+
44
+ def requested?(filename,chunk)
45
+ get(filename, chunk) == :requested
46
+ end
41
47
 
42
48
  #returns a high priority requested chunk
43
49
  def high_priority_chunk
44
50
  #right now return any chunk
45
- @files.each do |name,file|
46
- file.each_index do |i|
47
- return [name, i] if file[i] == :requested
48
- end
51
+ @files.each do |name, chunkmap|
52
+ range, _ = chunkmap.find { |_, state| state == :requested }
53
+ next unless range
54
+
55
+ return name, range.begin
49
56
  end
50
57
 
51
58
  nil
52
59
  end
53
-
54
- class FileStats
55
- attr_accessor :file_chunks, :chunks_requested, :url
56
- attr_accessor :chunks_provided, :chunks_transferring
57
-
58
- def initialize
59
- @url = ""
60
- @file_chunks = 0
61
- @chunks_requested = 0
62
- @chunks_provided = 0
63
- @chunks_transferring = 0
64
- end
65
- end
66
-
60
+
67
61
  # Returns an array of FileStats objects for debug output
68
62
  def get_file_stats
69
- @files.map do |name, file|
63
+ @files.map do |name, chunkmap|
70
64
  fs = FileStats.new
71
- fs.file_chunks = file.size
65
+ fs.file_chunks = chunkmap.last.end + 1
72
66
  fs.url = name
73
- file.each do |chunk|
74
- fs.chunks_requested += 1 if chunk == :requested
75
- fs.chunks_provided += 1 if chunk == :provided
76
- fs.chunks_transferring += 1 if chunk == :transfer
67
+
68
+ chunkmap.each do |range, state|
69
+ length = range.end - range.begin + 1
70
+
71
+ case state
72
+ when :requested then fs.chunks_requested += length
73
+ when :provided then fs.chunks_provided += length
74
+ when :transferring then fs.chunks_transferring += length
75
+ end
77
76
  end
78
77
 
79
78
  fs
80
79
  end
81
80
  end
82
-
81
+
83
82
  #########
84
83
  protected
85
84
  #########
86
85
 
87
- def get(filename,chunk)
86
+ def get(filename, chunk)
88
87
  @files[filename][chunk] rescue :neither
89
88
  end
89
+
90
+ class FileStats
91
+ attr_accessor :file_chunks, :chunks_requested, :url
92
+ attr_accessor :chunks_provided, :chunks_transferring
90
93
 
91
- def set(filename,range,state)
92
- chunks=@files[filename]||=Array.new
93
- range.each { |i| chunks[i]=state }
94
+ def initialize
95
+ @url = ""
96
+ @file_chunks = 0
97
+ @chunks_requested = 0
98
+ @chunks_provided = 0
99
+ @chunks_transferring = 0
100
+ end
94
101
  end
95
102
  end
96
103
  end
@@ -20,6 +20,7 @@
20
20
  # See http://distribustream.org/
21
21
  #++
22
22
 
23
+ require File.dirname(__FILE__) + '/../common'
23
24
  require File.dirname(__FILE__) + '/../common/protocol'
24
25
  require File.dirname(__FILE__) + '/chunk_info'
25
26
  require File.dirname(__FILE__) + '/trust'
@@ -57,11 +58,41 @@ module PDTP
57
58
 
58
59
  super
59
60
  end
61
+
62
+ # Address prefix of this peer
63
+ def prefix(mask = 24)
64
+ raise ArgumentError, "mask must be 8, 16, or 24" unless [8, 16, 24].include?(mask)
65
+ addr, _ = @cached_peer_info
66
+
67
+ octets = mask / 8
68
+ addr.split('.')[0..(octets - 1)].join('.')
69
+ end
70
+
71
+ # Calculate a score from 0 to 10 for two clients being a good match
72
+ def score(peer)
73
+ s = 0.0
74
+
75
+ # One point for each matching prefix
76
+ [8, 16, 24].each { |mask| s += 1 if prefix(mask) == peer.prefix(mask) }
77
+
78
+ # Up to three points for having ample bandwidth compared to us
79
+ [0.5, 1, 2].each { |divisor| s += 1 if peer.upstream_bandwidth > downstream_bandwidth / divisor }
80
+
81
+ # 0 - 4 points for trust
82
+ s += 4 * @trust.weight(peer.trust)
83
+
84
+ #puts "#{peer} trust: #{@trust.weight(peer.trust)}"
85
+
86
+ # Deprioritize file service by cutting its score in half
87
+ s /= 2 if peer.file_service?
88
+
89
+ s
90
+ end
60
91
 
61
92
  # Log a completed transfer and update internal data
62
93
  def success(transfer)
63
94
  if transfer.taker == self
64
- @chunk_info.provide transfer.url, transfer.chunkid..transfer.chunkid
95
+ @chunk_info.set :provided, transfer.url, transfer.chunkid..transfer.chunkid
65
96
  @trust.success transfer.giver.trust
66
97
  bandwidth_estimator = @downstream
67
98
  else
@@ -91,65 +122,82 @@ module PDTP
91
122
  def downstream_bandwidth
92
123
  @downstream.estimate rescue nil
93
124
  end
94
-
95
- # Returns true if this client wants the server to spawn a transfer for it
125
+
126
+ # Number of concurrent downloads desired desired
127
+ # FIXME hardcoded, should probably be computed or client-specified
128
+ def max_concurrent_downloads; 8; end
129
+
130
+ # For now, keep concurrent_downloads equal to uploads
131
+ alias_method :max_concurrent_uploads, :max_concurrent_downloads
132
+
133
+ # Maximum number of "half open" transfers allowed
134
+ # FIXME hardcoded, should probably be computed
135
+ def max_half_open; 8; end
136
+
137
+ # Are we below the limit on half-open transfer slots?
138
+ def empty_transfer_slots?
139
+ @transfers.select { |_, t| not t.verification_asked }.size < max_half_open
140
+ end
141
+
142
+ # List of all active downloads
143
+ def downloads
144
+ @transfers.select { |_, t| t.taker == self and t.verification_asked }
145
+ end
146
+
147
+ # List of all active uplaods
148
+ def uploads
149
+ @transfers.select { |_, t| t.giver == self and t.verification_asked }
150
+ end
151
+
152
+ # Returns true if this client wants the server to spawn a download for it
96
153
  def wants_download?
97
- transfer_state_allowed = 5
98
- total_allowed = 10
99
- transferring = 0
100
- @transfers.each do |key, t|
101
- transferring += 1 if t.verification_asked
102
- return false if transferring >= transfer_state_allowed
103
- end
104
-
105
- @transfers.size < total_allowed
106
- end
107
-
108
- # This could have a different definition, but it works fine to use wants_download?
109
- alias_method :wants_upload?, :wants_download?
154
+ return false unless @chunk_info.high_priority_chunk
155
+ return false unless empty_transfer_slots?
156
+
157
+ # Are we at our concurrent download limit?
158
+ downloads.size < max_concurrent_downloads
159
+ end
160
+
161
+ # Returns true if this client wants the server to spawn an uplaod for it
162
+ def wants_upload?
163
+ return false unless empty_transfer_slots?
164
+
165
+ # Are we at our concurrent upload limit?
166
+ uploads.size < max_concurrent_uploads
167
+ end
110
168
 
111
169
  # Returns a list of all the stalled transfers this client is a part of
112
170
  def stalled_transfers
113
- stalled = []
114
171
  timeout = 20.0
115
172
  now = Time.now
116
- @transfers.each do |key,t|
117
- #only delete if we are the acceptor to prevent race conditions
118
- next unless t.acceptor == self
119
- if now - t.creation_time > timeout and not t.verification_asked
120
- stalled << t
173
+
174
+ @transfers.inject([]) do |stalled, (_, t)|
175
+ # only delete if we are the acceptor to prevent race conditions
176
+ unless t.acceptor == self
177
+ stalled << t if now - t.creation_time > timeout and not t.verification_asked
121
178
  end
179
+
180
+ stalled
122
181
  end
123
- stalled
124
182
  end
125
183
 
126
184
  # Is this connection the file service?
127
185
  def file_service?
128
- @file_service
186
+ false
129
187
  end
130
-
131
- # Mark this connection as being a file service
132
- def mark_as_file_service
133
- @file_service = true
134
- end
135
-
188
+
136
189
  #
137
- # EventMachine callbacks to delegate to the dispatcher
190
+ # Delegate PDTP::Protocol callbacks to the dispatcher
138
191
  #
139
-
140
- def connection_completed
141
- raise(RuntimeError, 'server was never initialized') unless @dispatcher
142
- @dispatcher.connection_created self
143
- end
144
-
145
- def connection_destroyed
146
- raise(RuntimeError, 'server was never initialized') unless @dispatcher
147
- @dispatcher.connection_destroyed self
148
- end
149
192
 
150
- def receive_message(command, message)
151
- raise(RuntimeError, 'server was never initialized') unless @dispatcher
152
- @dispatcher.dispatch_message command, message, self
193
+ %w{connection_completed connection_destroyed receive_message}.each do |method|
194
+ module_eval <<-EOD
195
+ def #{method}(*args)
196
+ raise(RuntimeError, 'server was never initialized') unless @dispatcher
197
+ args << self
198
+ @dispatcher.#{method}(*args)
199
+ end
200
+ EOD
153
201
  end
154
202
  end
155
203
  end
@@ -21,12 +21,13 @@
21
21
  #++
22
22
 
23
23
  require File.dirname(__FILE__) + '/transfer_manager'
24
+ require File.dirname(__FILE__) + '/file_service_connection'
24
25
 
25
26
  module PDTP
26
27
  class Server
27
28
  # Core dispatching and control logic for PDTP servers
28
29
  class Dispatcher
29
- attr_reader :connections
30
+ attr_reader :connections, :server
30
31
 
31
32
  def initialize(server, file_service)
32
33
  @server = server
@@ -37,16 +38,22 @@ module PDTP
37
38
  end
38
39
 
39
40
  # Register a PDTP::Server::Connection with the Dispatcher
40
- def connection_created(connection)
41
+ def connection_completed(connection)
41
42
  addr, port = connection.get_peer_info
42
43
 
44
+ # FIXME hacked file service registration. There really ought to be a better way
45
+ # to both register and authenticate file services
43
46
  if @server.file_service_enabled? and not @seen_file_service and addr == @server.addr
44
- #display file service greeting when we see it connect
47
+ # Display file service greeting when we see it connect
45
48
  @server.log "file service running at #{addr}:#{@server.instance_eval { @http_port }}"
46
49
  @seen_file_service = true
47
- connection.mark_as_file_service
50
+
51
+ # Extend connection with file service-specific method implementations
52
+ connection.extend FileServiceConnection
53
+ elsif not @seen_file_service
54
+ raise RuntimeError, "File service failed to initialize. Ensure listen address is correct."
48
55
  else
49
- #display greeting for normal client
56
+ # Display greeting for normal client
50
57
  @server.log "client connected: #{connection.get_peer_info.inspect}"
51
58
  end
52
59
 
@@ -59,16 +66,8 @@ module PDTP
59
66
  @connections.delete connection
60
67
  end
61
68
 
62
- # This function removes all stalled transfers from the list
63
- # and spawns new transfers as appropriate
64
- # It must be called periodically by EventMachine
65
- def clear_all_stalled_transfers
66
- @connections.each { |connection| clear_stalled_transfers_for_client connection }
67
- @transfer_manager.spawn_all_transfers
68
- end
69
-
70
69
  # Handles all incoming messages from clients
71
- def dispatch_message(command, message, connection)
70
+ def receive_message(command, message, connection)
72
71
  # Store the command in the message hash
73
72
  message["type"] = command
74
73
 
@@ -98,7 +97,7 @@ module PDTP
98
97
  end
99
98
  connection.send_message :tell_info, response
100
99
  when "request", "provide", "unrequest", "unprovide"
101
- handle_requestprovide connection, message
100
+ handle_requestprovide connection, command, message
102
101
  when "ask_verify"
103
102
  #check if the specified transfer is a real one
104
103
  my_id = connection.client_id
@@ -136,6 +135,14 @@ module PDTP
136
135
  # Process all clients that are in need of new transfers
137
136
  @transfer_manager.spawn_all_transfers
138
137
  end
138
+
139
+ # This function removes all stalled transfers from the list
140
+ # and spawns new transfers as appropriate
141
+ # It must be called periodically by EventMachine
142
+ def clear_all_stalled_transfers
143
+ @connections.each { |connection| clear_stalled_transfers_for_client connection }
144
+ @transfer_manager.spawn_all_transfers
145
+ end
139
146
 
140
147
  #########
141
148
  protected
@@ -154,13 +161,8 @@ module PDTP
154
161
  local_hash = @file_service.get_chunk_hash transfer.url, transfer.chunkid
155
162
 
156
163
  if connection == transfer.taker
157
- success = (chunk_hash == local_hash)
158
-
159
- if success
160
- transfer.success
161
- else
162
- transfer.failure
163
- end
164
+ success = (chunk_hash == local_hash) # Capture for hash_verify message
165
+ success ? transfer.success : transfer.failure
164
166
 
165
167
  transfer.taker.send_message(:hash_verify,
166
168
  :url => transfer.url,
@@ -174,18 +176,24 @@ module PDTP
174
176
  @transfer_manager.process_client(connection)
175
177
  end
176
178
 
179
+ CHUNK_STATES = {
180
+ 'request' => :requested,
181
+ 'provide' => :provided,
182
+ 'unrequest' => :none,
183
+ 'unprovide' => :none,
184
+ } unless defined? CHUNK_STATES
185
+
177
186
  # Handles the request, provide, unrequest, unprovide messages
178
- def handle_requestprovide(connection, message)
179
- type = message["type"]
187
+ def handle_requestprovide(connection, command, message)
180
188
  url = message["url"]
181
189
  info = @file_service.get_info(url) rescue nil
182
190
  raise ProtocolWarn, "Requested URL: '#{url}' not found" if info.nil?
183
191
 
184
- exclude_partial = (type=="provide") #only exclude partial chunks from provides
192
+ exclude_partial = (command=="provide") #only exclude partial chunks from provides
185
193
  range = info.chunk_range_from_byte_range(message["range"],exclude_partial)
186
194
 
187
195
  #call request, provide, unrequest, or unprovide
188
- connection.chunk_info.send(type.to_sym, url, range)
196
+ connection.chunk_info.set(CHUNK_STATES[command], url, range)
189
197
  @transfer_manager.process_client(connection)
190
198
  end
191
199
  end
@@ -0,0 +1,58 @@
1
+ #--
2
+ # Copyright (C) 2006-07 ClickCaster, Inc. (info@clickcaster.com)
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+ #
17
+ # This source file is distributed as part of the
18
+ # DistribuStream file transfer system.
19
+ #
20
+ # See http://distribustream.org/
21
+ #++
22
+
23
+ require File.dirname(__FILE__) + '/../common/file_service'
24
+
25
+ module PDTP
26
+ class Server
27
+ #The server specific file utilities
28
+ class FileInfo < PDTP::FileInfo
29
+ attr_accessor :path
30
+
31
+ #Return a raw string of chunk data. The range parameter is local to this chunk
32
+ #and zero based
33
+ def chunk_data(chunkid, range = nil)
34
+ begin
35
+ range = 0..chunk_size(chunkid) - 1 if range.nil? # full range of chunk if range isnt specified
36
+ raise if range.first < 0 or range.last >= chunk_size(chunkid)
37
+ start = range.first + chunkid * @base_chunk_size
38
+ size = range.last - range.first + 1
39
+ file = open @path
40
+ file.pos = start
41
+ file.read size
42
+ rescue nil
43
+ end
44
+ end
45
+
46
+ #reads the specified byte range from the file and returns it as a string
47
+ def read(range)
48
+ #puts "READING: range=#{range}"
49
+ begin
50
+ file = open @path
51
+ file.pos = range.first
52
+ file.read range.last - range.first + 1
53
+ rescue nil
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -20,44 +20,14 @@
20
20
  # See http://distribustream.org/
21
21
  #++
22
22
 
23
- require "uri"
24
- require "pathname"
25
- require "digest/sha2"
26
- require File.dirname(__FILE__) + '/../common/file_service.rb'
23
+ require 'uri'
24
+ require 'pathname'
25
+ require 'digest/sha2'
26
+ require File.dirname(__FILE__) + '/../common/file_service'
27
+ require File.dirname(__FILE__) + '/file_info'
27
28
 
28
29
  module PDTP
29
30
  class Server
30
- #The server specific file utilities
31
- class FileInfo < PDTP::FileInfo
32
- attr_accessor :path
33
-
34
- #Return a raw string of chunk data. The range parameter is local to this chunk
35
- #and zero based
36
- def chunk_data(chunkid, range = nil)
37
- begin
38
- range = 0..chunk_size(chunkid) - 1 if range.nil? # full range of chunk if range isnt specified
39
- raise if range.first < 0 or range.last >= chunk_size(chunkid)
40
- start = range.first + chunkid * @base_chunk_size
41
- size = range.last - range.first + 1
42
- file = open @path
43
- file.pos = start
44
- file.read size
45
- rescue nil
46
- end
47
- end
48
-
49
- #reads the specified byte range from the file and returns it as a string
50
- def read(range)
51
- #puts "READING: range=#{range}"
52
- begin
53
- file = open @path
54
- file.pos = range.first
55
- file.read range.last - range.first + 1
56
- rescue nil
57
- end
58
- end
59
- end
60
-
61
31
  #The file service provides utilities for determining various information about files.
62
32
  class FileService < PDTP::FileService
63
33
  attr_accessor :root, :default_chunk_size
@@ -0,0 +1,44 @@
1
+ #--
2
+ # Copyright (C) 2006-07 ClickCaster, Inc. (info@clickcaster.com)
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+ #
17
+ # This source file is distributed as part of the
18
+ # DistribuStream file transfer system.
19
+ #
20
+ # See http://distribustream.org/
21
+ #++
22
+
23
+ require File.dirname(__FILE__) + '/../common'
24
+ require File.dirname(__FILE__) + '/../common/protocol'
25
+ require File.dirname(__FILE__) + '/chunk_info'
26
+ require File.dirname(__FILE__) + '/trust'
27
+ require File.dirname(__FILE__) + '/bandwidth_estimator'
28
+
29
+ module PDTP
30
+ class Server
31
+ # Mixin for PDTP::Server::Connection to give it file service capibility
32
+ module FileServiceConnection
33
+ # We are the file service
34
+ def file_service?
35
+ true
36
+ end
37
+
38
+ # The file service does not explicitly want uploads
39
+ #def wants_upload?
40
+ # false
41
+ #end
42
+ end
43
+ end
44
+ end
@@ -35,6 +35,7 @@ module PDTP
35
35
 
36
36
  def initialize(vhost, dispatcher)
37
37
  @vhost, @dispatcher = vhost, dispatcher
38
+ @status_erb = File.expand_path(File.dirname(__FILE__) + '/../../../status/index.erb')
38
39
  end
39
40
 
40
41
  # Process an incoming request to generate the status page
@@ -42,7 +43,7 @@ module PDTP
42
43
  response.start(200) do |head, out|
43
44
  out.write begin
44
45
  # Read the status page ERb template
45
- erb_data = File.read(File.dirname(__FILE__) + '/../../../status/index.erb')
46
+ erb_data = File.read @status_erb
46
47
 
47
48
  # Render the status ERb template
48
49
  html = ERB.new(erb_data).result(binding)
@@ -59,4 +60,4 @@ module PDTP
59
60
  end
60
61
  end
61
62
  end
62
- end
63
+ end
@@ -30,22 +30,22 @@ module PDTP
30
30
  @connections, @file_service = connections, file_service
31
31
  @updated_clients = []
32
32
  end
33
-
33
+
34
34
  # Add client to list of ones needing updates
35
35
  def process_client(client)
36
36
  @updated_clients << client
37
37
  end
38
-
38
+
39
39
  # Creates new transfers for all clients that have been updated
40
40
  def spawn_all_transfers
41
41
  @updated_clients.each { |client| spawn_transfers_for_client(client) }
42
42
  @updated_clients.clear
43
43
  end
44
-
44
+
45
45
  #########
46
46
  protected
47
47
  #########
48
-
48
+
49
49
  # Spawns uploads and downloads for this client.
50
50
  # Should be called every time there is a change that would affect
51
51
  # what this client has or wants
@@ -70,48 +70,62 @@ module PDTP
70
70
  return false
71
71
  end
72
72
 
73
- @connections.each do |c2|
74
- next if connection == c2
75
- next unless c2.wants_upload?
76
- if c2.chunk_info.provided?(url, chunkid)
77
- feasible_peers << c2
78
- break if feasible_peers.size > 5
79
- end
80
- end
81
-
82
- # we now have a list of clients that have the requested chunk.
83
- # pick one and start the transfer
84
- if feasible_peers.size > 0
85
- #FIXME base this on the trust model
86
- giver = feasible_peers[rand(feasible_peers.size)]
87
- return begin_transfer(connection,giver,url,chunkid)
88
- #FIXME should we try again if begin_transfer fails?
73
+ @connections.each do |peer|
74
+ next if connection == peer
75
+ next unless peer.wants_upload?
76
+ feasible_peers << peer if peer.chunk_info.provided?(url, chunkid)
89
77
  end
90
-
78
+
79
+ # debug info
80
+ # puts "Feasible peers: " + feasible_peers.map { |peer| peer.to_s }.join(", ")
81
+
82
+ giver = optimal_peer(connection, feasible_peers)
83
+ return begin_transfer(connection, giver, url, chunkid) if giver
84
+
85
+ #FIXME should we try again if begin_transfer fails?
91
86
  false
92
87
  end
88
+
89
+ # Apply constraints to the list of potential peers to select
90
+ # the optimal one
91
+ def optimal_peer(connection, peer_list)
92
+ # We can't do anything if there's zero or one candidates
93
+ return peer_list.first if peer_list.size <= 1
94
+
95
+ # Build a list of peers and their respective scores
96
+ scored_peers = peer_list.map { |peer| [peer, connection.score(peer)] }
97
+
98
+ # Sort the peers by score
99
+ sorted_peers = scored_peers.sort { |(_, a), (_, b)| b <=> a }
100
+
101
+ # debug info
102
+ #puts "Sorted peers: " + sorted_peers.map { |peer, score| "#{peer}(#{score})" }.join(", ")
103
+
104
+ # Return the top choice (from a nested array)
105
+ sorted_peers.first.first
106
+ end
93
107
 
94
108
  # Creates a single upload for the specified client
95
109
  # Returns true on success, false on failure
96
110
  def spawn_upload_for_client(connection)
97
- @connections.each do |c2|
98
- next if connection == c2
99
- next unless c2.wants_download?
111
+ @connections.each do |peer|
112
+ next if connection == peer
113
+ next unless peer.wants_download?
100
114
 
101
115
  begin
102
- url, chunkid = c2.chunk_info.high_priority_chunk
116
+ url, chunkid = peer.chunk_info.high_priority_chunk
103
117
  rescue
104
118
  next
105
119
  end
106
120
 
107
121
  if connection.chunk_info.provided?(url, chunkid)
108
- return begin_transfer(c2, connection, url, chunkid)
122
+ return begin_transfer(peer, connection, url, chunkid)
109
123
  end
110
124
  end
111
125
 
112
126
  false
113
127
  end
114
-
128
+
115
129
  # Creates a new transfer between two peers
116
130
  # Returns true on success, or false if the specified transfer is already in progress
117
131
  def begin_transfer(taker, giver, url, chunkid)
@@ -123,13 +137,13 @@ module PDTP
123
137
  t2 = giver.transfers[t.transfer_id]
124
138
  return false unless t1.nil? and t2.nil?
125
139
 
126
- taker.chunk_info.transfer(url, chunkid..chunkid)
140
+ taker.chunk_info.set(:transfer, url, chunkid..chunkid)
127
141
  taker.transfers[t.transfer_id] = t
128
142
  giver.transfers[t.transfer_id] = t
129
143
 
130
144
  #send transfer message to the connector
131
145
  addr, port = t.acceptor.get_peer_info
132
-
146
+
133
147
  t.connector.send_message(:transfer,
134
148
  :host => addr,
135
149
  :port => t.acceptor.listen_port,
@@ -138,9 +152,9 @@ module PDTP
138
152
  :range => byte_range,
139
153
  :peer_id => t.acceptor.client_id
140
154
  )
141
-
155
+
142
156
  true
143
157
  end
144
158
  end
145
159
  end
146
- end
160
+ end
@@ -81,18 +81,15 @@ module PDTP
81
81
  @outgoing.each { |_, link| link.trust = link.success / total_transfers }
82
82
  @outgoing.each do |target, link|
83
83
  [target.outgoing, target.implicit].each do |links|
84
- links.each do |nextlinkedge|
85
- nextlinktarget = nextlinkedge[0]
86
- nextlink = nextlinkedge[1]
84
+ links.each do |nextlinktarget, nextlink|
87
85
  next unless outgoing[nextlinktarget].nil?
88
-
89
- if implicit[nextlinktarget].nil? || implicit[nextlinktarget].trust < (link.trust * nextlink.trust)
90
- implicit[nextlinktarget] = Edge.new(
86
+ next unless implicit[nextlinktarget].nil? or implicit[nextlinktarget].trust < link.trust * nextlink.trust
87
+
88
+ implicit[nextlinktarget] = Edge.new(
91
89
  link.trust * nextlink.trust,
92
90
  nextlink.success,
93
91
  nextlink.transfers
94
- )
95
- end
92
+ )
96
93
  end
97
94
  end
98
95
  end
@@ -22,8 +22,8 @@
22
22
  <% each_peer do |peer| %>
23
23
  <tr class="row<%= cycle(' row_alternate', '') %>">
24
24
  <td>
25
- <%= peer_name(peer) %><br />
26
- <%= peer_address(peer) %>
25
+ <%= peer_name(peer) %><br />
26
+ <%= peer_address(peer) unless peer.file_service? %>
27
27
  </td>
28
28
  <td>
29
29
  Upstream: <%= upstream_bandwidth(peer) %><br />
metadata CHANGED
@@ -1,10 +1,10 @@
1
1
  --- !ruby/object:Gem::Specification
2
- rubygems_version: 0.9.4
2
+ rubygems_version: 0.9.0
3
3
  specification_version: 1
4
4
  name: distribustream
5
5
  version: !ruby/object:Gem::Version
6
- version: 0.5.0
7
- date: 2008-11-27 00:00:00 -07:00
6
+ version: 0.5.1
7
+ date: 2008-12-04 00:00:00 -07:00
8
8
  summary: DistribuStream is a fully open peercasting system allowing on-demand or live streaming media to be delivered at a fraction of the normal cost
9
9
  require_paths:
10
10
  - lib
@@ -33,45 +33,48 @@ authors:
33
33
  - James Sanders
34
34
  - Tom Stapleton
35
35
  files:
36
- - bin/dsclient
37
36
  - bin/dstream
37
+ - bin/dsclient
38
38
  - lib/pdtp
39
+ - lib/pdtp/server
40
+ - lib/pdtp/common.rb
39
41
  - lib/pdtp/client
40
- - lib/pdtp/client/callbacks.rb
41
- - lib/pdtp/client/connection.rb
42
+ - lib/pdtp/common
43
+ - lib/pdtp/client.rb
44
+ - lib/pdtp/server.rb
45
+ - lib/pdtp/server/file_info.rb
46
+ - lib/pdtp/server/file_service_protocol.rb
47
+ - lib/pdtp/server/chunk_info.rb
48
+ - lib/pdtp/server/transfer_manager.rb
49
+ - lib/pdtp/server/status_handler.rb
50
+ - lib/pdtp/server/trust.rb
51
+ - lib/pdtp/server/status_helper.rb
52
+ - lib/pdtp/server/bandwidth_estimator.rb
53
+ - lib/pdtp/server/file_service_connection.rb
54
+ - lib/pdtp/server/transfer.rb
55
+ - lib/pdtp/server/connection.rb
56
+ - lib/pdtp/server/file_service.rb
57
+ - lib/pdtp/server/dispatcher.rb
42
58
  - lib/pdtp/client/file_buffer.rb
43
- - lib/pdtp/client/file_service.rb
44
59
  - lib/pdtp/client/http_client.rb
45
60
  - lib/pdtp/client/http_handler.rb
61
+ - lib/pdtp/client/callbacks.rb
46
62
  - lib/pdtp/client/transfer.rb
47
- - lib/pdtp/client.rb
48
- - lib/pdtp/common
49
- - lib/pdtp/common/file_service.rb
50
- - lib/pdtp/common/http_server.rb
63
+ - lib/pdtp/client/connection.rb
64
+ - lib/pdtp/client/file_service.rb
51
65
  - lib/pdtp/common/length_prefix_protocol.rb
66
+ - lib/pdtp/common/http_server.rb
52
67
  - lib/pdtp/common/protocol.rb
53
- - lib/pdtp/common.rb
54
- - lib/pdtp/server
55
- - lib/pdtp/server/bandwidth_estimator.rb
56
- - lib/pdtp/server/chunk_info.rb
57
- - lib/pdtp/server/connection.rb
58
- - lib/pdtp/server/dispatcher.rb
59
- - lib/pdtp/server/file_service.rb
60
- - lib/pdtp/server/file_service_protocol.rb
61
- - lib/pdtp/server/status_handler.rb
62
- - lib/pdtp/server/status_helper.rb
63
- - lib/pdtp/server/transfer.rb
64
- - lib/pdtp/server/transfer_manager.rb
65
- - lib/pdtp/server/trust.rb
66
- - lib/pdtp/server.rb
68
+ - lib/pdtp/common/range_map.rb
69
+ - lib/pdtp/common/file_service.rb
70
+ - conf/example.yml
67
71
  - conf/bigchunk.yml
68
72
  - conf/debug.yml
69
- - conf/example.yml
73
+ - status/stylesheets
70
74
  - status/images
71
- - status/images/logo.png
72
75
  - status/index.erb
73
- - status/stylesheets
74
76
  - status/stylesheets/style.css
77
+ - status/images/logo.png
75
78
  - Rakefile
76
79
  - distribustream.gemspec
77
80
  - COPYING
@@ -126,3 +129,12 @@ dependencies:
126
129
  - !ruby/object:Gem::Version
127
130
  version: 1.1.0
128
131
  version:
132
+ - !ruby/object:Gem::Dependency
133
+ name: activesupport
134
+ version_requirement:
135
+ version_requirements: !ruby/object:Gem::Version::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: 1.4.0
140
+ version: