distribustream 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGES +13 -0
- data/README +3 -7
- data/Rakefile +2 -2
- data/distribustream.gemspec +18 -9
- data/lib/pdtp/client/callbacks.rb +4 -1
- data/lib/pdtp/common.rb +1 -1
- data/lib/pdtp/common/http_server.rb +5 -0
- data/lib/pdtp/common/protocol.rb +1 -8
- data/lib/pdtp/server/bandwidth_estimator.rb +148 -0
- data/lib/pdtp/server/chunk_info.rb +97 -0
- data/lib/pdtp/server/connection.rb +102 -3
- data/lib/pdtp/server/dispatcher.rb +82 -210
- data/lib/pdtp/server/file_service.rb +5 -4
- data/lib/pdtp/server/status_helper.rb +19 -3
- data/lib/pdtp/server/transfer.rb +26 -14
- data/lib/pdtp/server/transfer_manager.rb +146 -0
- data/status/index.erb +31 -17
- data/status/stylesheets/style.css +1 -1
- metadata +33 -30
- data/lib/pdtp/server/client_info.rb +0 -154
@@ -20,10 +20,7 @@
|
|
20
20
|
# See http://distribustream.org/
|
21
21
|
#++
|
22
22
|
|
23
|
-
require File.dirname(__FILE__) + '
|
24
|
-
require File.dirname(__FILE__) + '/file_service'
|
25
|
-
require File.dirname(__FILE__) + '/client_info'
|
26
|
-
require File.dirname(__FILE__) + '/transfer'
|
23
|
+
require File.dirname(__FILE__) + '/transfer_manager'
|
27
24
|
|
28
25
|
module PDTP
|
29
26
|
class Server
|
@@ -36,10 +33,10 @@ module PDTP
|
|
36
33
|
@file_service = file_service
|
37
34
|
@connections = []
|
38
35
|
@used_client_ids = {} #keeps a list of client ids in use, they must be unique
|
39
|
-
@
|
36
|
+
@transfer_manager = TransferManager.new @connections, @file_service
|
40
37
|
end
|
41
38
|
|
42
|
-
#
|
39
|
+
# Register a PDTP::Server::Connection with the Dispatcher
|
43
40
|
def connection_created(connection)
|
44
41
|
addr, port = connection.get_peer_info
|
45
42
|
|
@@ -53,203 +50,31 @@ module PDTP
|
|
53
50
|
@server.log "client connected: #{connection.get_peer_info.inspect}"
|
54
51
|
end
|
55
52
|
|
56
|
-
connection.user_data = ClientInfo.new
|
57
53
|
@connections << connection
|
58
54
|
end
|
59
55
|
|
60
|
-
#
|
56
|
+
# Unregister a PDTP::Server::Connection from the Dispatcher
|
61
57
|
def connection_destroyed(connection)
|
62
58
|
@server.log "client disconnected: #{connection.get_peer_info.inspect}"
|
63
59
|
@connections.delete connection
|
64
60
|
end
|
65
|
-
|
66
|
-
# returns the ClientInfo object associated with this connection
|
67
|
-
def client_info(connection)
|
68
|
-
connection.user_data ||= ClientInfo.new
|
69
|
-
end
|
70
|
-
|
71
|
-
# called when a transfer either finishes, successfully or not
|
72
|
-
def transfer_completed(transfer,connection,chunk_hash,send_response=true)
|
73
|
-
# did the transfer complete successfully?
|
74
|
-
local_hash=@file_service.get_chunk_hash(transfer.url,transfer.chunkid)
|
75
|
-
|
76
|
-
c1=client_info(transfer.taker)
|
77
|
-
c2=client_info(transfer.giver)
|
78
|
-
|
79
|
-
if connection==transfer.taker
|
80
|
-
success= (chunk_hash==local_hash)
|
81
|
-
|
82
|
-
if success
|
83
|
-
#the taker now has the file, so he can provide it
|
84
|
-
client_info(transfer.taker).chunk_info.provide(transfer.url,transfer.chunkid..transfer.chunkid)
|
85
|
-
c1.trust.success(c2.trust)
|
86
|
-
else
|
87
|
-
#transfer failed, the client still wants the chunk
|
88
|
-
client_info(transfer.taker).chunk_info.request(transfer.url,transfer.chunkid..transfer.chunkid)
|
89
|
-
c1.trust.failure(c2.trust)
|
90
|
-
end
|
91
|
-
|
92
|
-
transfer.taker.send_message(:hash_verify,
|
93
|
-
:url => transfer.url,
|
94
|
-
:range => transfer.byte_range,
|
95
|
-
:hash_ok => success
|
96
|
-
) if send_response
|
97
|
-
end
|
98
|
-
|
99
|
-
#remove this transfer from whoever sent it
|
100
|
-
client_info(connection).transfers.delete(transfer.transfer_id)
|
101
|
-
@updated_clients[connection]=true #flag this client for transfer creation
|
102
|
-
end
|
103
|
-
|
104
|
-
#Creates a new transfer between two peers
|
105
|
-
#returns true on success, or false if the specified transfer is already in progress
|
106
|
-
def begin_transfer(taker, giver, url, chunkid)
|
107
|
-
byte_range = @file_service.get_info(url).chunk_range(chunkid)
|
108
|
-
t = Transfer.new(taker, giver, url, chunkid, byte_range)
|
109
|
-
|
110
|
-
#make sure this transfer doesnt already exist
|
111
|
-
t1 = client_info(taker).transfers[t.transfer_id]
|
112
|
-
t2 = client_info(giver).transfers[t.transfer_id]
|
113
|
-
return false unless t1.nil? and t2.nil?
|
114
|
-
|
115
|
-
client_info(taker).chunk_info.transfer(url, chunkid..chunkid)
|
116
|
-
client_info(taker).transfers[t.transfer_id] = t
|
117
|
-
client_info(giver).transfers[t.transfer_id] = t
|
118
|
-
|
119
|
-
#send transfer message to the connector
|
120
|
-
addr, port = t.acceptor.get_peer_info
|
121
61
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
:method => t.connector == t.taker ? "get" : "put",
|
126
|
-
:url => url,
|
127
|
-
:range => byte_range,
|
128
|
-
:peer_id => client_info(t.acceptor).client_id
|
129
|
-
)
|
130
|
-
true
|
131
|
-
end
|
132
|
-
|
133
|
-
#this function removes all stalled transfers from the list
|
134
|
-
#and spawns new transfers as appropriate
|
135
|
-
#it must be called periodically by EventMachine
|
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
|
136
65
|
def clear_all_stalled_transfers
|
137
66
|
@connections.each { |connection| clear_stalled_transfers_for_client connection }
|
138
|
-
spawn_all_transfers
|
139
|
-
end
|
140
|
-
|
141
|
-
#removes all stalled transfers that this client is a part of
|
142
|
-
def clear_stalled_transfers_for_client(client_connection)
|
143
|
-
client_info(client_connection).get_stalled_transfers.each do |transfer|
|
144
|
-
transfer_completed transfer, client_connection, nil, false
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
#spawns uploads and downloads for this client.
|
149
|
-
#should be called every time there is a change that would affect
|
150
|
-
#what this client has or wants
|
151
|
-
def spawn_transfers_for_client(client_connection)
|
152
|
-
info = client_info client_connection
|
153
|
-
|
154
|
-
while info.wants_download? do
|
155
|
-
break if spawn_download_for_client(client_connection) == false
|
156
|
-
end
|
157
|
-
|
158
|
-
while info.wants_upload? do
|
159
|
-
break if spawn_upload_for_client(client_connection) == false
|
160
|
-
end
|
67
|
+
@transfer_manager.spawn_all_transfers
|
161
68
|
end
|
162
|
-
|
163
|
-
#
|
164
|
-
#returns true on success, false on failure
|
165
|
-
def spawn_download_for_client(client_connection)
|
166
|
-
feasible_peers=[]
|
167
|
-
|
168
|
-
c1info=client_info(client_connection)
|
169
|
-
begin
|
170
|
-
url,chunkid=c1info.chunk_info.high_priority_chunk
|
171
|
-
rescue
|
172
|
-
return false
|
173
|
-
end
|
174
|
-
|
175
|
-
@connections.each do |c2|
|
176
|
-
next if client_connection==c2
|
177
|
-
next if client_info(c2).wants_upload? == false
|
178
|
-
if client_info(c2).chunk_info.provided?(url,chunkid)
|
179
|
-
feasible_peers << c2
|
180
|
-
break if feasible_peers.size > 5
|
181
|
-
end
|
182
|
-
end
|
183
|
-
|
184
|
-
# we now have a list of clients that have the requested chunk.
|
185
|
-
# pick one and start the transfer
|
186
|
-
if feasible_peers.size > 0
|
187
|
-
#FIXME base this on the trust model
|
188
|
-
giver=feasible_peers[rand(feasible_peers.size)]
|
189
|
-
return begin_transfer(client_connection,giver,url,chunkid)
|
190
|
-
#FIXME should we try again if begin_transfer fails?
|
191
|
-
end
|
192
|
-
|
193
|
-
false
|
194
|
-
end
|
195
|
-
|
196
|
-
#creates a single upload for the specified client
|
197
|
-
#returns true on success, false on failure
|
198
|
-
def spawn_upload_for_client(client_connection)
|
199
|
-
c1info=client_info(client_connection)
|
200
|
-
|
201
|
-
@connections.each do |c2|
|
202
|
-
next if client_connection==c2
|
203
|
-
next if client_info(c2).wants_download? == false
|
204
|
-
|
205
|
-
begin
|
206
|
-
url,chunkid=client_info(c2).chunk_info.high_priority_chunk
|
207
|
-
rescue
|
208
|
-
next
|
209
|
-
end
|
210
|
-
|
211
|
-
if c1info.chunk_info.provided?(url,chunkid)
|
212
|
-
return begin_transfer(c2,client_connection,url,chunkid)
|
213
|
-
end
|
214
|
-
end
|
215
|
-
|
216
|
-
false
|
217
|
-
end
|
218
|
-
|
219
|
-
#creates new transfers for all clients that have been updated
|
220
|
-
def spawn_all_transfers
|
221
|
-
while @updated_clients.size > 0 do
|
222
|
-
tmp=@updated_clients
|
223
|
-
@updated_clients=Hash.new
|
224
|
-
tmp.each do |client,true_key|
|
225
|
-
spawn_transfers_for_client(client)
|
226
|
-
end
|
227
|
-
end
|
228
|
-
end
|
229
|
-
|
230
|
-
#handles the request, provide, unrequest, unprovide messages
|
231
|
-
def handle_requestprovide(connection,message)
|
232
|
-
type=message["type"]
|
233
|
-
url=message["url"]
|
234
|
-
info=@file_service.get_info(url) rescue nil
|
235
|
-
raise ProtocolWarn.new("Requested URL: '#{url}' not found") if info.nil?
|
236
|
-
|
237
|
-
exclude_partial= (type=="provide") #only exclude partial chunks from provides
|
238
|
-
range=info.chunk_range_from_byte_range(message["range"],exclude_partial)
|
239
|
-
|
240
|
-
#call request, provide, unrequest, or unprovide
|
241
|
-
client_info(connection).chunk_info.send( type.to_sym, url, range)
|
242
|
-
@updated_clients[connection]=true #add to the list of client that need new transfers
|
243
|
-
end
|
244
|
-
|
245
|
-
#handles all incoming messages from clients
|
69
|
+
|
70
|
+
# Handles all incoming messages from clients
|
246
71
|
def dispatch_message(command, message, connection)
|
247
|
-
#
|
72
|
+
# Store the command in the message hash
|
248
73
|
message["type"] = command
|
249
74
|
|
250
|
-
#
|
251
|
-
if command != "register" and
|
252
|
-
raise ProtocolError
|
75
|
+
# Require the client to register their client id and listen port before doing anything
|
76
|
+
if command != "register" and connection.client_id.nil?
|
77
|
+
raise ProtocolError, "You need to send a 'register' message first"
|
253
78
|
end
|
254
79
|
|
255
80
|
case command
|
@@ -257,12 +82,12 @@ module PDTP
|
|
257
82
|
cid = message["client_id"]
|
258
83
|
#make sure this id isnt in use
|
259
84
|
if @used_client_ids[cid]
|
260
|
-
raise ProtocolError
|
85
|
+
raise ProtocolError, "Your client id: #{cid} is already in use."
|
261
86
|
end
|
262
87
|
|
263
88
|
@used_client_ids[cid] = true
|
264
|
-
|
265
|
-
|
89
|
+
connection.listen_port = message["listen_port"]
|
90
|
+
connection.client_id = cid
|
266
91
|
when "ask_info"
|
267
92
|
info = @file_service.get_info(message["url"])
|
268
93
|
response = { :url => message["url"] }
|
@@ -276,45 +101,92 @@ module PDTP
|
|
276
101
|
handle_requestprovide connection, message
|
277
102
|
when "ask_verify"
|
278
103
|
#check if the specified transfer is a real one
|
279
|
-
my_id =
|
280
|
-
transfer_id=Transfer.gen_transfer_id(my_id,message["peer_id"],message["url"],message["range"])
|
281
|
-
|
282
|
-
|
283
|
-
|
104
|
+
my_id = connection.client_id
|
105
|
+
transfer_id = Transfer.gen_transfer_id(my_id,message["peer_id"],message["url"],message["range"])
|
106
|
+
authorized = !connection.transfers[transfer_id].nil?
|
107
|
+
|
108
|
+
connection.transfers[transfer_id].verification_asked = true if authorized
|
109
|
+
@server.debug "AskVerify not ok: id=#{transfer_id}" unless authorized
|
284
110
|
connection.send_message(:tell_verify,
|
285
111
|
:url => message["url"],
|
286
112
|
:peer_id => message["peer_id"],
|
287
113
|
:range => message["range"],
|
288
114
|
:peer => message["peer"],
|
289
|
-
:authorized=>
|
115
|
+
:authorized=>authorized
|
290
116
|
)
|
291
117
|
when "completed"
|
292
|
-
my_id =
|
118
|
+
my_id = connection.client_id
|
293
119
|
transfer_id = Transfer::gen_transfer_id(
|
294
120
|
my_id,
|
295
121
|
message["peer_id"],
|
296
122
|
message["url"],
|
297
123
|
message["range"]
|
298
124
|
)
|
299
|
-
transfer=
|
300
|
-
@server.debug
|
125
|
+
transfer = connection.transfers[transfer_id]
|
126
|
+
@server.debug "Completed: id=#{transfer_id} ok=#{transfer != nil}"
|
301
127
|
if transfer
|
302
|
-
transfer_completed
|
128
|
+
transfer_completed transfer, connection, message["hash"]
|
303
129
|
else
|
304
|
-
raise ProtocolWarn
|
130
|
+
raise ProtocolWarn, "You sent me a transfer completed message for unknown transfer: #{transfer_id}"
|
305
131
|
end
|
306
132
|
when 'protocol_error', 'protocol_warn' #ignore
|
307
|
-
else raise ProtocolError
|
133
|
+
else raise ProtocolError, "Unhandled message type: #{command}"
|
308
134
|
end
|
309
135
|
|
310
|
-
|
136
|
+
# Process all clients that are in need of new transfers
|
137
|
+
@transfer_manager.spawn_all_transfers
|
311
138
|
end
|
312
139
|
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
140
|
+
#########
|
141
|
+
protected
|
142
|
+
#########
|
143
|
+
|
144
|
+
# Removes all stalled transfers that this client is a part of
|
145
|
+
def clear_stalled_transfers_for_client(connection)
|
146
|
+
connection.stalled_transfers.each do |transfer|
|
147
|
+
transfer_completed transfer, connection, nil, false
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# Called when a transfer either finishes, successfully or not
|
152
|
+
def transfer_completed(transfer, connection, chunk_hash, send_response=true)
|
153
|
+
# Compute the SHA256 hash for the given file
|
154
|
+
local_hash = @file_service.get_chunk_hash transfer.url, transfer.chunkid
|
155
|
+
|
156
|
+
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
|
+
|
165
|
+
transfer.taker.send_message(:hash_verify,
|
166
|
+
:url => transfer.url,
|
167
|
+
:range => transfer.byte_range,
|
168
|
+
:hash_ok => success
|
169
|
+
) if send_response
|
170
|
+
end
|
171
|
+
|
172
|
+
# Remove this transfer from whoever sent it
|
173
|
+
connection.transfers.delete(transfer.transfer_id)
|
174
|
+
@transfer_manager.process_client(connection)
|
175
|
+
end
|
176
|
+
|
177
|
+
# Handles the request, provide, unrequest, unprovide messages
|
178
|
+
def handle_requestprovide(connection, message)
|
179
|
+
type = message["type"]
|
180
|
+
url = message["url"]
|
181
|
+
info = @file_service.get_info(url) rescue nil
|
182
|
+
raise ProtocolWarn, "Requested URL: '#{url}' not found" if info.nil?
|
183
|
+
|
184
|
+
exclude_partial = (type=="provide") #only exclude partial chunks from provides
|
185
|
+
range = info.chunk_range_from_byte_range(message["range"],exclude_partial)
|
186
|
+
|
187
|
+
#call request, provide, unrequest, or unprovide
|
188
|
+
connection.chunk_info.send(type.to_sym, url, range)
|
189
|
+
@transfer_manager.process_client(connection)
|
318
190
|
end
|
319
191
|
end
|
320
192
|
end
|
@@ -38,7 +38,7 @@ module PDTP
|
|
38
38
|
range = 0..chunk_size(chunkid) - 1 if range.nil? # full range of chunk if range isnt specified
|
39
39
|
raise if range.first < 0 or range.last >= chunk_size(chunkid)
|
40
40
|
start = range.first + chunkid * @base_chunk_size
|
41
|
-
size = range.last-range.first + 1
|
41
|
+
size = range.last - range.first + 1
|
42
42
|
file = open @path
|
43
43
|
file.pos = start
|
44
44
|
file.read size
|
@@ -72,7 +72,8 @@ module PDTP
|
|
72
72
|
# by checking whether the registered file exists
|
73
73
|
def get_info(url)
|
74
74
|
begin
|
75
|
-
host = URI.
|
75
|
+
host = URI.parse(url).host
|
76
|
+
|
76
77
|
#FIXME we should check host against a list of known hosts here
|
77
78
|
info = FileInfo.new
|
78
79
|
info.streaming = false
|
@@ -90,13 +91,13 @@ module PDTP
|
|
90
91
|
|
91
92
|
#returns the path of this file on the local filesystem
|
92
93
|
def get_local_path(url)
|
93
|
-
path = URI.
|
94
|
+
path = URI.parse(url).path
|
94
95
|
path = path[1..path.size-1] #remove leading /
|
95
96
|
(Pathname.new(@root) + path).to_s
|
96
97
|
end
|
97
98
|
|
98
99
|
#returns the SHA256 hash of the specified chunk
|
99
|
-
def get_chunk_hash(url,chunk_id)
|
100
|
+
def get_chunk_hash(url, chunk_id)
|
100
101
|
Digest::SHA256.hexdigest(get_info(url).chunk_data(chunk_id)) rescue nil
|
101
102
|
end
|
102
103
|
end
|
@@ -43,12 +43,12 @@ module PDTP
|
|
43
43
|
# Iterate over all of a peer's active transfers
|
44
44
|
def each_transfer(peer)
|
45
45
|
raise ArgumentError, "no block given" unless block_given?
|
46
|
-
|
46
|
+
peer.transfers.each { |_, transfer| yield transfer }
|
47
47
|
end
|
48
48
|
|
49
49
|
# Iterate over all of a peer's active files
|
50
50
|
def each_file(peer, &block)
|
51
|
-
|
51
|
+
peer.chunk_info.get_file_stats.each(&block)
|
52
52
|
end
|
53
53
|
|
54
54
|
# Name of a peer (client ID or file service)
|
@@ -56,7 +56,7 @@ module PDTP
|
|
56
56
|
if peer.file_service?
|
57
57
|
"<b>File Service</b>"
|
58
58
|
else
|
59
|
-
|
59
|
+
peer.client_id
|
60
60
|
end
|
61
61
|
end
|
62
62
|
|
@@ -65,6 +65,22 @@ module PDTP
|
|
65
65
|
host, port = peer.get_peer_info
|
66
66
|
"#{host}:#{port}"
|
67
67
|
end
|
68
|
+
|
69
|
+
# Upstream bandwidth of a peer
|
70
|
+
def upstream_bandwidth(peer)
|
71
|
+
bandwidth = peer.upstream_bandwidth
|
72
|
+
return 'N/A' if bandwidth.nil?
|
73
|
+
|
74
|
+
"#{bandwidth / 1024} kBps"
|
75
|
+
end
|
76
|
+
|
77
|
+
# Downstream bandwidth of a peer
|
78
|
+
def downstream_bandwidth(peer)
|
79
|
+
bandwidth = peer.downstream_bandwidth
|
80
|
+
return 'N/A' if bandwidth.nil?
|
81
|
+
|
82
|
+
"#{bandwidth / 1024} kBps"
|
83
|
+
end
|
68
84
|
|
69
85
|
# Information about an active transfer
|
70
86
|
def transfer_info(peer, transfer)
|
data/lib/pdtp/server/transfer.rb
CHANGED
@@ -30,34 +30,46 @@ module PDTP
|
|
30
30
|
attr_accessor :creation_time
|
31
31
|
attr_accessor :verification_asked
|
32
32
|
|
33
|
-
#
|
33
|
+
# Generates a transfer id based on 2 client ids, a url, and a byte range
|
34
34
|
def self.gen_transfer_id(id1,id2,url,byte_range)
|
35
|
-
a = id1<id2 ? id1 : id2
|
36
|
-
b = id1<id2 ? id2 : id1
|
35
|
+
a = id1 < id2 ? id1 : id2
|
36
|
+
b = id1 < id2 ? id2 : id1
|
37
37
|
"#{a}$#{b}$#{url}$#{byte_range}"
|
38
38
|
end
|
39
39
|
|
40
|
-
def initialize(taker,giver,url,chunkid,byte_range,connector_receives=true)
|
41
|
-
@taker
|
40
|
+
def initialize(taker, giver, url, chunkid, byte_range, connector_receives = true)
|
41
|
+
@taker, @giver, @url, @chunkid, @byte_range = taker, giver, url, chunkid, byte_range
|
42
42
|
|
43
43
|
@verification_asked = false
|
44
44
|
@creation_time = Time.now
|
45
|
+
|
45
46
|
if connector_receives
|
46
|
-
@connector
|
47
|
-
@acceptor
|
47
|
+
@connector = @taker
|
48
|
+
@acceptor = @giver
|
48
49
|
else
|
49
|
-
@connector
|
50
|
-
@acceptor
|
50
|
+
@connector = @giver
|
51
|
+
@acceptor = @taker
|
51
52
|
end
|
52
53
|
|
53
54
|
recompute_transfer_id
|
54
55
|
end
|
55
56
|
|
56
|
-
#
|
57
|
+
# Calculates the transfer id for this transfer based on the local data
|
57
58
|
def recompute_transfer_id
|
58
|
-
id1=connector.
|
59
|
-
id2=acceptor.
|
60
|
-
@transfer_id=Transfer::gen_transfer_id
|
59
|
+
id1 = connector.client_id
|
60
|
+
id2 = acceptor.client_id
|
61
|
+
@transfer_id = Transfer::gen_transfer_id id1, id2, @url, @byte_range
|
62
|
+
end
|
63
|
+
|
64
|
+
# Update internal data upon a successful transfer
|
65
|
+
def success
|
66
|
+
giver.success self
|
67
|
+
taker.success self
|
68
|
+
end
|
69
|
+
|
70
|
+
# Update internal data upon a failed transfer
|
71
|
+
def failure
|
72
|
+
taker.failure self
|
61
73
|
end
|
62
74
|
|
63
75
|
def to_s
|
@@ -67,7 +79,7 @@ module PDTP
|
|
67
79
|
def debug_str
|
68
80
|
str = ''
|
69
81
|
str << "to_s=#{to_s}"
|
70
|
-
str << " taker_id=#{@taker.
|
82
|
+
str << " taker_id=#{@taker.client_id} giver_id=#{@giver.client_id}"
|
71
83
|
end
|
72
84
|
end
|
73
85
|
end
|