mogilefs-client 1.3.1 → 2.0.0

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