pager-mogilefs-client 1.2.1.20080519

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,222 @@
1
+ require 'socket'
2
+ require 'thread'
3
+ require 'mogilefs'
4
+
5
+ ##
6
+ # MogileFS::Backend communicates with the MogileFS trackers.
7
+
8
+ class MogileFS::Backend
9
+
10
+ ##
11
+ # Adds MogileFS commands +names+.
12
+
13
+ def self.add_command(*names)
14
+ names.each do |name|
15
+ define_method name do |*args|
16
+ do_request name, args.first || {}
17
+ end
18
+ end
19
+ end
20
+
21
+ ##
22
+ # The last error
23
+ #--
24
+ # TODO Use Exceptions
25
+
26
+ attr_reader :lasterr
27
+
28
+ ##
29
+ # The string attached to the last error
30
+ #--
31
+ # TODO Use Exceptions
32
+
33
+ attr_reader :lasterrstr
34
+
35
+ ##
36
+ # Creates a new MogileFS::Backend.
37
+ #
38
+ # :hosts is a required argument and must be an Array containing one or more
39
+ # 'hostname:port' pairs as Strings.
40
+ #
41
+ # :timeout adjusts the request timeout before an error is returned.
42
+
43
+ def initialize(args)
44
+ @hosts = args[:hosts]
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"
49
+ end
50
+
51
+ @mutex = Mutex.new
52
+ @timeout = args[:timeout] || 3
53
+ @socket = nil
54
+ @lasterr = nil
55
+ @lasterrstr = nil
56
+
57
+ @dead = {}
58
+ end
59
+
60
+ ##
61
+ # Closes this backend's socket.
62
+
63
+ def shutdown
64
+ @socket.close unless @socket.nil? or @socket.closed?
65
+ @socket = nil
66
+ end
67
+
68
+ # MogileFS::MogileFS commands
69
+
70
+ add_command :create_open
71
+ add_command :create_close
72
+ add_command :get_paths
73
+ add_command :delete
74
+ add_command :sleep
75
+ add_command :rename
76
+ add_command :list_keys
77
+
78
+ # MogileFS::Backend commands
79
+
80
+ add_command :get_hosts
81
+ add_command :get_devices
82
+ add_command :list_fids
83
+ add_command :stats
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
94
+
95
+ private unless defined? $TESTING
96
+
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
103
+
104
+ ##
105
+ # Performs the +cmd+ request with +args+.
106
+
107
+ def do_request(cmd, args)
108
+ @mutex.synchronize do
109
+ request = make_request cmd, args
110
+
111
+ begin
112
+ bytes_sent = socket.send request, 0
113
+ rescue SystemCallError
114
+ @socket = nil
115
+ raise "couldn't connect to mogilefsd backend"
116
+ end
117
+
118
+ unless bytes_sent == request.length then
119
+ raise "request truncated (sent #{bytes_sent} expected #{request.length})"
120
+ end
121
+
122
+ readable?
123
+
124
+ return parse_response(socket.gets)
125
+ end
126
+ end
127
+
128
+ ##
129
+ # Makes a new request string for +cmd+ and +args+.
130
+
131
+ def make_request(cmd, args)
132
+ return "#{cmd} #{url_encode args}\r\n"
133
+ end
134
+
135
+ ##
136
+ # Turns the +line+ response from the server into a Hash of options, an
137
+ # error, or raises, as appropriate.
138
+
139
+ def parse_response(line)
140
+ if line =~ /^ERR\s+(\w+)\s*(.*)/ then
141
+ @lasterr = $1
142
+ @lasterrstr = $2 ? url_unescape($2) : nil
143
+ return nil
144
+ end
145
+
146
+ return url_decode($1) if line =~ /^OK\s+\d*\s*(\S*)/
147
+
148
+ raise "Invalid response from server: #{line.inspect}"
149
+ end
150
+
151
+ ##
152
+ # Raises if the socket does not become readable in +@timeout+ seconds.
153
+
154
+ 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
+ raise MogileFS::UnreadableSocketError, "#{peer}never became readable"
159
+ end
160
+ return true
161
+ end
162
+
163
+ ##
164
+ # Returns a socket connected to a MogileFS tracker.
165
+
166
+ def socket
167
+ return @socket if @socket and not @socket.closed?
168
+
169
+ now = Time.now
170
+
171
+ @hosts.sort_by { rand(3) - 1 }.each do |host|
172
+ next if @dead.include? host and @dead[host] > now - 5
173
+
174
+ begin
175
+ @socket = connect_to(*host.split(':'))
176
+ rescue SystemCallError
177
+ @dead[host] = now
178
+ next
179
+ end
180
+
181
+ return @socket
182
+ end
183
+
184
+ raise "couldn't connect to mogilefsd backend"
185
+ end
186
+
187
+ ##
188
+ # Turns a url params string into a Hash.
189
+
190
+ def url_decode(str)
191
+ pairs = str.split('&').map do |pair|
192
+ pair.split('=', 2).map { |v| url_unescape v }
193
+ end
194
+
195
+ return Hash[*pairs.flatten]
196
+ end
197
+
198
+ ##
199
+ # Turns a Hash (or Array of pairs) into a url params string.
200
+
201
+ def url_encode(params)
202
+ return params.map do |k,v|
203
+ "#{url_escape k.to_s}=#{url_escape v.to_s}"
204
+ end.join("&")
205
+ end
206
+
207
+ ##
208
+ # Escapes naughty URL characters.
209
+
210
+ def url_escape(str)
211
+ return str.gsub(/([^\w\,\-.\/\\\: ])/) { "%%%02x" % $1[0] }.tr(' ', '+')
212
+ end
213
+
214
+ ##
215
+ # Unescapes naughty URL characters.
216
+
217
+ def url_unescape(str)
218
+ return str.gsub(/%([a-f0-9][a-f0-9])/i) { [$1.to_i(16)].pack 'C' }.tr('+', ' ')
219
+ end
220
+
221
+ end
222
+
@@ -0,0 +1,65 @@
1
+ require 'mogilefs/backend'
2
+
3
+ ##
4
+ # MogileFS::Client is the MogileFS client base class. Concrete clients like
5
+ # MogileFS::MogileFS and MogileFS::Admin are implemented atop this one to do
6
+ # real work.
7
+
8
+ class MogileFS::Client
9
+
10
+ ##
11
+ # The backend connection for this client
12
+
13
+ attr_reader :backend
14
+
15
+ attr_accessor :hosts if defined? $TESTING
16
+
17
+ ##
18
+ # Creates a new Client. See MogileFS::Backend#initialize for how to specify
19
+ # hosts. If :readonly is set to true, the client will not modify anything
20
+ # on the server.
21
+ #
22
+ # MogileFS::Client.new :hosts => ['kaa:6001', 'ziz:6001'], :readonly => true
23
+
24
+ def initialize(args)
25
+ @hosts = args[:hosts]
26
+ @readonly = args[:readonly] ? true : false
27
+ @timeout = args[:timeout]
28
+
29
+ reload
30
+ end
31
+
32
+ ##
33
+ # Creates a new MogileFS::Backend.
34
+
35
+ def reload
36
+ @backend = MogileFS::Backend.new :hosts => @hosts, :timeout => @timeout
37
+ end
38
+
39
+ ##
40
+ # The last error reported by the backend.
41
+ #--
42
+ # TODO use Exceptions
43
+
44
+ def err
45
+ @backend.lasterr
46
+ end
47
+
48
+ ##
49
+ # The last error message reported by the backend.
50
+ #--
51
+ # TODO use Exceptions
52
+
53
+ def errstr
54
+ @backend.lasterrstr
55
+ end
56
+
57
+ ##
58
+ # Is this a read-only client?
59
+
60
+ def readonly?
61
+ return @readonly
62
+ end
63
+
64
+ end
65
+
@@ -0,0 +1,118 @@
1
+ require 'fcntl'
2
+ require 'socket'
3
+ require 'stringio'
4
+ require 'uri'
5
+ require 'mogilefs/backend'
6
+
7
+ ##
8
+ # HTTPFile wraps up the new file operations for storing files onto an HTTP
9
+ # storage node.
10
+ #
11
+ # You really don't want to create an HTTPFile by hand. Instead you want to
12
+ # create a new file using MogileFS::MogileFS.new_file.
13
+ #
14
+ # WARNING! HTTP mode is completely untested as I cannot make it work on
15
+ # FreeBSD. Please send patches/tests if you find bugs.
16
+ #--
17
+ # TODO dup'd content in MogileFS::NFSFile
18
+
19
+ class MogileFS::HTTPFile < StringIO
20
+
21
+ ##
22
+ # The path this file will be stored to.
23
+
24
+ attr_reader :path
25
+
26
+ ##
27
+ # The key for this file. This key won't represent a real file until you've
28
+ # called #close.
29
+
30
+ attr_reader :key
31
+
32
+ ##
33
+ # The class of this file.
34
+
35
+ attr_reader :class
36
+
37
+ ##
38
+ # Works like File.open. Use MogileFS::MogileFS#new_file instead of this
39
+ # method.
40
+
41
+ def self.open(*args)
42
+ fp = new(*args)
43
+
44
+ return fp unless block_given?
45
+
46
+ begin
47
+ yield fp
48
+ ensure
49
+ fp.close
50
+ end
51
+ end
52
+
53
+ ##
54
+ # Creates a new HTTPFile with MogileFS-specific data. Use
55
+ # MogileFS::MogileFS#new_file instead of this method.
56
+
57
+ def initialize(mg, fid, path, devid, klass, key, dests, content_length)
58
+ super ''
59
+ @mg = mg
60
+ @fid = fid
61
+ @path = path
62
+ @devid = devid
63
+ @klass = klass
64
+ @key = key
65
+
66
+ @dests = dests.map { |(_,u)| URI.parse u }
67
+ @tried = {}
68
+
69
+ @socket = nil
70
+ end
71
+
72
+ ##
73
+ # Closes the file handle and marks it as closed in MogileFS.
74
+
75
+ def close
76
+ connect_client
77
+
78
+ resp = @client.put(@path.request_uri, :body => string)
79
+ raise "HTTP response status from upload: #{resp.http_status}" unless resp.http_status.to_i == 200
80
+
81
+ @mg.backend.create_close(:fid => @fid, :devid => @devid,
82
+ :domain => @mg.domain, :key => @key,
83
+ :path => @path, :size => length)
84
+ return nil
85
+ end
86
+
87
+ private
88
+
89
+ def connected?
90
+ return !(@client.nil?)
91
+ end
92
+
93
+ def connect_client
94
+ return @client if connected?
95
+
96
+ next_path
97
+
98
+ if @path.nil? then
99
+ @tried.clear
100
+ next_path
101
+ raise 'Unable to open socket to storage node' if @path.nil?
102
+ end
103
+
104
+ @client = RFuzz::HttpClient.new(@path.host, @path.port)
105
+ end
106
+
107
+ def next_path
108
+ @path = nil
109
+ @dests.each do |dest|
110
+ unless @tried.include? dest then
111
+ @path = dest
112
+ return
113
+ end
114
+ end
115
+ end
116
+
117
+ end
118
+
@@ -0,0 +1,237 @@
1
+ require 'open-uri'
2
+ require 'timeout'
3
+
4
+ require 'rfuzz/client'
5
+
6
+ require 'mogilefs/client'
7
+ require 'mogilefs/nfsfile'
8
+
9
+ ##
10
+ # Timeout error class.
11
+
12
+ class MogileFS::Timeout < Timeout::Error; end
13
+
14
+ ##
15
+ # MogileFS File manipulation client.
16
+
17
+ class MogileFS::MogileFS < MogileFS::Client
18
+
19
+ ##
20
+ # The path to the local MogileFS mount point if you are using NFS mode.
21
+
22
+ attr_reader :root
23
+
24
+ ##
25
+ # The domain of keys for this MogileFS client.
26
+
27
+ attr_reader :domain
28
+
29
+ ##
30
+ # Creates a new MogileFS::MogileFS instance. +args+ must include a key
31
+ # :domain specifying the domain of this client. A key :root will be used to
32
+ # specify the root of the NFS file system.
33
+
34
+ def initialize(args = {})
35
+ @domain = args[:domain]
36
+ @root = args[:root]
37
+
38
+ raise ArgumentError, "you must specify a domain" unless @domain
39
+
40
+ super
41
+ end
42
+
43
+ ##
44
+ # Enumerates keys starting with +key+.
45
+
46
+ def each_key(prefix)
47
+ after = nil
48
+
49
+ keys, after = list_keys prefix
50
+
51
+ until keys.empty? do
52
+ keys.each { |k| yield k }
53
+ keys, after = list_keys prefix, after
54
+ end
55
+
56
+ return nil
57
+ end
58
+
59
+ ##
60
+ # Retrieves the contents of +key+.
61
+
62
+ def get_file_data(key)
63
+ paths = get_paths key
64
+
65
+ return nil unless paths
66
+
67
+ paths.each do |path|
68
+ next unless path
69
+ case path
70
+ when /^http:\/\// then
71
+ begin
72
+ path = URI.parse path
73
+ data = timeout(5, MogileFS::Timeout) {
74
+ RFuzz::HttpClient.new(path.host, path.port).get(path).http_body
75
+ }
76
+ return data
77
+ rescue MogileFS::Timeout, RFuzz::HttpClientError, RFuzz::HttpClientParserError
78
+ next
79
+ end
80
+ else
81
+ next unless File.exist? path
82
+ return File.read(path)
83
+ end
84
+ end
85
+
86
+ return nil
87
+ end
88
+
89
+ ##
90
+ # Get the paths for +key+.
91
+
92
+ def get_paths(key, noverify = true, zone = nil)
93
+ noverify = noverify ? 1 : 0
94
+ res = @backend.get_paths(:domain => @domain, :key => key,
95
+ :noverify => noverify, :zone => zone)
96
+
97
+ return nil if res.nil? and @backend.lasterr == 'unknown_key'
98
+ paths = (1..res['paths'].to_i).map { |i| res["path#{i}"] }
99
+ return paths if paths.empty?
100
+ return paths if paths.first =~ /^http:\/\//
101
+ return paths.map { |path| File.join @root, path }
102
+ end
103
+
104
+ ##
105
+ # Creates a new file +key+ in +klass+. +bytes+ is currently unused.
106
+ #
107
+ # The +block+ operates like File.open.
108
+
109
+ def new_file(key, klass, bytes = 0, &block) # :yields: file
110
+ raise 'readonly mogilefs' if readonly?
111
+
112
+ res = @backend.create_open(:domain => @domain, :class => klass,
113
+ :key => key, :multi_dest => 1)
114
+
115
+ raise "#{@backend.lasterr}: #{@backend.lasterrstr}" if res.nil? # HACK
116
+
117
+ dests = nil
118
+
119
+ if res.include? 'dev_count' then # HACK HUH?
120
+ dests = (1..res['dev_count'].to_i).map do |i|
121
+ [res["devid_#{i}"], res["path_#{i}"]]
122
+ end
123
+ else
124
+ # 0x0040: d0e4 4f4b 2064 6576 6964 3d31 2666 6964 ..OK.devid=1&fid
125
+ # 0x0050: 3d33 2670 6174 683d 6874 7470 3a2f 2f31 =3&path=http://1
126
+ # 0x0060: 3932 2e31 3638 2e31 2e37 323a 3735 3030 92.168.1.72:7500
127
+ # 0x0070: 2f64 6576 312f 302f 3030 302f 3030 302f /dev1/0/000/000/
128
+ # 0x0080: 3030 3030 3030 3030 3033 2e66 6964 0d0a 0000000003.fid..
129
+
130
+ dests = [[res['devid'], res['path']]]
131
+ end
132
+
133
+ dest = dests.first
134
+ devid, path = dest
135
+
136
+ case path
137
+ when /^http:\/\// then
138
+ MogileFS::HTTPFile.open(self, res['fid'], path, devid, klass, key,
139
+ dests, bytes, &block)
140
+ else
141
+ MogileFS::NFSFile.open(self, res['fid'], path, devid, klass, key, &block)
142
+ end
143
+ end
144
+
145
+ ##
146
+ # Copies the contents of +file+ into +key+ in class +klass+. +file+ can be
147
+ # either a file name or an object that responds to #read.
148
+
149
+ def store_file(key, klass, file)
150
+ raise 'readonly mogilefs' if readonly?
151
+
152
+ new_file key, klass do |mfp|
153
+ if file.respond_to? :read then
154
+ return copy(file, mfp)
155
+ else
156
+ return File.open(file) { |fp| copy(fp, mfp) }
157
+ end
158
+ end
159
+ end
160
+
161
+ ##
162
+ # Stores +content+ into +key+ in class +klass+.
163
+
164
+ def store_content(key, klass, content)
165
+ raise 'readonly mogilefs' if readonly?
166
+
167
+ new_file key, klass do |mfp|
168
+ mfp << content
169
+ end
170
+
171
+ return content.length
172
+ end
173
+
174
+ ##
175
+ # Removes +key+.
176
+
177
+ def delete(key)
178
+ raise 'readonly mogilefs' if readonly?
179
+
180
+ res = @backend.delete :domain => @domain, :key => key
181
+
182
+ if res.nil? and @backend.lasterr != 'unknown_key' then
183
+ raise "unable to delete #{key}: #{@backend.lasterr}"
184
+ end
185
+ end
186
+
187
+ ##
188
+ # Sleeps +duration+.
189
+
190
+ def sleep(duration)
191
+ @backend.sleep :duration => duration
192
+ end
193
+
194
+ ##
195
+ # Renames a key +from+ to key +to+.
196
+
197
+ def rename(from, to)
198
+ raise 'readonly mogilefs' if readonly?
199
+
200
+ res = @backend.rename :domain => @domain, :from_key => from, :to_key => to
201
+
202
+ if res.nil? and @backend.lasterr != 'unknown_key' then
203
+ raise "unable to rename #{from_key} to #{to_key}: #{@backend.lasterr}"
204
+ end
205
+ end
206
+
207
+ ##
208
+ # Lists keys starting with +prefix+ follwing +after+ up to +limit+. If
209
+ # +after+ is nil the list starts at the beginning.
210
+
211
+ def list_keys(prefix, after = nil, limit = 1000)
212
+ res = @backend.list_keys(:domain => domain, :prefix => prefix,
213
+ :after => after, :limit => limit)
214
+
215
+ return nil if res.nil?
216
+
217
+ keys = (1..res['key_count'].to_i).map { |i| res["key_#{i}"] }
218
+
219
+ return keys, res['next_after']
220
+ end
221
+
222
+ private
223
+
224
+ def copy(from, to) # HACK use FileUtils
225
+ bytes = 0
226
+
227
+ until from.eof? do
228
+ chunk = from.read 8192
229
+ to.write chunk
230
+ bytes += chunk.length
231
+ end
232
+
233
+ return bytes
234
+ end
235
+
236
+ end
237
+