Class: MogileFS::Backend

Inherits:
Object
  • Object
show all
Defined in:
lib/mogilefs/backend.rb

Overview

MogileFS::Backend communicates with the MogileFS trackers.

Constant Summary collapse

BACKEND_ERRORS =
{}
RS =

record-separator for mogilefsd responses, update this if the protocol changes

"\n"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(args) ⇒ Backend

Creates a new MogileFS::Backend.

:hosts is a required argument and must be an Array containing one or more ‘hostname:port’ pairs as Strings.

:timeout adjusts the request timeout before an error is returned.

Raises:

  • (ArgumentError)


57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/mogilefs/backend.rb', line 57

def initialize(args)
  @hosts = args[:hosts]
  raise ArgumentError, "must specify at least one host" unless @hosts
  raise ArgumentError, "must specify at least one host" if @hosts.empty?
  unless @hosts == @hosts.select { |h| h =~ /:\d+$/ } then
    raise ArgumentError, ":hosts must be in 'host:port' form"
  end

  @mutex = Mutex.new
  @timeout = args[:timeout] || 3
  @socket = nil
  @lasterr = nil
  @lasterrstr = nil

  @dead = {}
end

Instance Attribute Details

#lasterrObject (readonly)

The last error



42
43
44
# File 'lib/mogilefs/backend.rb', line 42

def lasterr
  @lasterr
end

#lasterrstrObject (readonly)

The string attached to the last error



47
48
49
# File 'lib/mogilefs/backend.rb', line 47

def lasterrstr
  @lasterrstr
end

Class Method Details

.add_command(*names) ⇒ Object

Adds MogileFS commands names.



14
15
16
17
18
19
20
# File 'lib/mogilefs/backend.rb', line 14

def self.add_command(*names)
  names.each do |name|
    define_method name do |*args|
      do_request name, args.first || {}
    end
  end
end

.add_error(err_snake) ⇒ Object

this converts an error code from a mogilefsd tracker to an exception:

Examples of some exceptions that get created:

class AfterMismatchError < MogileFS::Error; end
class DomainNotFoundError < MogileFS::Error; end
class InvalidCharsError < MogileFS::Error; end


30
31
32
33
34
35
36
37
# File 'lib/mogilefs/backend.rb', line 30

def self.add_error(err_snake)
  err_camel = err_snake.gsub(/(?:^|_)([a-z])/) { $1.upcase }
  err_camel << 'Error' unless /Error\z/ =~ err_camel
  unless self.const_defined?(err_camel)
    self.class_eval("class #{err_camel} < MogileFS::Error; end")
  end
  BACKEND_ERRORS[err_snake] = self.const_get(err_camel)
end

Instance Method Details

#do_request(cmd, args) ⇒ Object

Performs the cmd request with args.



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/mogilefs/backend.rb', line 162

def do_request(cmd, args)
  response = nil
  request = make_request cmd, args
  @mutex.synchronize do
    begin
      io = socket
      begin
        bytes_sent = io.write request
        bytes_sent == request.size or
          raise MogileFS::RequestTruncatedError,
             "request truncated (sent #{bytes_sent} expected #{request.size})"
      rescue SystemCallError
        raise MogileFS::UnreachableBackendError
      end

      readable?(io)
      response = io.gets(RS) and return parse_response(response)
    ensure
      # 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!
      # we close the socket if it times out like this
      response or shutdown_unlocked
    end
  end # @mutex.synchronize
end

#error(err_snake) ⇒ Object

this converts an error code from a mogilefsd tracker to an exception Most of these exceptions should already be defined, but since the MogileFS server code is liable to change and we may not always be able to keep up with the changes



199
200
201
# File 'lib/mogilefs/backend.rb', line 199

def error(err_snake)
  BACKEND_ERRORS[err_snake] || self.class.add_error(err_snake)
end

#make_request(cmd, args) ⇒ Object

Makes a new request string for cmd and args.



191
192
193
# File 'lib/mogilefs/backend.rb', line 191

def make_request(cmd, args)
  "#{cmd} #{url_encode args}\r\n"
end

#parse_response(line) ⇒ Object

Turns the line response from the server into a Hash of options, an error, or raises, as appropriate.



207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/mogilefs/backend.rb', line 207

def parse_response(line)
  if line =~ /^ERR\s+(\w+)\s*([^\r\n]*)/
    @lasterr = $1
    @lasterrstr = $2 ? url_unescape($2) : nil
    raise error(@lasterr), @lasterrstr
  end

  return url_decode($1) if line =~ /^OK\s+\d*\s*(\S*)/

  raise MogileFS::InvalidResponseError,
        "Invalid response from server: #{line.inspect}"
end

#readable?(io = @socket) ⇒ Boolean

Raises if the socket does not become readable in @timeout seconds.

Returns:

  • (Boolean)


223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/mogilefs/backend.rb', line 223

def readable?(io = @socket)
  timeleft = @timeout
  peer = nil
  loop do
    t0 = Time.now
    found = IO.select([io], nil, nil, timeleft)
    return true if found && found[0]
    timeleft -= (Time.now - t0)
    timeleft >= 0 and next
    peer = io ? "#{io.mogilefs_peername} " : nil

    raise MogileFS::UnreadableSocketError, "#{peer}never became readable"
  end
  false
end

#shutdownObject

Closes this backend’s socket.



77
78
79
# File 'lib/mogilefs/backend.rb', line 77

def shutdown
  @mutex.synchronize { shutdown_unlocked }
end

#shutdown_unlockedObject

:nodoc:



152
153
154
155
156
157
# File 'lib/mogilefs/backend.rb', line 152

def shutdown_unlocked # :nodoc:
  if @socket
    @socket.close rescue nil # ignore errors
    @socket = nil
  end
end

#socketObject

Returns a socket connected to a MogileFS tracker.



242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/mogilefs/backend.rb', line 242

def socket
  return @socket if @socket and not @socket.closed?

  now = Time.now

  @hosts.sort_by { rand(3) - 1 }.each do |host|
    next if @dead.include? host and @dead[host] > now - 5

    begin
      @socket = Socket.mogilefs_new(*(host.split(/:/) << @timeout))
    rescue SystemCallError, MogileFS::Timeout
      @dead[host] = now
      next
    end

    return @socket
  end

  raise MogileFS::UnreachableBackendError
end

#url_decode(str) ⇒ Object

Turns a url params string into a Hash.



266
267
268
269
270
# File 'lib/mogilefs/backend.rb', line 266

def url_decode(str)
  Hash[*(str.split(/&/).map { |pair|
    pair.split(/=/, 2).map { |x| url_unescape(x) }
  } ).flatten]
end

#url_encode(params) ⇒ Object

Turns a Hash (or Array of pairs) into a url params string.



275
276
277
278
279
# File 'lib/mogilefs/backend.rb', line 275

def url_encode(params)
  params.map do |k,v|
    "#{url_escape k.to_s}=#{url_escape v.to_s}"
  end.join("&")
end

#url_escape(str) ⇒ Object

Ruby 1.8



288
289
290
# File 'lib/mogilefs/backend.rb', line 288

def url_escape(str)
  str.gsub(/([^\w\,\-.\/\\\: ])/) { "%%%02x" % $1.ord }.tr(' ', '+')
end

#url_unescape(str) ⇒ Object

Unescapes naughty URL characters.



296
297
298
# File 'lib/mogilefs/backend.rb', line 296

def url_unescape(str)
  str.gsub(/%([a-f0-9][a-f0-9])/i) { [$1.to_i(16)].pack 'C' }.tr('+', ' ')
end