mogilefs-client 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +32 -0
- data/Manifest.txt +18 -0
- data/README +9 -0
- data/Rakefile +56 -0
- data/lib/mogilefs.rb +24 -0
- data/lib/mogilefs/admin.rb +274 -0
- data/lib/mogilefs/backend.rb +219 -0
- data/lib/mogilefs/client.rb +64 -0
- data/lib/mogilefs/httpfile.rb +144 -0
- data/lib/mogilefs/mogilefs.rb +214 -0
- data/lib/mogilefs/nfsfile.rb +81 -0
- data/lib/mogilefs/pool.rb +50 -0
- data/test/setup.rb +55 -0
- data/test/test_admin.rb +37 -0
- data/test/test_backend.rb +208 -0
- data/test/test_client.rb +49 -0
- data/test/test_mogilefs.rb +123 -0
- data/test/test_pool.rb +98 -0
- metadata +63 -0
data/LICENSE
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
Portions copyright 2004 David Heinemeier Hansson.
|
2
|
+
|
3
|
+
All original code copyright 2005 Eric Hodel, The Robot Co-op. All rights
|
4
|
+
reserved.
|
5
|
+
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
7
|
+
modification, are permitted provided that the following conditions
|
8
|
+
are met:
|
9
|
+
|
10
|
+
1. Redistributions of source code must retain the above copyright
|
11
|
+
notice, this list of conditions and the following disclaimer.
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright
|
13
|
+
notice, this list of conditions and the following disclaimer in the
|
14
|
+
documentation and/or other materials provided with the distribution.
|
15
|
+
3. Neither the names of the authors nor the names of their contributors
|
16
|
+
may be used to endorse or promote products derived from this software
|
17
|
+
without specific prior written permission.
|
18
|
+
4. Redistribution in Rails or any sub-projects of Rails is not allowed
|
19
|
+
until Rails runs without warnings with the ``-W2'' flag enabled.
|
20
|
+
|
21
|
+
THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS
|
22
|
+
OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
23
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
24
|
+
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE
|
25
|
+
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
|
26
|
+
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
|
27
|
+
OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
28
|
+
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
29
|
+
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
30
|
+
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
31
|
+
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
32
|
+
|
data/Manifest.txt
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
LICENSE
|
2
|
+
Manifest.txt
|
3
|
+
README
|
4
|
+
Rakefile
|
5
|
+
lib/mogilefs.rb
|
6
|
+
lib/mogilefs/admin.rb
|
7
|
+
lib/mogilefs/backend.rb
|
8
|
+
lib/mogilefs/client.rb
|
9
|
+
lib/mogilefs/httpfile.rb
|
10
|
+
lib/mogilefs/mogilefs.rb
|
11
|
+
lib/mogilefs/nfsfile.rb
|
12
|
+
lib/mogilefs/pool.rb
|
13
|
+
test/setup.rb
|
14
|
+
test/test_admin.rb
|
15
|
+
test/test_backend.rb
|
16
|
+
test/test_client.rb
|
17
|
+
test/test_mogilefs.rb
|
18
|
+
test/test_pool.rb
|
data/README
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
This is a client for Danga's MogileFS file store. For information on MogileFS
|
2
|
+
see:
|
3
|
+
|
4
|
+
http://danga.com/mogilefs/
|
5
|
+
|
6
|
+
This client is only known to work in NFS mode. HTTP mode is implemented but completely untested. If you find a bug, please report it in an email to:
|
7
|
+
|
8
|
+
eric@robotcoop.com.
|
9
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/testtask'
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
require 'rake/gempackagetask'
|
6
|
+
|
7
|
+
$VERBOSE = nil
|
8
|
+
|
9
|
+
spec = Gem::Specification.new do |s|
|
10
|
+
s.name = 'mogilefs-client'
|
11
|
+
s.version = '1.0.1'
|
12
|
+
s.summary = 'A Ruby MogileFS client'
|
13
|
+
s.description = 'A Ruby MogileFS client. MogileFS is a distributed filesystem written by Danga Interactive. This client supports NFS mode and has untested support for HTTP mode.'
|
14
|
+
s.author = 'Eric Hodel'
|
15
|
+
s.email = 'eric@robotcoop.com'
|
16
|
+
|
17
|
+
s.has_rdoc = true
|
18
|
+
s.files = File.read('Manifest.txt').split($/)
|
19
|
+
s.require_path = 'lib'
|
20
|
+
end
|
21
|
+
|
22
|
+
desc 'Run tests'
|
23
|
+
task :default => [ :test ]
|
24
|
+
|
25
|
+
Rake::TestTask.new('test') do |t|
|
26
|
+
t.libs << 'test'
|
27
|
+
t.pattern = 'test/test_*.rb'
|
28
|
+
t.verbose = true
|
29
|
+
end
|
30
|
+
|
31
|
+
desc 'Update Manifest.txt'
|
32
|
+
task :update_manifest do
|
33
|
+
sh "find . -type f | sed -e 's%./%%' | egrep -v 'svn|swp|~' | egrep -v '^(doc|pkg)/' | sort > Manifest.txt"
|
34
|
+
end
|
35
|
+
|
36
|
+
desc 'Generate RDoc'
|
37
|
+
Rake::RDocTask.new :rdoc do |rd|
|
38
|
+
rd.rdoc_dir = 'doc'
|
39
|
+
rd.rdoc_files.add 'lib', 'README', 'LICENSE'
|
40
|
+
rd.main = 'README'
|
41
|
+
rd.options << '-d' if `which dot` =~ /\/dot/
|
42
|
+
end
|
43
|
+
|
44
|
+
desc 'Build Gem'
|
45
|
+
Rake::GemPackageTask.new spec do |pkg|
|
46
|
+
pkg.need_tar = true
|
47
|
+
end
|
48
|
+
|
49
|
+
desc 'Clean up'
|
50
|
+
task :clean => [ :clobber_rdoc, :clobber_package ]
|
51
|
+
|
52
|
+
desc 'Clean up'
|
53
|
+
task :clobber => [ :clean ]
|
54
|
+
|
55
|
+
# vim: syntax=Ruby
|
56
|
+
|
data/lib/mogilefs.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
##
|
2
|
+
# MogileFS is a Ruby client for Danga Interactive's open source distributed
|
3
|
+
# filesystem.
|
4
|
+
#
|
5
|
+
# To read more about Danga's MogileFS: http://danga.com/mogilefs/
|
6
|
+
|
7
|
+
module MogileFS
|
8
|
+
|
9
|
+
##
|
10
|
+
# Raised when a socket remains unreadable for too long.
|
11
|
+
|
12
|
+
class UnreadableSocketError < RuntimeError; end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
require 'socket'
|
17
|
+
|
18
|
+
require 'mogilefs/backend'
|
19
|
+
require 'mogilefs/nfsfile'
|
20
|
+
require 'mogilefs/httpfile'
|
21
|
+
require 'mogilefs/client'
|
22
|
+
require 'mogilefs/mogilefs'
|
23
|
+
require 'mogilefs/admin'
|
24
|
+
|
@@ -0,0 +1,274 @@
|
|
1
|
+
require 'mogilefs/client'
|
2
|
+
|
3
|
+
##
|
4
|
+
# A MogileFS Administration Client
|
5
|
+
|
6
|
+
class MogileFS::Admin < MogileFS::Client
|
7
|
+
|
8
|
+
##
|
9
|
+
# Returns an Array of host status Hashes. If +hostid+ is given only that
|
10
|
+
# host is returned.
|
11
|
+
#
|
12
|
+
# admin.get_hosts 1
|
13
|
+
#
|
14
|
+
# Returns:
|
15
|
+
#
|
16
|
+
# [{"status"=>"alive",
|
17
|
+
# "http_get_port"=>"",
|
18
|
+
# "http_port"=>"",
|
19
|
+
# "hostid"=>"1",
|
20
|
+
# "hostip"=>"",
|
21
|
+
# "hostname"=>"rur-1",
|
22
|
+
# "remoteroot"=>"/mnt/mogilefs/rur-1",
|
23
|
+
# "altip"=>"",
|
24
|
+
# "altmask"=>""}]
|
25
|
+
|
26
|
+
def get_hosts(hostid = nil)
|
27
|
+
args = hostid ? { :hostid => hostid } : {}
|
28
|
+
res = @backend.get_hosts args
|
29
|
+
return clean('hosts', 'host', res)
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# Returns an Array of device status Hashes. If devid is given only that
|
34
|
+
# device is returned.
|
35
|
+
#
|
36
|
+
# admin.get_devices 1
|
37
|
+
#
|
38
|
+
# Returns:
|
39
|
+
#
|
40
|
+
# [{"status"=>"alive",
|
41
|
+
# "mb_asof"=>"",
|
42
|
+
# "mb_free"=>"0",
|
43
|
+
# "devid"=>"1",
|
44
|
+
# "hostid"=>"1",
|
45
|
+
# "mb_used"=>"",
|
46
|
+
# "mb_total"=>""}]
|
47
|
+
|
48
|
+
def get_devices(devid = nil)
|
49
|
+
args = devid ? { :devid => devid } : {}
|
50
|
+
res = @backend.get_devices args
|
51
|
+
return clean('devices', 'dev', res)
|
52
|
+
end
|
53
|
+
|
54
|
+
##
|
55
|
+
# Returns an Array of fid Hashes from +from_fid+ to +to_fid+.
|
56
|
+
#
|
57
|
+
# admin.list_fids 0, 100
|
58
|
+
#
|
59
|
+
# Returns:
|
60
|
+
#
|
61
|
+
# [{"fid"=>"99",
|
62
|
+
# "class"=>"normal",
|
63
|
+
# "domain"=>"test",
|
64
|
+
# "devcount"=>"2",
|
65
|
+
# "length"=>"4",
|
66
|
+
# "key"=>"file_key"},
|
67
|
+
# {"fid"=>"82",
|
68
|
+
# "class"=>"normal",
|
69
|
+
# "devcount"=>"2",
|
70
|
+
# "domain"=>"test",
|
71
|
+
# "length"=>"9",
|
72
|
+
# "key"=>"new_new_key"}]
|
73
|
+
|
74
|
+
def list_fids(from_fid, to_fid)
|
75
|
+
res = @backend.list_fids :from => from_fid, :to => to_fid
|
76
|
+
return clean('fid_count', 'fid_', res)
|
77
|
+
end
|
78
|
+
|
79
|
+
##
|
80
|
+
# Returns a statistics structure representing the state of mogilefs.
|
81
|
+
#
|
82
|
+
# admin.get_stats
|
83
|
+
#
|
84
|
+
# Returns:
|
85
|
+
#
|
86
|
+
# {"fids"=>{"max"=>"99", "count"=>"2"},
|
87
|
+
# "device"=>
|
88
|
+
# [{"status"=>"alive", "files"=>"2", "id"=>"1", "host"=>"rur-1"},
|
89
|
+
# {"status"=>"alive", "files"=>"2", "id"=>"2", "host"=>"rur-2"}],
|
90
|
+
# "replication"=>
|
91
|
+
# [{"files"=>"2", "class"=>"normal", "devcount"=>"2", "domain"=>"test"}],
|
92
|
+
# "file"=>[{"files"=>"2", "class"=>"normal", "domain"=>"test"}]}
|
93
|
+
|
94
|
+
def get_stats(type = 'all')
|
95
|
+
res = @backend.stats type => 1
|
96
|
+
stats = {}
|
97
|
+
|
98
|
+
stats['replication'] = clean 'replicationcount', 'replication', res, false
|
99
|
+
stats['file'] = clean 'filescount', 'files', res, false
|
100
|
+
stats['device'] = clean 'devicescount', 'devices', res, false
|
101
|
+
|
102
|
+
if res['fidmax'] or res['fidcount'] then
|
103
|
+
stats['fids'] = { 'max' => res['fidmax'], 'count' => res['fidcount'] }
|
104
|
+
end
|
105
|
+
|
106
|
+
return stats
|
107
|
+
end
|
108
|
+
|
109
|
+
##
|
110
|
+
# Returns the domains present in the mogilefs.
|
111
|
+
#
|
112
|
+
# admin.get_domains
|
113
|
+
#
|
114
|
+
# Returns:
|
115
|
+
#
|
116
|
+
# {"test"=>{"normal"=>"2", "default"=>"2"}}
|
117
|
+
|
118
|
+
def get_domains
|
119
|
+
res = @backend.get_domains
|
120
|
+
|
121
|
+
domains = {}
|
122
|
+
(1..res['domains'].to_i).each do |i|
|
123
|
+
domain = clean "domain#{i}classes", "domain#{i}class", res, false
|
124
|
+
domains[res["domain#{i}"]] = Hash[*domain.map { |d| d.values}.flatten]
|
125
|
+
end
|
126
|
+
|
127
|
+
return domains
|
128
|
+
end
|
129
|
+
|
130
|
+
##
|
131
|
+
# Creates a new domain named +domain+. Returns nil if creation failed.
|
132
|
+
|
133
|
+
def create_domain(domain)
|
134
|
+
raise 'readonly mogilefs' if readonly?
|
135
|
+
res = @backend.create_domain :domain => domain
|
136
|
+
return res['domain'] unless res.nil?
|
137
|
+
end
|
138
|
+
|
139
|
+
##
|
140
|
+
# Deletes +domain+. Returns true if successful, false if not.
|
141
|
+
|
142
|
+
def delete_domain(domain)
|
143
|
+
raise 'readonly mogilefs' if readonly?
|
144
|
+
res = @backend.delete_domain :domain => domain
|
145
|
+
return !res.nil?
|
146
|
+
end
|
147
|
+
|
148
|
+
##
|
149
|
+
# Creates a new class in +domain+ named +klass+ with files replicated to
|
150
|
+
# +mindevcount+ devices. Returns nil on failure.
|
151
|
+
|
152
|
+
def create_class(domain, klass, mindevcount)
|
153
|
+
return modify_class(domain, klass, mindevcount, :create)
|
154
|
+
end
|
155
|
+
|
156
|
+
##
|
157
|
+
# Updates class +klass+ in +domain+ to be replicated to +mindevcount+
|
158
|
+
# devices. Returns nil on failure.
|
159
|
+
|
160
|
+
def update_class(domain, klass, mindevcount)
|
161
|
+
return modify_class(domain, klass, mindevcount, :update)
|
162
|
+
end
|
163
|
+
|
164
|
+
##
|
165
|
+
# Removes class +klass+ from +domain+. Returns true if successful, false if
|
166
|
+
# not.
|
167
|
+
|
168
|
+
def delete_class(domain, klass)
|
169
|
+
res = @backend.delete_class :domain => domain, :class => klass
|
170
|
+
return !res.nil?
|
171
|
+
end
|
172
|
+
|
173
|
+
##
|
174
|
+
# Creates a new host named +host+. +args+ must contain :ip and :port.
|
175
|
+
# Returns true if successful, false if not.
|
176
|
+
|
177
|
+
def create_host(host, args = {})
|
178
|
+
raise ArgumentError, "Must specify ip and port" unless \
|
179
|
+
args.include? :ip and args.include? :port
|
180
|
+
|
181
|
+
return modify_host(host, args, 'create')
|
182
|
+
end
|
183
|
+
|
184
|
+
##
|
185
|
+
# Updates +host+ with +args+. Returns true if successful, false if not.
|
186
|
+
|
187
|
+
def update_host(host, args = {})
|
188
|
+
return modify_host(host, args, 'update')
|
189
|
+
end
|
190
|
+
|
191
|
+
##
|
192
|
+
# Deletes host +host+. Returns nil on failure.
|
193
|
+
|
194
|
+
def delete_host(host)
|
195
|
+
raise 'readonly mogilefs' if readonly?
|
196
|
+
res = @backend.delete_host :host => host
|
197
|
+
return !res.nil?
|
198
|
+
end
|
199
|
+
|
200
|
+
##
|
201
|
+
# Changes the device status of +device+ on +host+ to +state+ which can be
|
202
|
+
# 'alive', 'down', or 'dead'.
|
203
|
+
|
204
|
+
def change_device_state(host, device, state)
|
205
|
+
raise 'readonly mogilefs' if readonly?
|
206
|
+
res = @backend.set_state :host => host, :device => device, :state => state
|
207
|
+
return !res.nil?
|
208
|
+
end
|
209
|
+
|
210
|
+
protected unless defined? $TESTING
|
211
|
+
|
212
|
+
##
|
213
|
+
# Modifies +klass+ on +domain+ to store files on +mindevcount+ devices via
|
214
|
+
# +action+. Returns the class name if successful, nil if not.
|
215
|
+
|
216
|
+
def modify_class(domain, klass, mindevcount, action)
|
217
|
+
raise 'readonly mogilefs' if readonly?
|
218
|
+
res = @backend.send("#{action}_class", :domain => domain, :class => klass,
|
219
|
+
:mindevcount => mindevcount)
|
220
|
+
|
221
|
+
return res['class'] unless res.nil?
|
222
|
+
end
|
223
|
+
|
224
|
+
##
|
225
|
+
# Modifies +host+ using +args+ via +action+. Returns true if successful,
|
226
|
+
# false if not.
|
227
|
+
|
228
|
+
def modify_host(host, args = {}, action = 'create')
|
229
|
+
args[:host] = host
|
230
|
+
res = @backend.send "#{action}_host", args
|
231
|
+
return !res.nil?
|
232
|
+
end
|
233
|
+
|
234
|
+
##
|
235
|
+
# Turns the response +res+ from the backend into an Array of Hashes from 1
|
236
|
+
# to res[+count+]. If +underscore+ is true then a '_' character is assumed
|
237
|
+
# between the prefix and the hash key value.
|
238
|
+
#
|
239
|
+
# res = {"host1_remoteroot"=>"/mnt/mogilefs/rur-1",
|
240
|
+
# "host1_hostname"=>"rur-1",
|
241
|
+
# "host1_hostid"=>"1",
|
242
|
+
# "host1_http_get_port"=>"",
|
243
|
+
# "host1_altip"=>"",
|
244
|
+
# "hosts"=>"1",
|
245
|
+
# "host1_hostip"=>"",
|
246
|
+
# "host1_http_port"=>"",
|
247
|
+
# "host1_status"=>"alive",
|
248
|
+
# "host1_altmask"=>""}
|
249
|
+
# admin.clean 'hosts', 'host', res
|
250
|
+
#
|
251
|
+
# Returns:
|
252
|
+
#
|
253
|
+
# [{"status"=>"alive",
|
254
|
+
# "http_get_port"=>"",
|
255
|
+
# "http_port"=>"",
|
256
|
+
# "hostid"=>"1",
|
257
|
+
# "hostip"=>"",
|
258
|
+
# "hostname"=>"rur-1",
|
259
|
+
# "remoteroot"=>"/mnt/mogilefs/rur-1",
|
260
|
+
# "altip"=>"",
|
261
|
+
# "altmask"=>""}]
|
262
|
+
|
263
|
+
def clean(count, prefix, res, underscore = true)
|
264
|
+
underscore = underscore ? '_' : ''
|
265
|
+
return (1..res[count].to_i).map do |i|
|
266
|
+
dev = res.select { |k,_| k =~ /^#{prefix}#{i}#{underscore}/ }.map do |k,v|
|
267
|
+
[k.sub(/^#{prefix}#{i}#{underscore}/, ''), v]
|
268
|
+
end
|
269
|
+
Hash[*dev.flatten]
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
end
|
274
|
+
|
@@ -0,0 +1,219 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'thread'
|
3
|
+
require 'mogilefs'
|
4
|
+
|
5
|
+
##
|
6
|
+
# MogileFS::Backend communicates with the MogileFS trackers.
|
7
|
+
|
8
|
+
class MogileFS::Backend
|
9
|
+
|
10
|
+
##
|
11
|
+
# Adds MogileFS commands +names+.
|
12
|
+
|
13
|
+
def self.add_command(*names)
|
14
|
+
names.each do |name|
|
15
|
+
define_method name do |*args|
|
16
|
+
do_request name, args.first || {}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
##
|
22
|
+
# The last error
|
23
|
+
#--
|
24
|
+
# TODO Use Exceptions
|
25
|
+
|
26
|
+
attr_reader :lasterr
|
27
|
+
|
28
|
+
##
|
29
|
+
# The string attached to the last error
|
30
|
+
#--
|
31
|
+
# TODO Use Exceptions
|
32
|
+
|
33
|
+
attr_reader :lasterrstr
|
34
|
+
|
35
|
+
##
|
36
|
+
# Creates a new MogileFS::Backend.
|
37
|
+
#
|
38
|
+
# :hosts is a required argument and must be an Array containing one or more
|
39
|
+
# 'hostname:port' pairs as Strings.
|
40
|
+
#
|
41
|
+
# :timeout adjusts the request timeout before an error is returned.
|
42
|
+
|
43
|
+
def initialize(args)
|
44
|
+
@hosts = args[:hosts]
|
45
|
+
raise ArgumentError, "must specify at least one host" unless @hosts
|
46
|
+
raise ArgumentError, "must specify at least one host" if @hosts.empty?
|
47
|
+
unless @hosts == @hosts.select { |h| h =~ /:\d+$/ } then
|
48
|
+
raise ArgumentError, ":hosts must be in 'host:port' form"
|
49
|
+
end
|
50
|
+
|
51
|
+
@mutex = Mutex.new
|
52
|
+
@timeout = args[:timeout] || 3
|
53
|
+
@socket = nil
|
54
|
+
@lasterr = nil
|
55
|
+
@lasterrstr = nil
|
56
|
+
|
57
|
+
@dead = {}
|
58
|
+
end
|
59
|
+
|
60
|
+
##
|
61
|
+
# Closes this backend's socket.
|
62
|
+
|
63
|
+
def shutdown
|
64
|
+
@socket.close unless @socket.nil? or @socket.closed?
|
65
|
+
@socket = nil
|
66
|
+
end
|
67
|
+
|
68
|
+
# MogileFS::MogileFS commands
|
69
|
+
|
70
|
+
add_command :create_open
|
71
|
+
add_command :create_close
|
72
|
+
add_command :get_paths
|
73
|
+
add_command :delete
|
74
|
+
add_command :sleep
|
75
|
+
add_command :rename
|
76
|
+
add_command :list_keys
|
77
|
+
|
78
|
+
# MogileFS::Backend commands
|
79
|
+
|
80
|
+
add_command :get_hosts
|
81
|
+
add_command :get_devices
|
82
|
+
add_command :list_fids
|
83
|
+
add_command :stats
|
84
|
+
add_command :get_domains
|
85
|
+
add_command :create_domain
|
86
|
+
add_command :delete_domain
|
87
|
+
add_command :create_class
|
88
|
+
add_command :update_class
|
89
|
+
add_command :delete_class
|
90
|
+
add_command :create_host
|
91
|
+
add_command :update_host
|
92
|
+
add_command :delete_host
|
93
|
+
add_command :set_state
|
94
|
+
|
95
|
+
private unless defined? $TESTING
|
96
|
+
|
97
|
+
##
|
98
|
+
# Returns a new TCPSocket connected to +port+ on +host+.
|
99
|
+
|
100
|
+
def connect_to(host, port)
|
101
|
+
return TCPSocket.new(host, port)
|
102
|
+
end
|
103
|
+
|
104
|
+
##
|
105
|
+
# Performs the +cmd+ request with +args+.
|
106
|
+
|
107
|
+
def do_request(cmd, args)
|
108
|
+
@mutex.synchronize do
|
109
|
+
request = make_request cmd, args
|
110
|
+
|
111
|
+
begin
|
112
|
+
bytes_sent = socket.send request, 0
|
113
|
+
rescue SystemCallError
|
114
|
+
@socket = nil
|
115
|
+
raise "couldn't connect to mogilefsd backend"
|
116
|
+
end
|
117
|
+
|
118
|
+
unless bytes_sent == request.length then
|
119
|
+
raise "request truncated (sent #{bytes_sent} expected #{request.length})"
|
120
|
+
end
|
121
|
+
|
122
|
+
readable?
|
123
|
+
|
124
|
+
return parse_response(socket.gets)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
##
|
129
|
+
# Makes a new request string for +cmd+ and +args+.
|
130
|
+
|
131
|
+
def make_request(cmd, args)
|
132
|
+
return "#{cmd} #{url_encode args}\r\n"
|
133
|
+
end
|
134
|
+
|
135
|
+
##
|
136
|
+
# Turns the +line+ response from the server into a Hash of options, an
|
137
|
+
# error, or raises, as appropriate.
|
138
|
+
|
139
|
+
def parse_response(line)
|
140
|
+
if line =~ /^ERR\s+(\w+)\s*(.*)/ then
|
141
|
+
@lasterr = $1
|
142
|
+
@lasterrstr = $2 ? url_unescape($2) : nil
|
143
|
+
return nil
|
144
|
+
end
|
145
|
+
|
146
|
+
return url_decode($1) if line =~ /^OK\s+\d*\s*(\S*)/
|
147
|
+
|
148
|
+
raise "Invalid response from server: #{line.inspect}"
|
149
|
+
end
|
150
|
+
|
151
|
+
##
|
152
|
+
# Raises if the socket does not become readable in +@timeout+ seconds.
|
153
|
+
|
154
|
+
def readable?
|
155
|
+
found = select [socket], nil, nil, @timeout
|
156
|
+
raise MogileFS::UnreadableSocketError if found.nil? or found.empty?
|
157
|
+
return true
|
158
|
+
end
|
159
|
+
|
160
|
+
##
|
161
|
+
# Returns a socket connected to a MogileFS tracker.
|
162
|
+
|
163
|
+
def socket
|
164
|
+
return @socket if @socket and not @socket.closed?
|
165
|
+
|
166
|
+
now = Time.now
|
167
|
+
|
168
|
+
@hosts.sort_by { rand(3) - 1 }.each do |host|
|
169
|
+
next if @dead.include? host and @dead[host] > now - 5
|
170
|
+
|
171
|
+
begin
|
172
|
+
@socket = connect_to(*host.split(':'))
|
173
|
+
rescue SystemCallError
|
174
|
+
@dead[host] = now
|
175
|
+
next
|
176
|
+
end
|
177
|
+
|
178
|
+
return @socket
|
179
|
+
end
|
180
|
+
|
181
|
+
raise "couldn't connect to mogilefsd backend"
|
182
|
+
end
|
183
|
+
|
184
|
+
##
|
185
|
+
# Turns a url params string into a Hash.
|
186
|
+
|
187
|
+
def url_decode(str)
|
188
|
+
pairs = str.split('&').map do |pair|
|
189
|
+
pair.split('=', 2).map { |v| url_unescape v }
|
190
|
+
end
|
191
|
+
|
192
|
+
return Hash[*pairs.flatten]
|
193
|
+
end
|
194
|
+
|
195
|
+
##
|
196
|
+
# Turns a Hash (or Array of pairs) into a url params string.
|
197
|
+
|
198
|
+
def url_encode(params)
|
199
|
+
return params.map do |k,v|
|
200
|
+
"#{url_escape k.to_s}=#{url_escape v.to_s}"
|
201
|
+
end.join("&")
|
202
|
+
end
|
203
|
+
|
204
|
+
##
|
205
|
+
# Escapes naughty URL characters.
|
206
|
+
|
207
|
+
def url_escape(str)
|
208
|
+
return str.gsub(/([^\w\,\-.\/\\\: ])/) { "%%%02x" % $1[0] }.tr(' ', '+')
|
209
|
+
end
|
210
|
+
|
211
|
+
##
|
212
|
+
# Unescapes naughty URL characters.
|
213
|
+
|
214
|
+
def url_unescape(str)
|
215
|
+
return str.gsub(/%([a-f0-9][a-f0-9])/i) { [$1.to_i(16)].pack 'C' }.tr('+', ' ')
|
216
|
+
end
|
217
|
+
|
218
|
+
end
|
219
|
+
|