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,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