mogilefs-client 1.0.1

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