pager-mogilefs-client 1.2.1.20080519

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.
@@ -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
+