6 # MogileFS::Backend communicates with the MogileFS trackers.
8 class MogileFS::Backend
11 # Adds MogileFS commands +names+.
13 def self.add_command(*names)
15 define_method name do |*args|
16 do_request name, args.first || {}
29 # The string attached to the last error
33 attr_reader :lasterrstr
36 # Creates a new MogileFS::Backend.
38 # :hosts is a required argument and must be an Array containing one or more
39 # 'hostname:port' pairs as Strings.
41 # :timeout adjusts the request timeout before an error is returned.
45 raise ArgumentError, "must specify at least one host" unless @hosts
46 raise ArgumentError, "must specify at least one host" if @hosts.empty?
47 unless @hosts == @hosts.select { |h| h =~ /:\d+$/ } then
48 raise ArgumentError, ":hosts must be in 'host:port' form"
52 @timeout = args[:timeout] || 3
61 # Closes this backend's socket.
64 @socket.close unless @socket.nil? or @socket.closed?
68 # MogileFS::MogileFS commands
70 add_command :create_open
71 add_command :create_close
72 add_command :get_paths
76 add_command :list_keys
78 # MogileFS::Backend commands
80 add_command :get_hosts
81 add_command :get_devices
82 add_command :list_fids
84 add_command :get_domains
85 add_command :create_domain
86 add_command :delete_domain
87 add_command :create_class
88 add_command :update_class
89 add_command :delete_class
90 add_command :create_host
91 add_command :update_host
92 add_command :delete_host
93 add_command :set_state
95 private unless defined? $TESTING
98 # Returns a new TCPSocket connected to +port+ on +host+.
100 def connect_to(host, port)
101 return TCPSocket.new(host, port)
105 # Performs the +cmd+ request with +args+.
107 def do_request(cmd, args)
108 @mutex.synchronize do
109 request = make_request cmd, args
112 bytes_sent = socket.send request, 0
113 rescue SystemCallError
115 raise "couldn't connect to mogilefsd backend"
118 unless bytes_sent == request.length then
119 raise "request truncated (sent #{bytes_sent} expected #{request.length})"
124 return parse_response(socket.gets)
129 # Makes a new request string for +cmd+ and +args+.
131 def make_request(cmd, args)
132 return "#{cmd} #{url_encode args}\r\n"
136 # Turns the +line+ response from the server into a Hash of options, an
137 # error, or raises, as appropriate.
139 def parse_response(line)
140 if line =~ /^ERR\s+(\w+)\s*(.*)/ then
142 @lasterrstr = $2 ? url_unescape($2) : nil
146 return url_decode($1) if line =~ /^OK\s+\d*\s*(\S*)/
148 raise "Invalid response from server: #{line.inspect}"
152 # Raises if the socket does not become readable in +@timeout+ seconds.
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 raise MogileFS::UnreadableSocketError, "#{peer}never became readable"
164 # Returns a socket connected to a MogileFS tracker.
167 return @socket if @socket and not @socket.closed?
171 @hosts.sort_by { rand(3) - 1 }.each do |host|
172 next if @dead.include? host and @dead[host] > now - 5
175 @socket = connect_to(*host.split(':'))
176 rescue SystemCallError
184 raise "couldn't connect to mogilefsd backend"
188 # Turns a url params string into a Hash.
191 pairs = str.split('&').map do |pair|
192 pair.split('=', 2).map { |v| url_unescape v }
195 return Hash[*pairs.flatten]
199 # Turns a Hash (or Array of pairs) into a url params string.
201 def url_encode(params)
202 return params.map do |k,v|
203 "#{url_escape k.to_s}=#{url_escape v.to_s}"
208 # Escapes naughty URL characters.
211 return str.gsub(/([^\w\,\-.\/\\\: ])/) { "%%%02x" % $1[0] }.tr(' ', '+')
215 # Unescapes naughty URL characters.
217 def url_unescape(str)
218 return str.gsub(/%([a-f0-9][a-f0-9])/i) { [$1.to_i(16)].pack 'C' }.tr('+', ' ')