mogilefs-client 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/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
+