em_aws 0.3.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/em-aws.rb ADDED
@@ -0,0 +1,17 @@
1
+ require 'em-http'
2
+ require 'em-synchrony'
3
+ require 'em-synchrony/em-http'
4
+ require 'em-hot_tub'
5
+ require 'aws-sdk-v1'
6
+ require_relative 'em-aws/patches'
7
+ require_relative 'em-aws/version'
8
+ require_relative 'em-aws/http_handler'
9
+
10
+ AWS.eager_autoload! # lazy load isn't thread safe
11
+
12
+ module EventMachine
13
+ module AWS;end
14
+ end
15
+
16
+ # Backwards compatibility
17
+ EmAws = EventMachine::AWS
@@ -0,0 +1,188 @@
1
+ # http://docs.amazonwebservices.com/AWSRubySDK/latest/
2
+ require 'hot_tub'
3
+ require 'em-synchrony'
4
+ require 'em-synchrony/em-http'
5
+ require 'em-synchrony/thread'
6
+ module EventMachine
7
+ module AWS
8
+
9
+ # An em-http-request handler for the aws-sdk for fiber based asynchronous ruby application.
10
+ # See https://github.com/igrigorik/async-rails and
11
+ # http://www.mikeperham.com/2010/04/03/introducing-phat-an-asynchronous-rails-app/
12
+ # for examples of Aync-Rails application
13
+ #
14
+ # In Rails add the following to your aws.rb initializer
15
+ #
16
+ # require 'aws-sdk'
17
+ # require 'aws/core/http/em_http_handler'
18
+ # AWS.config(
19
+ # :http_handler => AWS::Http::EMHttpHandler.new(
20
+ # :proxy => {:host => '127.0.0.1', # proxy address
21
+ # :port => 9000, # proxy port
22
+ # :type => :socks5},
23
+ # :pool_size => 10, # Default is 10
24
+ # :max_size => 40, # Maximum size of pool, nil by default so pool can grow to meet concurrency under load
25
+ # :reap_timeout => 600, # How long to wait to reap connections after load dies down
26
+ # :async => false)) # If set to true all requests are handle asynchronously and initially return nil
27
+ #
28
+ # EM-AWS exposes all connections options for EM-Http-Request at initialization
29
+ # For more information on available options see https://github.com/igrigorik/em-http-request/wiki/Issuing-Requests#available-connection--request-parameters
30
+ # If Options from the request section of the above link are present, they
31
+ # set on every request but may be over written by the request object
32
+ class HttpHandler < ::AWS::Core::Http::NetHttpHandler
33
+
34
+ attr_reader :default_options, :client_options, :pool_options
35
+
36
+ # Constructs a new HTTP handler using EM-Synchrony.
37
+ # @param [Hash] options Default options to send to EM-Synchrony on
38
+ # each request. These options will be sent to +get+, +post+,
39
+ # +head+, +put+, or +delete+ when a request is made. Note
40
+ # that +:body+, +:head+, +:parser+, and +:ssl_ca_file+ are
41
+ # ignored. If you need to set the CA file see:
42
+ # https://github.com/igrigorik/em-http-request/wiki/Issuing-Requests#available-connection--request-parameters
43
+ def initialize options = {}
44
+ @default_options = options
45
+ @pool_options = fetch_pool_options
46
+ @client_options = fetch_client_options
47
+ @verify_content_length = options[:verify_response_body_content_length]
48
+ @sessions = EM::HotTub::Sessions.new(pool_options) do |url|
49
+ EM::HttpRequest.new(url,@client_options)
50
+ end
51
+ end
52
+
53
+ def handle(request,response,&read_block)
54
+ process_request(request,response,&read_block)
55
+ end
56
+
57
+ # If the request option :async are set to true that request will handled
58
+ # asynchronously returning nil initially and processing in the background
59
+ # managed by EM-Synchrony. If the client option :async all requests will
60
+ # be handled asynchronously.
61
+ # EX:
62
+ # EM.synchrony do
63
+ # s3 = AWS::S3.new
64
+ # s3.obj.write('test', :async => true) => nil
65
+ # EM::Synchrony.sleep(2)
66
+ # s3.obj.read => # 'test'
67
+ # EM.stop
68
+ # end
69
+ def handle_async(request,response,handle,&read_block)
70
+ process_request(request,response,true,&read_block)
71
+ end
72
+
73
+ private
74
+
75
+ REMOVE_OPTIONS = [:pool_size,:max_size, :never_block, :blocking_timeout].freeze
76
+
77
+ def fetch_client_options
78
+ co = @default_options.select{ |k,v| !REMOVE_OPTIONS.include?(k) }
79
+ co[:inactivity_timeout] ||= 0.to_i
80
+ co[:connect_timeout] ||= 10
81
+ co[:keepalive] = true unless co.key?(:keepalive)
82
+ co
83
+ end
84
+
85
+ def fetch_pool_options
86
+ po = {}
87
+ po[:wait_timeout] = (@default_options[:wait_timeout] || @default_options[:blocking_timeout] || 10).to_i
88
+ po[:size] = (@default_options[:pool_size] || 5).to_i
89
+ po[:size] = 1 if po[:size] < 1
90
+ po[:max_size] = @default_options[:max_size].to_i if @default_options[:max_size]
91
+ po[:reap_timeout] = @default_options[:reap_timeout].to_i if @default_options[:reap_timeout]
92
+ po
93
+ end
94
+
95
+ def fetch_url(request)
96
+ "#{(request.use_ssl? ? "https" : "http")}://#{request.host}:#{request.port}"
97
+ end
98
+
99
+ def fetch_headers(request)
100
+ headers = { 'content-type' => '' }
101
+ request.headers.each_pair do |key,value|
102
+ headers[key] = value.to_s
103
+ end
104
+ {:head => headers}
105
+ end
106
+
107
+ def fetch_request_options(request)
108
+ opts = @client_options.merge(fetch_headers(request))
109
+ opts[:query] = request.querystring
110
+ if request.body_stream.respond_to?(:path)
111
+ opts[:file] = request.body_stream.path
112
+ else
113
+ opts[:body] = request.body.to_s
114
+ end
115
+ opts[:path] = request.path if request.path
116
+ opts
117
+ end
118
+
119
+ def pool(url)
120
+ @sessions.get_or_set(url, @pool_options) { EM::HttpRequest.new(url,@client_options) }
121
+ end
122
+
123
+ def fetch_response(request,opts={},&read_block)
124
+ method = "a#{request.http_method}".downcase.to_sym # aget, apost, aput, adelete, ahead
125
+ url = fetch_url(request)
126
+ result = nil
127
+ if @sessions
128
+ pool(url).run do |connection|
129
+ req = connection.send(method, opts)
130
+ req.stream &read_block if block_given?
131
+ result = EM::Synchrony.sync req unless opts[:async]
132
+ end
133
+ else
134
+ clnt_opts = @client_options.merge(:inactivity_timeout => request.read_timeout)
135
+ req = EM::HttpRequest.new(url,clnt_opts).send(method,opts)
136
+ req.stream &read_block if block_given? and req.response_header.status.to_i < 300
137
+ result = EM::Synchrony.sync req unless opts[:async]
138
+ end
139
+ result
140
+ end
141
+
142
+ # AWS needs all header keys downcased and values need to be arrays
143
+ def fetch_response_headers(response)
144
+ response_headers = response.response_header.raw.to_hash
145
+ aws_headers = {}
146
+ response_headers.each_pair do |k,v|
147
+ key = k.downcase
148
+ #['x-amz-crc32', 'x-amz-expiration','x-amz-restore','x-amzn-errortype']
149
+ if v.is_a?(Array)
150
+ aws_headers[key] = v
151
+ else
152
+ aws_headers[key] = [v]
153
+ end
154
+ end
155
+ response_headers.merge(aws_headers)
156
+ end
157
+
158
+ # Builds and attempts the request. Occasionally under load em-http-request
159
+ # em-http-request returns a status of 0 for various http timeouts, see:
160
+ # https://github.com/igrigorik/em-http-request/issues/76
161
+ # https://github.com/eventmachine/eventmachine/issues/175
162
+ def process_request(request,response,async=false,&read_block)
163
+ opts = fetch_request_options(request)
164
+ opts[:async] = (async || opts[:async])
165
+ exp_length = determine_expected_content_length(response)
166
+ begin
167
+ http_response = fetch_response(request,opts,&read_block)
168
+
169
+ unless opts[:async]
170
+ response.status = http_response.response_header.status.to_i
171
+ raise Timeout::Error if response.status == 0
172
+ response.headers = fetch_response_headers(http_response)
173
+ response.body = http_response.response
174
+ end
175
+
176
+ run_check = exp_length && request.http_method != "HEAD" && @verify_content_length
177
+ if run_check && response.body && response.body.bytesize != exp_length
178
+ raise TruncatedBodyError, 'content-length does not match'
179
+ end
180
+ rescue *NETWORK_ERRORS => error
181
+ raise error if block_given?
182
+ response.network_error = error
183
+ end
184
+ nil
185
+ end
186
+ end
187
+ end
188
+ end
File without changes
@@ -0,0 +1,5 @@
1
+ module EventMachine
2
+ module AWS
3
+ VERSION = "1.0.0"
4
+ end
5
+ end
data/lib/em_aws.rb CHANGED
@@ -1,12 +1 @@
1
- require 'em_aws/patches'
2
- require 'aws-sdk'
3
- require 'em_aws/version'
4
- require 'em-http'
5
- require 'em-synchrony'
6
- require 'em-synchrony/em-http'
7
- require 'aws/core/http/em_http_handler'
8
- require 'hot_tub'
9
-
10
- AWS.eager_autoload! # lazy load isn't thread safe
11
- HotTub.logger = AWS.config.logger if AWS.config.logger
12
- module EmAws;end
1
+ require_relative 'em-aws'
@@ -0,0 +1,297 @@
1
+ # Copyright 2011 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You
4
+ # may not use this file except in compliance with the License. A copy of
5
+ # the License is located at
6
+ #
7
+ # http://aws.amazon.com/apache2.0/
8
+ #
9
+ # or in the "license" file accompanying this file. This file is
10
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
+ # ANY KIND, either express or implied. See the License for the specific
12
+ # language governing permissions and limitations under the License.
13
+
14
+ require 'spec_helper'
15
+ require 'eventmachine'
16
+ require 'evma_httpserver'
17
+ module AWS::Core
18
+ module Http
19
+ class EMFooIO
20
+ def path
21
+ "/my_path/test.text"
22
+ end
23
+ end
24
+
25
+ # A server for testing response,
26
+ # borrowed from: http://www.igvita.com/2008/05/27/ruby-eventmachine-the-speed-demon/
27
+ class AwsServer < EventMachine::Connection
28
+ include EventMachine::HttpServer
29
+
30
+ def process_http_request
31
+ resp = EventMachine::DelegatedHttpResponse.new( self )
32
+ resp.status = 200
33
+ resp.content = "Hello World!"
34
+ resp.send_response
35
+ end
36
+ end
37
+
38
+ # A slow server for testing timeout,
39
+ # borrowed from: http://www.igvita.com/2008/05/27/ruby-eventmachine-the-speed-demon/
40
+ class SlowServer < EventMachine::Connection
41
+ include EventMachine::HttpServer
42
+
43
+ def process_http_request
44
+ resp = EventMachine::DelegatedHttpResponse.new( self )
45
+
46
+ sleep 2 # Simulate a long running request
47
+
48
+ resp.status = 200
49
+ resp.content = "Hello World!"
50
+ resp.send_response
51
+ end
52
+ end
53
+
54
+ describe EM::AWS::HttpHandler do
55
+
56
+ around(:each) do |example|
57
+ EM.synchrony do
58
+ example.run
59
+ EM.stop
60
+ end
61
+ end
62
+
63
+ let(:handler_opts) do
64
+ {:verify_response_body_content_length => true}
65
+ end
66
+
67
+ let(:handler) { EM::AWS::HttpHandler.new(handler_opts) }
68
+
69
+ let(:request) do
70
+ double('aws-request',
71
+ :http_method => 'POST',
72
+ :endpoint => 'https://host.com',
73
+ :uri => '/path?querystring',
74
+ :path => '/path',
75
+ :host => 'host.com',
76
+ :port => 443,
77
+ :querystring => 'querystring',
78
+ :body_stream => StringIO.new('body'),
79
+ :body => 'body',
80
+ :use_ssl? => true,
81
+ :ssl_verify_peer? => true,
82
+ :ssl_ca_file => '/ssl/ca',
83
+ :ssl_ca_path => nil,
84
+ :read_timeout => 60,
85
+ :continue_timeout => 1,
86
+ :headers => { 'foo' => 'bar' })
87
+ end
88
+
89
+ let(:response) { Response.new }
90
+
91
+ let(:read_block) { }
92
+
93
+ let(:handle!) { handler.handle(request, response, &read_block) }
94
+
95
+ let(:http) { double('http-session').as_null_object }
96
+
97
+ let(:http_response) {
98
+ double = double('http response',
99
+ :code => '200',
100
+ :response => 'resp-body',
101
+ :to_hash => { 'header-name' => ['header-value'] })
102
+ allow(double).to receive(:stream) do |&block|
103
+ block ? block.call('resp-body') : 'resp-body'
104
+ end
105
+ double
106
+ }
107
+
108
+ before(:each) do
109
+ allow(http).to receive(:request).and_yield(http_response)
110
+ end
111
+
112
+ it 'should be accessible from AWS as well as AWS::Core' do
113
+ expect(AWS::Http::EMHttpHandler.new).to be_an(EM::AWS::HttpHandler)
114
+ end
115
+
116
+ describe '#handle' do
117
+ context 'exceptions' do
118
+ it 'should rescue Timeout::Error' do
119
+ allow(handler).to receive(:fetch_response).and_raise(Timeout::Error)
120
+
121
+ expect {
122
+ handle!
123
+ }.to_not raise_error
124
+ end
125
+
126
+ it 'should rescue Errno::ETIMEDOUT' do
127
+ allow(handler).to receive(:fetch_response).and_raise(Errno::ETIMEDOUT)
128
+
129
+ expect {
130
+ handle!
131
+ }.to_not raise_error
132
+ end
133
+
134
+ it 'should indicate that there was a network_error' do
135
+ allow(handler).to receive(:fetch_response).and_raise(Errno::ETIMEDOUT)
136
+
137
+ handle!
138
+
139
+ expect(response).to be_network_error
140
+ end
141
+ end
142
+
143
+ context 'default request options' do
144
+ before(:each) do
145
+ allow(handler).to receive(:default_request_options).and_return(:foo => "BAR", :private_key_file => "blarg")
146
+ end
147
+
148
+ it 'passes extra options through to synchrony' do
149
+ expect(handler.default_request_options[:foo]).to eql("BAR")
150
+ end
151
+
152
+ it 'uses the default when the request option is not set' do
153
+ #puts handler.default_request_options
154
+ expect(handler.default_request_options[:private_key_file]).to eql("blarg")
155
+ end
156
+ end
157
+ end
158
+
159
+ describe '#fetch_request_options' do
160
+ it "should set :query and :body to request.querystring" do
161
+ opts = handler.send(:fetch_request_options, request)
162
+ expect(opts[:query]).to eql(request.querystring)
163
+ end
164
+
165
+ it "should set :path to request.path" do
166
+ opts = handler.send(:fetch_request_options, request)
167
+ expect(opts[:path]).to eql(request.path)
168
+ end
169
+
170
+ context "request.body_stream is a StringIO" do
171
+ it "should set :body to request.body_stream" do
172
+ opts = handler.send(:fetch_request_options, request)
173
+ expect(opts[:body]).to eql("body")
174
+ end
175
+ end
176
+
177
+ context "request.body_stream is an object that responds to :path" do
178
+ let(:io_object) { EMFooIO.new }
179
+
180
+ before(:each) do
181
+ allow(request).to receive(:body_stream).and_return(io_object)
182
+ end
183
+
184
+ it "should set :file to object.path " do
185
+ opts = handler.send(:fetch_request_options, request)
186
+ expect(opts[:file]).to eql(io_object.path)
187
+ end
188
+ end
189
+ end
190
+
191
+ describe '#fetch_client_options' do
192
+ it "should remove pool related options" do
193
+ opts = handler.send(:fetch_client_options)
194
+
195
+ expect(opts.has_key?(:size)).to eql(false)
196
+ expect(opts.has_key?(:never_block)).to eql(false)
197
+ expect(opts.has_key?(:blocking_timeout)).to eql(false)
198
+ end
199
+
200
+ context "when keepalive is not set" do
201
+
202
+ it "should be true" do
203
+ opts = handler.send(:fetch_client_options)
204
+
205
+ expect(opts[:keepalive]).to eql(true)
206
+ end
207
+ end
208
+
209
+ context "when keepalive is false" do
210
+
211
+ it "should be false" do
212
+ handler.default_options[:keepalive] = false
213
+ opts = handler.send(:fetch_client_options)
214
+
215
+ expect(opts[:keepalive]).to eql(false)
216
+ end
217
+ end
218
+ end
219
+
220
+ context 'content-length checking' do
221
+
222
+ let(:http_response) {
223
+ double = double('http-response',
224
+ :code => '200',
225
+ :response => 'resp-body',
226
+ :to_hash => { 'content-length' => ["10000"] })
227
+ allow(double).to receive(:stream) do |&block|
228
+ block ? block.call('resp-body') : 'resp-body'
229
+ end
230
+ double
231
+ }
232
+
233
+ it 'should raise if content-length does not match' do
234
+ server = nil
235
+ EventMachine::run do
236
+ server = EventMachine::start_server '127.0.0.1', '8081', AwsServer
237
+ end
238
+ allow(handler).to receive(:fetch_url).and_return("http://127.0.0.1:8081")
239
+ allow(handler).to receive(:determine_expected_content_length).and_return(1)
240
+ handle!
241
+ expect(response.network_error).to be_a_kind_of(NetHttpHandler::TruncatedBodyError)
242
+ EventMachine.stop_server(server)
243
+ end
244
+
245
+ context 'can turn off length checking' do
246
+ let(:handler_opts) {{:verify_response_body_content_length => false}}
247
+
248
+ let(:handler) { described_class.new(handler_opts) }
249
+
250
+ it 'should not raise if length does not match but check is off' do
251
+ expect(response.network_error).to be_nil
252
+ end
253
+
254
+ end
255
+ end
256
+
257
+ context 'slow requests' do
258
+ context 'with inactivity_timeout = 0' do
259
+ it "should not timeout" do
260
+ server = nil
261
+ # turn on our test server
262
+ EventMachine::run do
263
+ server = EventMachine::start_server '127.0.0.1', '8081', SlowServer
264
+ end
265
+
266
+ allow(handler).to receive(:fetch_url).and_return("http://127.0.0.1:8081")
267
+
268
+ handle!
269
+
270
+ expect(response.network_error).to be_nil
271
+
272
+ EventMachine.stop_server(server)
273
+ end
274
+ end
275
+ context 'with inactivity_timeout > 0' do
276
+ it "should timeout" do
277
+ server = nil
278
+ # turn on our test server
279
+ EventMachine::run do
280
+ server = EventMachine::start_server '127.0.0.1', '8081', SlowServer
281
+ end
282
+
283
+ allow(handler).to receive(:fetch_url).and_return("http://127.0.0.1:8081")
284
+ handler.client_options[:inactivity_timeout] = 0.01
285
+ allow(handler).to receive(:connect_timeout).and_return(1) #just to speed up the test
286
+
287
+ handle!
288
+
289
+ expect(response.network_error).to be_a(Timeout::Error)
290
+
291
+ EventMachine.stop_server(server)
292
+ end
293
+ end
294
+ end
295
+ end
296
+ end
297
+ end