mogilefs-client 1.3.1 → 2.0.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.
@@ -1,6 +1,6 @@
1
- require 'socket'
2
- require 'thread'
3
1
  require 'mogilefs'
2
+ require 'mogilefs/util'
3
+ require 'thread'
4
4
 
5
5
  ##
6
6
  # MogileFS::Backend communicates with the MogileFS trackers.
@@ -18,17 +18,29 @@ class MogileFS::Backend
18
18
  end
19
19
  end
20
20
 
21
+ BACKEND_ERRORS = {}
22
+
23
+ # this converts an error code from a mogilefsd tracker to an exception:
24
+ #
25
+ # Examples of some exceptions that get created:
26
+ # class AfterMismatchError < MogileFS::Error; end
27
+ # class DomainNotFoundError < MogileFS::Error; end
28
+ # class InvalidCharsError < MogileFS::Error; end
29
+ def self.add_error(err_snake)
30
+ err_camel = err_snake.gsub(/(?:^|_)([a-z])/) { $1.upcase } << 'Error'
31
+ unless self.const_defined?(err_camel)
32
+ self.class_eval("class #{err_camel} < MogileFS::Error; end")
33
+ end
34
+ BACKEND_ERRORS[err_snake] = self.const_get(err_camel)
35
+ end
36
+
21
37
  ##
22
38
  # The last error
23
- #--
24
- # TODO Use Exceptions
25
39
 
26
40
  attr_reader :lasterr
27
41
 
28
42
  ##
29
43
  # The string attached to the last error
30
- #--
31
- # TODO Use Exceptions
32
44
 
33
45
  attr_reader :lasterrstr
34
46
 
@@ -61,8 +73,10 @@ class MogileFS::Backend
61
73
  # Closes this backend's socket.
62
74
 
63
75
  def shutdown
64
- @socket.close unless @socket.nil? or @socket.closed?
65
- @socket = nil
76
+ if @socket
77
+ @socket.close rescue nil # ignore errors
78
+ @socket = nil
79
+ end
66
80
  end
67
81
 
68
82
  # MogileFS::MogileFS commands
@@ -76,7 +90,7 @@ class MogileFS::Backend
76
90
  add_command :list_keys
77
91
 
78
92
  # MogileFS::Backend commands
79
-
93
+
80
94
  add_command :get_hosts
81
95
  add_command :get_devices
82
96
  add_command :list_fids
@@ -92,14 +106,44 @@ class MogileFS::Backend
92
106
  add_command :delete_host
93
107
  add_command :set_state
94
108
 
95
- private unless defined? $TESTING
109
+ # Errors copied from MogileFS/Worker/Query.pm
110
+ add_error 'dup'
111
+ add_error 'after_mismatch'
112
+ add_error 'bad_params'
113
+ add_error 'class_exists'
114
+ add_error 'class_has_files'
115
+ add_error 'class_not_found'
116
+ add_error 'db'
117
+ add_error 'domain_has_files'
118
+ add_error 'domain_exists'
119
+ add_error 'domain_not_empty'
120
+ add_error 'domain_not_found'
121
+ add_error 'failure'
122
+ add_error 'host_exists'
123
+ add_error 'host_mismatch'
124
+ add_error 'host_not_empty'
125
+ add_error 'host_not_found'
126
+ add_error 'invalid_chars'
127
+ add_error 'invalid_checker_level'
128
+ add_error 'invalid_mindevcount'
129
+ add_error 'key_exists'
130
+ add_error 'no_class'
131
+ add_error 'no_devices'
132
+ add_error 'no_domain'
133
+ add_error 'no_host'
134
+ add_error 'no_ip'
135
+ add_error 'no_key'
136
+ add_error 'no_port'
137
+ add_error 'none_match'
138
+ add_error 'plugin_aborted'
139
+ add_error 'state_too_high'
140
+ add_error 'unknown_command'
141
+ add_error 'unknown_host'
142
+ add_error 'unknown_key'
143
+ add_error 'unknown_state'
144
+ add_error 'unreg_domain'
96
145
 
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
146
+ private unless defined? $TESTING
103
147
 
104
148
  ##
105
149
  # Performs the +cmd+ request with +args+.
@@ -111,17 +155,18 @@ class MogileFS::Backend
111
155
  begin
112
156
  bytes_sent = socket.send request, 0
113
157
  rescue SystemCallError
114
- @socket = nil
115
- raise "couldn't connect to mogilefsd backend"
158
+ shutdown
159
+ raise MogileFS::UnreachableBackendError
116
160
  end
117
161
 
118
162
  unless bytes_sent == request.length then
119
- raise "request truncated (sent #{bytes_sent} expected #{request.length})"
163
+ raise MogileFS::RequestTruncatedError,
164
+ "request truncated (sent #{bytes_sent} expected #{request.length})"
120
165
  end
121
166
 
122
167
  readable?
123
168
 
124
- return parse_response(socket.gets)
169
+ parse_response(socket.gets)
125
170
  end
126
171
  end
127
172
 
@@ -129,7 +174,15 @@ class MogileFS::Backend
129
174
  # Makes a new request string for +cmd+ and +args+.
130
175
 
131
176
  def make_request(cmd, args)
132
- return "#{cmd} #{url_encode args}\r\n"
177
+ "#{cmd} #{url_encode args}\r\n"
178
+ end
179
+
180
+ # this converts an error code from a mogilefsd tracker to an exception
181
+ # Most of these exceptions should already be defined, but since the
182
+ # MogileFS server code is liable to change and we may not always be
183
+ # able to keep up with the changes
184
+ def error(err_snake)
185
+ BACKEND_ERRORS[err_snake] || self.class.add_error(err_snake)
133
186
  end
134
187
 
135
188
  ##
@@ -140,25 +193,41 @@ class MogileFS::Backend
140
193
  if line =~ /^ERR\s+(\w+)\s*(.*)/ then
141
194
  @lasterr = $1
142
195
  @lasterrstr = $2 ? url_unescape($2) : nil
196
+ raise error(@lasterr)
143
197
  return nil
144
198
  end
145
199
 
146
200
  return url_decode($1) if line =~ /^OK\s+\d*\s*(\S*)/
147
201
 
148
- raise "Invalid response from server: #{line.inspect}"
202
+ raise MogileFS::InvalidResponseError,
203
+ "Invalid response from server: #{line.inspect}"
149
204
  end
150
205
 
151
206
  ##
152
207
  # Raises if the socket does not become readable in +@timeout+ seconds.
153
208
 
154
209
  def readable?
155
- found = select [socket], nil, nil, @timeout
156
- if found.nil? or found.empty? then
157
- peer = (@socket ? "#{@socket.peeraddr[3]}:#{@socket.peeraddr[1]} " : nil)
158
- socket.close # we DO NOT want the response we timed out waiting for, to crop up later on, on the same socket, intersperesed with a subsequent request! so, we close the socket if it times out like this
159
- raise MogileFS::UnreadableSocketError, "#{peer}never became readable"
210
+ timeleft = @timeout
211
+ peer = nil
212
+ loop do
213
+ t0 = Time.now
214
+ found = IO.select([socket], nil, nil, timeleft)
215
+ return true if found && found[0]
216
+ timeleft -= (Time.now - t0)
217
+
218
+ if timeleft < 0
219
+ peer = @socket ? "#{@socket.mogilefs_peername} " : nil
220
+
221
+ # we DO NOT want the response we timed out waiting for, to crop up later
222
+ # on, on the same socket, intersperesed with a subsequent request! so,
223
+ # we close the socket if it times out like this
224
+ shutdown
225
+ raise MogileFS::UnreadableSocketError, "#{peer}never became readable"
226
+ break
227
+ end
228
+ shutdown
160
229
  end
161
- return true
230
+ false
162
231
  end
163
232
 
164
233
  ##
@@ -173,8 +242,8 @@ class MogileFS::Backend
173
242
  next if @dead.include? host and @dead[host] > now - 5
174
243
 
175
244
  begin
176
- @socket = connect_to(*host.split(':'))
177
- rescue SystemCallError
245
+ @socket = Socket.mogilefs_new(*(host.split(/:/) << @timeout))
246
+ rescue SystemCallError, MogileFS::Timeout
178
247
  @dead[host] = now
179
248
  next
180
249
  end
@@ -182,25 +251,23 @@ class MogileFS::Backend
182
251
  return @socket
183
252
  end
184
253
 
185
- raise "couldn't connect to mogilefsd backend"
254
+ raise MogileFS::UnreachableBackendError
186
255
  end
187
256
 
188
257
  ##
189
258
  # Turns a url params string into a Hash.
190
259
 
191
260
  def url_decode(str)
192
- pairs = str.split('&').map do |pair|
193
- pair.split('=', 2).map { |v| url_unescape v }
194
- end
195
-
196
- return Hash[*pairs.flatten]
261
+ Hash[*(str.split(/&/).map { |pair|
262
+ pair.split(/=/, 2).map { |x| url_unescape(x) }
263
+ } ).flatten]
197
264
  end
198
265
 
199
266
  ##
200
267
  # Turns a Hash (or Array of pairs) into a url params string.
201
268
 
202
269
  def url_encode(params)
203
- return params.map do |k,v|
270
+ params.map do |k,v|
204
271
  "#{url_escape k.to_s}=#{url_escape v.to_s}"
205
272
  end.join("&")
206
273
  end
@@ -209,14 +276,14 @@ class MogileFS::Backend
209
276
  # Escapes naughty URL characters.
210
277
 
211
278
  def url_escape(str)
212
- return str.gsub(/([^\w\,\-.\/\\\: ])/) { "%%%02x" % $1[0] }.tr(' ', '+')
279
+ str.gsub(/([^\w\,\-.\/\\\: ])/) { "%%%02x" % $1[0] }.tr(' ', '+')
213
280
  end
214
281
 
215
282
  ##
216
283
  # Unescapes naughty URL characters.
217
284
 
218
285
  def url_unescape(str)
219
- return str.gsub(/%([a-f0-9][a-f0-9])/i) { [$1.to_i(16)].pack 'C' }.tr('+', ' ')
286
+ str.gsub(/%([a-f0-9][a-f0-9])/i) { [$1.to_i(16)].pack 'C' }.tr('+', ' ')
220
287
  end
221
288
 
222
289
  end
@@ -0,0 +1,153 @@
1
+ require 'zlib'
2
+ require 'digest/md5'
3
+ require 'uri'
4
+ require 'mogilefs/util'
5
+
6
+ module MogileFS::Bigfile
7
+ GZIP_HEADER = "\x1f\x8b".freeze # mogtool(1) has this
8
+ # VALID_TYPES = %w(file tarball partition).map { |x| x.freeze }.freeze
9
+
10
+ # returns a big_info hash if successful
11
+ def bigfile_stat(key)
12
+ parse_info(get_file_data(key))
13
+ end
14
+
15
+ # returns total bytes written and the big_info hash if successful, raises an
16
+ # exception if not wr_io is expected to be an IO-like object capable of
17
+ # receiving the syswrite method.
18
+ def bigfile_write(key, wr_io, opts = { :verify => false })
19
+ info = bigfile_stat(key)
20
+ zi = nil
21
+ md5 = opts[:verify] ? Digest::MD5.new : nil
22
+ total = 0
23
+
24
+ # we only decode raw zlib deflated streams that mogtool (unfortunately)
25
+ # generates. tarballs and gzip(1) are up to to the application to decrypt.
26
+ filter = Proc.new do |buf|
27
+ if zi == nil
28
+ if info[:compressed] && info[:type] == 'file' &&
29
+ buf.length >= 2 && buf[0,2] != GZIP_HEADER
30
+ zi = Zlib::Inflate.new
31
+
32
+ # mogtool(1) seems to have a bug that causes it to generate bogus
33
+ # MD5s if zlib deflate is used. Don't trust those MD5s for now...
34
+ md5 = nil
35
+ else
36
+ zi = false
37
+ end
38
+ end
39
+ buf ||= ''
40
+ if zi
41
+ zi.inflate(buf)
42
+ else
43
+ md5 << buf
44
+ buf
45
+ end
46
+ end if (info[:compressed] || md5)
47
+
48
+ info[:parts].each_with_index do |part,part_nr|
49
+ next if part_nr == 0 # info[:parts][0] is always empty
50
+ uris = verify_uris(part[:paths].map { |path| URI.parse(path) })
51
+ if uris.empty?
52
+ # part[:paths] may not be valid anymore due to rebalancing, however we
53
+ # can get_keys on key,<part_nr> and retry paths if all paths fail
54
+ part[:paths] = get_paths("#{key.gsub(/^big_info:/, '')},#{part_nr}")
55
+ uris = verify_uris(part[:paths].map { |path| URI.parse(path) })
56
+ raise MogileFS::Backend::NoDevices if uris.empty?
57
+ end
58
+
59
+ sock = http_get_sock(uris[0])
60
+ md5.reset if md5
61
+ w = sysrwloop(sock, wr_io, filter)
62
+
63
+ if md5 && md5.hexdigest != part[:md5]
64
+ raise MogileFS::ChecksumMismatchError, "#{md5} != #{part[:md5]}"
65
+ end
66
+ total += w
67
+ end
68
+
69
+ syswrite_full(wr_io, zi.finish) if zi
70
+
71
+ [ total, info ]
72
+ end
73
+
74
+ private
75
+
76
+ include MogileFS::Util
77
+
78
+ ##
79
+ # parses the contents of a _big_info: string or IO object
80
+ def parse_info(info = '')
81
+ rv = { :parts => [] }
82
+ info.each_line do |line|
83
+ line.chomp!
84
+ case line
85
+ when /^(des|type|filename)\s+(.+)$/
86
+ rv[$1.to_sym] = $2
87
+ when /^compressed\s+([01])$/
88
+ rv[:compressed] = ($1 == '1')
89
+ when /^(chunks|size)\s+(\d+)$/
90
+ rv[$1.to_sym] = $2.to_i
91
+ when /^part\s+(\d+)\s+bytes=(\d+)\s+md5=(.+)\s+paths:\s+(.+)$/
92
+ rv[:parts][$1.to_i] = {
93
+ :bytes => $2.to_i,
94
+ :md5 => $3.downcase,
95
+ :paths => $4.split(/\s*,\s*/),
96
+ }
97
+ end
98
+ end
99
+
100
+ rv
101
+ end
102
+
103
+ end # module MogileFS::Bigfile
104
+
105
+ __END__
106
+ # Copied from mogtool:
107
+ # http://code.sixapart.com/svn/mogilefs/utils/mogtool, r1221
108
+
109
+ # this is a temporary file that we delete when we're doing recording all chunks
110
+
111
+ _big_pre:<key>
112
+
113
+ starttime=UNIXTIMESTAMP
114
+
115
+ # when done, we write the _info file and delete the _pre.
116
+
117
+ _big_info:<key>
118
+
119
+ des Cow's ljdb backup as of 2004-11-17
120
+ type { partition, file, tarball }
121
+ compressed {0, 1}
122
+ filename ljbinlog.305.gz
123
+ partblocks 234324324324
124
+
125
+
126
+ part 1 <bytes> <md5hex>
127
+ part 2 <bytes> <md5hex>
128
+ part 3 <bytes> <md5hex>
129
+ part 4 <bytes> <md5hex>
130
+ part 5 <bytes> <md5hex>
131
+
132
+ _big:<key>,<n>
133
+ _big:<key>,<n>
134
+ _big:<key>,<n>
135
+
136
+
137
+ Receipt format:
138
+
139
+ BEGIN MOGTOOL RECIEPT
140
+ type partition
141
+ des Foo
142
+ compressed foo
143
+
144
+ part 1 bytes=23423432 md5=2349823948239423984 paths: http://dev5/2/23/23/.fid, http://dev6/23/423/4/324.fid
145
+ part 1 bytes=23423432 md5=2349823948239423984 paths: http://dev5/2/23/23/.fid, http://dev6/23/423/4/324.fid
146
+ part 1 bytes=23423432 md5=2349823948239423984 paths: http://dev5/2/23/23/.fid, http://dev6/23/423/4/324.fid
147
+ part 1 bytes=23423432 md5=2349823948239423984 paths: http://dev5/2/23/23/.fid, http://dev6/23/423/4/324.fid
148
+
149
+
150
+ END RECIEPT
151
+
152
+
153
+
@@ -38,8 +38,6 @@ class MogileFS::Client
38
38
 
39
39
  ##
40
40
  # The last error reported by the backend.
41
- #--
42
- # TODO use Exceptions
43
41
 
44
42
  def err
45
43
  @backend.lasterr
@@ -47,8 +45,6 @@ class MogileFS::Client
47
45
 
48
46
  ##
49
47
  # The last error message reported by the backend.
50
- #--
51
- # TODO use Exceptions
52
48
 
53
49
  def errstr
54
50
  @backend.lasterrstr
@@ -58,7 +54,7 @@ class MogileFS::Client
58
54
  # Is this a read-only client?
59
55
 
60
56
  def readonly?
61
- return @readonly
57
+ @readonly
62
58
  end
63
59
 
64
60
  end
@@ -1,5 +1,3 @@
1
- require 'fcntl'
2
- require 'socket'
3
1
  require 'stringio'
4
2
  require 'uri'
5
3
  require 'mogilefs/backend'
@@ -12,18 +10,23 @@ require 'mogilefs/util'
12
10
  # You really don't want to create an HTTPFile by hand. Instead you want to
13
11
  # create a new file using MogileFS::MogileFS.new_file.
14
12
  #
15
- # WARNING! HTTP mode is completely untested as I cannot make it work on
16
- # FreeBSD. Please send patches/tests if you find bugs.
17
13
  #--
18
14
  # TODO dup'd content in MogileFS::NFSFile
19
15
 
20
16
  class MogileFS::HTTPFile < StringIO
21
17
  include MogileFS::Util
22
18
 
19
+ class EmptyResponseError < MogileFS::Error; end
20
+ class BadResponseError < MogileFS::Error; end
21
+ class UnparseableResponseError < MogileFS::Error; end
22
+ class NoStorageNodesError < MogileFS::Error
23
+ def message; 'Unable to open socket to storage node'; end
24
+ end
25
+
23
26
  ##
24
- # The path this file will be stored to.
27
+ # The URI this file will be stored to.
25
28
 
26
- attr_reader :path
29
+ attr_reader :uri
27
30
 
28
31
  ##
29
32
  # The key for this file. This key won't represent a real file until you've
@@ -37,9 +40,9 @@ class MogileFS::HTTPFile < StringIO
37
40
  attr_reader :class
38
41
 
39
42
  ##
40
- # The bigfile name in case we have file > 256M
43
+ # The big_io name in case we have file > 256M
41
44
 
42
- attr_accessor :bigfile
45
+ attr_accessor :big_io
43
46
 
44
47
  ##
45
48
  # Works like File.open. Use MogileFS::MogileFS#new_file instead of this
@@ -61,93 +64,84 @@ class MogileFS::HTTPFile < StringIO
61
64
  # Creates a new HTTPFile with MogileFS-specific data. Use
62
65
  # MogileFS::MogileFS#new_file instead of this method.
63
66
 
64
- def initialize(mg, fid, path, devid, klass, key, dests, content_length)
67
+ def initialize(mg, fid, klass, key, dests, content_length)
65
68
  super ''
66
69
  @mg = mg
67
70
  @fid = fid
68
- @path = path
69
- @devid = devid
71
+ @uri = @devid = nil
70
72
  @klass = klass
71
73
  @key = key
72
- @bigfile = nil
74
+ @big_io = nil
73
75
 
74
- @dests = dests.map { |(_,u)| URI.parse u }
76
+ @dests = dests
75
77
  @tried = {}
76
78
 
77
79
  @socket = nil
78
80
  end
79
81
 
80
82
  ##
81
- # Closes the file handle and marks it as closed in MogileFS.
82
-
83
- def close
84
- connect_socket
85
-
86
- file_size = nil
87
- if @bigfile
83
+ # Writes an HTTP PUT request to +sock+ to upload the file and
84
+ # returns file size if the socket finished writing
85
+ def upload(devid, uri)
86
+ file_size = length
87
+ sock = Socket.mogilefs_new(uri.host, uri.port)
88
+ sock.mogilefs_tcp_cork = true
89
+
90
+ if @big_io
88
91
  # Don't try to run out of memory
89
- fp = File.open(@bigfile)
90
- file_size = File.size(@bigfile)
91
- @socket.write "PUT #{@path.request_uri} HTTP/1.0\r\nContent-Length: #{file_size}\r\n\r\n"
92
- sysrwloop(fp, @socket)
93
- fp.close
92
+ File.open(@big_io) do |fp|
93
+ file_size = fp.stat.size
94
+ fp.sync = true
95
+ syswrite_full(sock, "PUT #{uri.request_uri} HTTP/1.0\r\n" \
96
+ "Content-Length: #{file_size}\r\n\r\n")
97
+ sysrwloop(fp, sock)
98
+ end
94
99
  else
95
- @socket.write "PUT #{@path.request_uri} HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n#{string}"
100
+ syswrite_full(sock, "PUT #{uri.request_uri} HTTP/1.0\r\n" \
101
+ "Content-Length: #{length}\r\n\r\n#{string}")
96
102
  end
103
+ sock.mogilefs_tcp_cork = false
104
+
105
+ line = sock.gets or
106
+ raise EmptyResponseError, 'Unable to read response line from server'
97
107
 
98
- if connected? then
99
- line = @socket.gets
100
- raise 'Unable to read response line from server' if line.nil?
101
-
102
- if line =~ %r%^HTTP/\d+\.\d+\s+(\d+)% then
103
- status = Integer $1
104
- case status
105
- when 200..299 then # success!
106
- else
107
- raise "HTTP response status from upload: #{status}"
108
- end
108
+ if line =~ %r%^HTTP/\d+\.\d+\s+(\d+)% then
109
+ case $1.to_i
110
+ when 200..299 then # success!
109
111
  else
110
- raise "Response line not understood: #{line}"
112
+ raise BadResponseError, "HTTP response status from upload: #{$1}"
111
113
  end
112
-
113
- @socket.close
114
+ else
115
+ raise UnparseableResponseError, "Response line not understood: #{line}"
114
116
  end
115
117
 
116
- @mg.backend.create_close(:fid => @fid, :devid => @devid,
118
+ @mg.backend.create_close(:fid => @fid, :devid => devid,
117
119
  :domain => @mg.domain, :key => @key,
118
- :path => @path, :size => length)
119
- return file_size if @bigfile
120
- return nil
121
- end
120
+ :path => uri.to_s, :size => file_size)
121
+ file_size
122
+ end # def upload
122
123
 
123
- private
124
-
125
- def connected?
126
- return !(@socket.nil? or @socket.closed?)
127
- end
128
-
129
- def connect_socket
130
- return @socket if connected?
131
-
132
- next_path
133
-
134
- if @path.nil? then
135
- @tried.clear
136
- next_path
137
- raise 'Unable to open socket to storage node' if @path.nil?
138
- end
139
-
140
- @socket = TCPSocket.new @path.host, @path.port
141
- end
142
-
143
- def next_path
144
- @path = nil
145
- @dests.each do |dest|
146
- unless @tried.include? dest then
147
- @path = dest
148
- return
124
+ def close
125
+ try_dests = @dests.dup
126
+ last_err = nil
127
+
128
+ loop do
129
+ devid, url = try_dests.shift
130
+ devid && url or break
131
+
132
+ uri = URI.parse(url)
133
+ begin
134
+ bytes = upload(devid, uri)
135
+ @devid, @uri = devid, uri
136
+ return bytes
137
+ rescue SystemCallError, Errno::ECONNREFUSED, MogileFS::Timeout,
138
+ EmptyResponseError, BadResponseError,
139
+ UnparseableResponseError => err
140
+ last_err = @tried[url] = err
149
141
  end
150
142
  end
143
+
144
+ raise last_err ? last_err : NoStorageNodesError
151
145
  end
152
146
 
153
147
  end