em_aws 0.3.2 → 1.0.0

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