distribustream 0.5.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
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: