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,27 +1,13 @@
1
- require 'open-uri'
2
- require 'net/http'
3
- require 'timeout'
4
-
5
1
  require 'mogilefs/client'
6
- require 'mogilefs/nfsfile'
7
2
  require 'mogilefs/util'
8
3
 
9
- ##
10
- # Timeout error class.
11
-
12
- class MogileFS::Timeout < Timeout::Error; end
13
-
14
4
  ##
15
5
  # MogileFS File manipulation client.
16
6
 
17
7
  class MogileFS::MogileFS < MogileFS::Client
18
8
 
19
9
  include MogileFS::Util
20
-
21
- ##
22
- # The path to the local MogileFS mount point if you are using NFS mode.
23
-
24
- attr_reader :root
10
+ include MogileFS::Bigfile
25
11
 
26
12
  ##
27
13
  # The domain of keys for this MogileFS client.
@@ -33,20 +19,26 @@ class MogileFS::MogileFS < MogileFS::Client
33
19
 
34
20
  attr_accessor :get_file_data_timeout
35
21
 
22
+ ##
23
+ # internal Regexp for matching an "HTTP 200 OK" head response
24
+ HTTP_200_OK = %r{\AHTTP/\d+\.\d+\s+200\s+}.freeze
25
+
36
26
  ##
37
27
  # Creates a new MogileFS::MogileFS instance. +args+ must include a key
38
- # :domain specifying the domain of this client. A key :root will be used to
39
- # specify the root of the NFS file system.
28
+ # :domain specifying the domain of this client.
40
29
 
41
30
  def initialize(args = {})
42
31
  @domain = args[:domain]
43
- @root = args[:root]
44
32
 
45
33
  @get_file_data_timeout = 5
46
34
 
47
35
  raise ArgumentError, "you must specify a domain" unless @domain
48
36
 
49
- super
37
+ if @backend = args[:db_backend]
38
+ @readonly = true
39
+ else
40
+ super
41
+ end
50
42
  end
51
43
 
52
44
  ##
@@ -62,7 +54,7 @@ class MogileFS::MogileFS < MogileFS::Client
62
54
  keys, after = list_keys prefix, after
63
55
  end
64
56
 
65
- return nil
57
+ nil
66
58
  end
67
59
 
68
60
  ##
@@ -78,23 +70,10 @@ class MogileFS::MogileFS < MogileFS::Client
78
70
  case path
79
71
  when /^http:\/\// then
80
72
  begin
81
- path = URI.parse path
82
-
83
- if block_given?
84
- sock = nil
85
- timeout @get_file_data_timeout, MogileFS::Timeout do
86
- sock = TCPSocket.new(path.host, path.port)
87
- sock.sync = true
88
- sock.syswrite("GET #{path.request_uri} HTTP/1.0\r\n\r\n")
89
- buf = sock.recv(4096, Socket::MSG_PEEK)
90
- head, body = buf.split(/\r\n\r\n/, 2)
91
- head = sock.recv(head.size + 4)
92
- end
93
- return yield(sock)
94
- else
95
- return path.read
96
- end
97
- rescue MogileFS::Timeout
73
+ sock = http_get_sock(URI.parse(path))
74
+ return block_given? ? yield(sock) : sock.read
75
+ rescue MogileFS::Timeout, Errno::ECONNREFUSED,
76
+ EOFError, SystemCallError, MogileFS::InvalidResponseError
98
77
  next
99
78
  end
100
79
  else
@@ -103,22 +82,18 @@ class MogileFS::MogileFS < MogileFS::Client
103
82
  end
104
83
  end
105
84
 
106
- return nil
85
+ nil
107
86
  end
108
87
 
109
88
  ##
110
89
  # Get the paths for +key+.
111
90
 
112
91
  def get_paths(key, noverify = true, zone = nil)
113
- noverify = noverify ? 1 : 0
114
- res = @backend.get_paths(:domain => @domain, :key => key,
115
- :noverify => noverify, :zone => zone)
116
-
117
- return nil if res.nil? and @backend.lasterr == 'unknown_key'
118
- paths = (1..res['paths'].to_i).map { |i| res["path#{i}"] }
119
- return paths if paths.empty?
120
- return paths if paths.first =~ /^http:\/\//
121
- return paths.map { |path| File.join @root, path }
92
+ opts = { :domain => @domain, :key => key,
93
+ :noverify => noverify ? 1 : 0, :zone => zone }
94
+ @backend.respond_to?(:_get_paths) and return @backend._get_paths(opts)
95
+ res = @backend.get_paths(opts)
96
+ (1..res['paths'].to_i).map { |i| res["path#{i}"] }
122
97
  end
123
98
 
124
99
  ##
@@ -126,41 +101,35 @@ class MogileFS::MogileFS < MogileFS::Client
126
101
  #
127
102
  # The +block+ operates like File.open.
128
103
 
129
- def new_file(key, klass, bytes = 0, &block) # :yields: file
130
- raise 'readonly mogilefs' if readonly?
131
-
132
- res = @backend.create_open(:domain => @domain, :class => klass,
133
- :key => key, :multi_dest => 1)
104
+ def new_file(key, klass = nil, bytes = 0, &block) # :yields: file
105
+ raise MogileFS::ReadOnlyError if readonly?
106
+ opts = { :domain => @domain, :key => key, :multi_dest => 1 }
107
+ opts[:class] = klass if klass
108
+ res = @backend.create_open(opts)
134
109
 
135
- raise "#{@backend.lasterr}: #{@backend.lasterrstr}" if res.nil? || res == {} # HACK
136
-
137
- dests = nil
138
-
139
- if res.include? 'dev_count' then # HACK HUH?
140
- dests = (1..res['dev_count'].to_i).map do |i|
110
+ dests = if dev_count = res['dev_count'] # multi_dest succeeded
111
+ (1..dev_count.to_i).map do |i|
141
112
  [res["devid_#{i}"], res["path_#{i}"]]
142
113
  end
143
- else
114
+ else # single destination returned
144
115
  # 0x0040: d0e4 4f4b 2064 6576 6964 3d31 2666 6964 ..OK.devid=1&fid
145
116
  # 0x0050: 3d33 2670 6174 683d 6874 7470 3a2f 2f31 =3&path=http://1
146
117
  # 0x0060: 3932 2e31 3638 2e31 2e37 323a 3735 3030 92.168.1.72:7500
147
118
  # 0x0070: 2f64 6576 312f 302f 3030 302f 3030 302f /dev1/0/000/000/
148
119
  # 0x0080: 3030 3030 3030 3030 3033 2e66 6964 0d0a 0000000003.fid..
149
120
 
150
- dests = [[res['devid'], res['path']]]
121
+ [[res['devid'], res['path']]]
151
122
  end
152
123
 
153
- dest = dests.first
154
- devid, path = dest
155
-
156
- case path
124
+ case (dests[0][1] rescue nil)
157
125
  when nil, '' then
158
- raise 'Empty path for mogile upload'
126
+ raise MogileFS::EmptyPathError
159
127
  when /^http:\/\// then
160
- MogileFS::HTTPFile.open(self, res['fid'], path, devid, klass, key,
128
+ MogileFS::HTTPFile.open(self, res['fid'], klass, key,
161
129
  dests, bytes, &block)
162
130
  else
163
- MogileFS::NFSFile.open(self, res['fid'], path, devid, klass, key, &block)
131
+ raise MogileFS::UnsupportedPathError,
132
+ "paths '#{dests.inspect}' returned by backend is not supported"
164
133
  end
165
134
  end
166
135
 
@@ -169,15 +138,15 @@ class MogileFS::MogileFS < MogileFS::Client
169
138
  # either a file name or an object that responds to #read.
170
139
 
171
140
  def store_file(key, klass, file)
172
- raise 'readonly mogilefs' if readonly?
141
+ raise MogileFS::ReadOnlyError if readonly?
173
142
 
174
143
  new_file key, klass do |mfp|
175
144
  if file.respond_to? :sysread then
176
145
  return sysrwloop(file, mfp)
177
146
  else
178
147
  if File.size(file) > 0x10000 # Bigass file, handle differently
179
- mfp.bigfile = file
180
- return mfp.close
148
+ mfp.big_io = file
149
+ return
181
150
  else
182
151
  return File.open(file) { |fp| sysrwloop(fp, mfp) }
183
152
  end
@@ -189,26 +158,22 @@ class MogileFS::MogileFS < MogileFS::Client
189
158
  # Stores +content+ into +key+ in class +klass+.
190
159
 
191
160
  def store_content(key, klass, content)
192
- raise 'readonly mogilefs' if readonly?
161
+ raise MogileFS::ReadOnlyError if readonly?
193
162
 
194
163
  new_file key, klass do |mfp|
195
164
  mfp << content
196
165
  end
197
166
 
198
- return content.length
167
+ content.length
199
168
  end
200
169
 
201
170
  ##
202
171
  # Removes +key+.
203
172
 
204
173
  def delete(key)
205
- raise 'readonly mogilefs' if readonly?
174
+ raise MogileFS::ReadOnlyError if readonly?
206
175
 
207
- res = @backend.delete :domain => @domain, :key => key
208
-
209
- if res.nil? and @backend.lasterr != 'unknown_key' then
210
- raise "unable to delete #{key}: #{@backend.lasterr}"
211
- end
176
+ @backend.delete :domain => @domain, :key => key
212
177
  end
213
178
 
214
179
  ##
@@ -222,40 +187,43 @@ class MogileFS::MogileFS < MogileFS::Client
222
187
  # Renames a key +from+ to key +to+.
223
188
 
224
189
  def rename(from, to)
225
- raise 'readonly mogilefs' if readonly?
226
-
227
- res = @backend.rename :domain => @domain, :from_key => from, :to_key => to
190
+ raise MogileFS::ReadOnlyError if readonly?
228
191
 
229
- if res.nil? and @backend.lasterr != 'unknown_key' then
230
- raise "unable to rename #{from} to #{to}: #{@backend.lasterr}"
231
- end
192
+ @backend.rename :domain => @domain, :from_key => from, :to_key => to
193
+ nil
232
194
  end
233
195
 
234
196
  ##
235
197
  # Returns the size of +key+.
236
198
  def size(key)
237
- paths = get_paths key
238
-
239
- return nil unless paths
199
+ @backend.respond_to?(:_size) and return @backend._size(domain, key)
200
+ paths = get_paths(key) or return nil
201
+ paths_size(paths)
202
+ end
240
203
 
204
+ def paths_size(paths)
241
205
  paths.each do |path|
242
206
  next unless path
243
207
  case path
244
208
  when /^http:\/\// then
245
209
  begin
246
210
  url = URI.parse path
247
-
248
- req = Net::HTTP::Head.new url.request_uri
249
-
250
- res = timeout @get_file_data_timeout, MogileFS::Timeout do
251
- Net::HTTP.start url.host, url.port do |http|
252
- http.request req
211
+ s = Socket.mogilefs_new_request(url.host, url.port,
212
+ "HEAD #{url.request_uri} HTTP/1.0\r\n\r\n",
213
+ @get_file_data_timeout)
214
+ res = s.recv(4096, 0)
215
+ if res =~ HTTP_200_OK
216
+ head, body = res.split(/\r\n\r\n/, 2)
217
+ if head =~ /^Content-Length:\s*(\d+)/i
218
+ return $1.to_i
253
219
  end
254
220
  end
255
-
256
- return res['Content-Length'].to_i
257
- rescue MogileFS::Timeout
258
221
  next
222
+ rescue MogileFS::Timeout, Errno::ECONNREFUSED,
223
+ EOFError, SystemCallError
224
+ next
225
+ ensure
226
+ s.close rescue nil
259
227
  end
260
228
  else
261
229
  next unless File.exist? path
@@ -263,23 +231,55 @@ class MogileFS::MogileFS < MogileFS::Client
263
231
  end
264
232
  end
265
233
 
266
- return nil
234
+ nil
267
235
  end
268
236
 
269
237
  ##
270
238
  # Lists keys starting with +prefix+ follwing +after+ up to +limit+. If
271
239
  # +after+ is nil the list starts at the beginning.
272
240
 
273
- def list_keys(prefix, after = nil, limit = 1000)
274
- res = @backend.list_keys(:domain => domain, :prefix => prefix,
275
- :after => after, :limit => limit)
241
+ def list_keys(prefix, after = nil, limit = 1000, &block)
242
+ if @backend.respond_to?(:_list_keys)
243
+ return @backend._list_keys(domain, prefix, after, limit, &block)
244
+ end
276
245
 
277
- return nil if res.nil?
246
+ res = begin
247
+ @backend.list_keys(:domain => domain, :prefix => prefix,
248
+ :after => after, :limit => limit)
249
+ rescue MogileFS::Backend::NoneMatchError
250
+ return nil
251
+ end
278
252
 
279
253
  keys = (1..res['key_count'].to_i).map { |i| res["key_#{i}"] }
254
+ if block_given?
255
+ # emulate the MogileFS::Mysql interface, slowly...
256
+ keys.each do |key|
257
+ paths = get_paths(key) or next
258
+ length = paths_size(paths) or next
259
+ yield key, length, paths.size
260
+ end
261
+ end
280
262
 
281
- return keys, res['next_after']
263
+ [ keys, res['next_after'] ]
282
264
  end
283
265
 
266
+ protected
267
+
268
+ # given a URI, this returns a readable socket with ready data from the
269
+ # body of the response.
270
+ def http_get_sock(uri)
271
+ sock = Socket.mogilefs_new_request(uri.host, uri.port,
272
+ "GET #{uri.request_uri} HTTP/1.0\r\n\r\n",
273
+ @get_file_data_timeout)
274
+ buf = sock.recv(4096, Socket::MSG_PEEK)
275
+ head, body = buf.split(/\r\n\r\n/, 2)
276
+ if head =~ HTTP_200_OK
277
+ sock.recv(head.size + 4, 0)
278
+ return sock
279
+ end
280
+ raise MogileFS::InvalidResponseError,
281
+ "GET on #{uri} returned: #{head.inspect}"
282
+ end # def http_get_sock
283
+
284
284
  end
285
285
 
@@ -0,0 +1,166 @@
1
+ require 'mogilefs'
2
+ require 'mogilefs/backend' # for the exceptions
3
+
4
+ # read-only interface that can be a backend for MogileFS::MogileFS
5
+ #
6
+ # This provides direct, read-only access to any slave MySQL database to
7
+ # provide better performance, scalability and eliminate mogilefsd as a
8
+ # point of failure
9
+ class MogileFS::Mysql
10
+
11
+ attr_reader :my
12
+ attr_reader :query_method
13
+
14
+ ##
15
+ # Creates a new MogileFS::Mysql instance. +args+ must include a key
16
+ # :domain specifying the domain of this client and :mysql, specifying
17
+ # an already-initialized Mysql object.
18
+ #
19
+ # The Mysql object can be either the standard Mysql driver or the
20
+ # Mysqlplus one supporting c_async_query.
21
+ def initialize(args = {})
22
+ @my = args[:mysql]
23
+ @query_method = @my.respond_to?(:c_async_query) ? :c_async_query : :query
24
+ @last_update_device = @last_update_domain = Time.at(0)
25
+ @cache_domain = @cache_device = nil
26
+ end
27
+
28
+ ##
29
+ # Lists keys starting with +prefix+ follwing +after+ up to +limit+. If
30
+ # +after+ is nil the list starts at the beginning.
31
+ def _list_keys(domain, prefix = '', after = '', limit = 1000, &block)
32
+ # this code is based on server/lib/MogileFS/Worker/Query.pm
33
+ dmid = get_dmid(domain)
34
+
35
+ # don't modify passed arguments
36
+ limit ||= 1000
37
+ limit = limit.to_i
38
+ limit = 1000 if limit > 1000 || limit <= 0
39
+ after, prefix = "#{after}", "#{prefix}"
40
+
41
+ if after.length > 0 && /^#{Regexp.quote(prefix)}/ !~ after
42
+ raise MogileFS::Backend::AfterMismatchError
43
+ end
44
+
45
+ raise MogileFS::Backend::InvalidCharsError if /[%\\]/ =~ prefix
46
+ prefix.gsub!(/_/, '\_') # not sure why MogileFS::Worker::Query does this...
47
+
48
+ sql = <<-EOS
49
+ SELECT dkey,length,devcount FROM file
50
+ WHERE dmid = #{dmid}
51
+ AND dkey LIKE '#{@my.quote(prefix)}%'
52
+ AND dkey > '#{@my.quote(after)}'
53
+ ORDER BY dkey LIMIT #{limit}
54
+ EOS
55
+
56
+ keys = []
57
+ query(sql).each do |dkey,length,devcount|
58
+ yield(dkey, length, devcount) if block_given?
59
+ keys << dkey
60
+ end
61
+
62
+ keys.empty? ? nil : [ keys, (keys.last || '') ]
63
+ end
64
+
65
+ ##
66
+ # Returns the size of +key+.
67
+ def _size(domain, key)
68
+ dmid = get_dmid(domain)
69
+
70
+ sql = <<-EOS
71
+ SELECT length FROM file
72
+ WHERE dmid = #{dmid} AND dkey = '#{@my.quote(key)}'
73
+ LIMIT 1
74
+ EOS
75
+
76
+ res = query(sql).fetch_row
77
+ return res[0].to_i if res && res[0]
78
+ raise MogileFS::Backend::UnknownKeyError
79
+ end
80
+
81
+ ##
82
+ # Get the paths for +key+.
83
+ def _get_paths(params = {})
84
+ zone = params[:zone]
85
+ noverify = (params[:noverify] == 1) # TODO this is unused atm
86
+ dmid = get_dmid(params[:domain])
87
+ devices = refresh_device or raise MogileFS::Backend::NoDevicesError
88
+ urls = []
89
+ sql = <<-EOS
90
+ SELECT fid FROM file
91
+ WHERE dmid = #{dmid} AND dkey = '#{@my.quote(params[:key])}'
92
+ LIMIT 1
93
+ EOS
94
+
95
+ res = query(sql).fetch_row
96
+ res && res[0] or raise MogileFS::Backend::UnknownKeyError
97
+ fid = res[0]
98
+ sql = "SELECT devid FROM file_on WHERE fid = '#{@my.quote(fid)}'"
99
+ query(sql).each do |devid,|
100
+ unless devinfo = devices[devid.to_i]
101
+ devices = refresh_device(true)
102
+ devinfo = devices[devid.to_i] or next
103
+ end
104
+
105
+ port = devinfo[:http_get_port]
106
+ host = zone && zone == 'alt' ? devinfo[:altip] : devinfo[:hostip]
107
+ nfid = '%010u' % fid
108
+ b, mmm, ttt = /(\d)(\d{3})(\d{3})(?:\d{3})/.match(nfid)[1..3]
109
+ uri = "/dev#{devid}/#{b}/#{mmm}/#{ttt}/#{nfid}.fid"
110
+ urls << "http://#{host}:#{port}#{uri}"
111
+ end
112
+ urls
113
+ end
114
+
115
+ def sleep(params); Kernel.sleep(params[:duration] || 10); {}; end
116
+
117
+ private
118
+
119
+ unless defined? GET_DEVICES
120
+ GET_DOMAINS = 'SELECT dmid,namespace FROM domain'.freeze
121
+
122
+ GET_DEVICES = <<-EOS
123
+ SELECT d.devid, h.hostip, h.altip, h.http_port, h.http_get_port
124
+ FROM device d
125
+ LEFT JOIN host h ON d.hostid = h.hostid
126
+ WHERE d.status IN ('alive','readonly','drain');
127
+ EOS
128
+ GET_DEVICES.freeze
129
+ end
130
+
131
+ def query(sql)
132
+ @my.send(@query_method, sql)
133
+ end
134
+
135
+ def refresh_device(force = false)
136
+ return @cache_device if ! force && ((Time.now - @last_update_device) < 60)
137
+ tmp = {}
138
+ res = query(GET_DEVICES)
139
+ res.each do |devid, hostip, altip, http_port, http_get_port|
140
+ http_port = http_port ? http_port.to_i : 80
141
+ tmp[devid.to_i] = {
142
+ :hostip => hostip.freeze,
143
+ :altip => (altip || hostip).freeze,
144
+ :http_port => http_port,
145
+ :http_get_port => http_get_port ? http_get_port.to_i : http_port,
146
+ }.freeze
147
+ end
148
+ @last_update_device = Time.now
149
+ @cache_device = tmp.freeze
150
+ end
151
+
152
+ def refresh_domain(force = false)
153
+ return @cache_domain if ! force && ((Time.now - @last_update_domain) < 5)
154
+ tmp = {}
155
+ res = query(GET_DOMAINS)
156
+ res.each { |dmid,namespace| tmp[namespace] = dmid.to_i }
157
+ @last_update_domain = Time.now
158
+ @cache_domain = tmp.freeze
159
+ end
160
+
161
+ def get_dmid(domain)
162
+ refresh_domain[domain] || refresh_domain(true)[domain] or \
163
+ raise MogileFS::Backend::DomainNotFoundError, domain
164
+ end
165
+
166
+ end