web_fetch 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 22d65c1496314f4ab5ce0f6393e6c59209ccc3dff7e13657eb574111187c9674
4
- data.tar.gz: 172a59a513ac5073b2071cc82c6a20b8143a426e77139c12bf015d40ea558b94
3
+ metadata.gz: aab3d826db4d54d4ecd8f935086af80ee34edf38b80c62cfeaed980a5e045f31
4
+ data.tar.gz: dd4818dd4dcfb3949b37ae124370871906558e185f573ca6f9f9c0a65e1c33a4
5
5
  SHA512:
6
- metadata.gz: 8c57ff4c160cd76b68a84e16ddd8cc7976041b9e6c6d675ad08d616c060577e680491a30fbc5ab1fb6215bc017dafccb6d84e0c679d5bacea871b4fbfd11f063
7
- data.tar.gz: 61481a84084e8837d1194199ccd3454cfe659d789e1eaff559779d5286ec446ae6417baa09cae10d66f2493612fb80c90c7ec75a8f9215ce1461a8684aaf233d
6
+ metadata.gz: f525afda585cc0f785db99477a0f6cda72cca602cc0a84bc6726c72214c76a8074069a9d034c9c050cc4a1815f73ca164104755c8325b86361314d1a9eeb05bf
7
+ data.tar.gz: 9f5678f845dee9af389ff24b46f96189c06ca6255dd8ba88da1b265a4bad1732736b8a9cfc547500f3c6e157c3250fcf4c84ab93558232149251be9f61ac5fdf
data/README.md CHANGED
@@ -96,6 +96,7 @@ result.headers
96
96
  result.status # HTTP status code
97
97
  result.success? # False if a network error (not HTTP error) occurred
98
98
  result.error # Underlying network error if applicable
99
+ result.response_time
99
100
  ```
100
101
 
101
102
  Note that `WebFech::Promise#fetch` will block until the result is complete by default. If you want to continue executing other code if the result is not ready (e.g. to see if any other results are ready), you can pass `wait: false`
@@ -10,7 +10,7 @@ require 'json'
10
10
  require 'digest'
11
11
  require 'securerandom'
12
12
  require 'faraday'
13
- require 'childprocess'
13
+ require 'subprocess'
14
14
  require 'active_support/gzip'
15
15
 
16
16
  locales_path = File.expand_path('../config/locales/*.yml', __dir__)
@@ -26,10 +26,9 @@ end
26
26
 
27
27
  require 'web_fetch/logger'
28
28
  require 'web_fetch/helpers'
29
- require 'web_fetch/event_machine_helpers'
30
- require 'web_fetch/http_helpers'
31
- require 'web_fetch/concerns/validatable'
29
+ require 'web_fetch/concerns/event_machine_helpers'
32
30
  require 'web_fetch/concerns/http_helpers'
31
+ require 'web_fetch/concerns/validatable'
33
32
  require 'web_fetch/concerns/client_http'
34
33
  require 'web_fetch/storage'
35
34
  require 'web_fetch/server'
@@ -16,17 +16,17 @@ module WebFetch
16
16
 
17
17
  def self.create(host, port, options = {})
18
18
  # Will block until process is responsive
19
- process = spawn(host, port, options)
19
+ process = build_process(host, port, options)
20
20
  client = new(host, port, process: process)
21
21
  sleep 0.1 until client.alive?
22
22
  client
23
23
  end
24
24
 
25
25
  def stop
26
- # Will block until process dies
27
26
  return if @process.nil?
28
27
 
29
- @process.stop
28
+ @process.terminate
29
+ # Will block until process dies
30
30
  @process.wait
31
31
  end
32
32
 
@@ -77,22 +77,16 @@ module WebFetch
77
77
  end
78
78
 
79
79
  class << self
80
- def spawn(host, port, options)
81
- process = build_process(host, port, options)
82
- process.cwd = File.join(File.dirname(__dir__), '..')
83
- process.io.inherit!
84
- process.start
85
- process
86
- end
87
-
88
- private
89
-
90
80
  def build_process(host, port, options)
91
81
  command = options.fetch(:start_command, standard_start_command)
92
82
  args = ['--host', host, '--port', port.to_s]
93
83
  args += ['--log', options[:log]] unless options[:log].nil?
94
84
  args.push('--daemonize') if options[:daemonize]
95
- ChildProcess.build(*command, *args)
85
+ Subprocess.popen(command + args, cwd: cwd)
86
+ end
87
+
88
+ def cwd
89
+ File.join(File.dirname(__dir__), '..')
96
90
  end
97
91
 
98
92
  def standard_start_command
@@ -112,13 +106,18 @@ module WebFetch
112
106
 
113
107
  def new_result(outcome)
114
108
  response = outcome[:response]
109
+ # FIXME: This is sort-of duplicated from `Promise#new_result` but we
110
+ # build it very slightly differently. This means we have to update in
111
+ # both places if we change the structure. Not quite sure how to unify
112
+ # this and ensure the same structure in both places.
115
113
  Result.new(
116
114
  body: response[:body],
117
115
  headers: response[:headers],
118
116
  status: response[:status],
119
117
  success: response[:success],
120
118
  error: response[:error],
121
- uid: outcome[:uid]
119
+ uid: outcome[:uid],
120
+ response_time: response[:response_time]
122
121
  )
123
122
  end
124
123
 
@@ -3,7 +3,9 @@
3
3
  module WebFetch
4
4
  # EventMachine layer-specific helpers
5
5
  module EventMachineHelpers
6
- def request_async(request)
6
+ def request_async(target)
7
+ request = target[:request]
8
+ target[:start_time] = Time.now.utc
7
9
  async_request = EM::HttpRequest.new(request[:url])
8
10
  method = request.fetch(:method, 'GET').downcase.to_sym
9
11
  async_request.public_send(
@@ -17,11 +19,13 @@ module WebFetch
17
19
  def apply_callbacks(request)
18
20
  request[:deferred].callback do
19
21
  Logger.debug("HTTP fetch complete for uid: #{request[:uid]}")
22
+ save_response_time(request)
20
23
  request[:succeeded] = true
21
24
  end
22
25
 
23
26
  request[:deferred].errback do
24
27
  Logger.debug("HTTP fetch failed for uid: #{request[:uid]}")
28
+ save_response_time(request)
25
29
  request[:failed] = true
26
30
  end
27
31
  end
@@ -45,5 +49,9 @@ module WebFetch
45
49
  end
46
50
  end
47
51
  end
52
+
53
+ def save_response_time(request)
54
+ request[:response_time] = Time.now.utc - request[:start_time]
55
+ end
48
56
  end
49
57
  end
@@ -1,15 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WebFetch
4
- module HttpHelpers
4
+ # Convenience methods for WebFetch HTTP layer
5
+ module HTTPHelpers
6
+ def respond_immediately(result, response)
7
+ response.status = result[:status]
8
+ response.content = compress(result[:payload].to_json)
9
+ response.send_response
10
+ end
11
+
12
+ def pending(result, response)
13
+ respond_immediately({
14
+ payload: {
15
+ uid: result[:request][:uid],
16
+ pending: true,
17
+ message: I18n.t(:pending)
18
+ }
19
+ }, response)
20
+ end
21
+
5
22
  def compress(string)
23
+ return string unless accept_gzip?
24
+
6
25
  ActiveSupport::Gzip.compress(string)
7
26
  end
8
27
 
9
28
  def default_headers(response)
10
29
  response.headers['Content-Type'] = 'application/json; charset=utf-8'
11
30
  response.headers['Cache-Control'] = 'max-age=0, private, must-revalidate'
12
- response.headers['Content-Encoding'] = 'gzip'
31
+ response.headers['Content-Encoding'] = 'gzip' if accept_gzip?
13
32
  response.headers['Vary'] = 'Accept-Encoding'
14
33
  end
15
34
 
@@ -26,39 +45,48 @@ module WebFetch
26
45
  JSON.parse(@http_post_content, symbolize_names: true)
27
46
  end
28
47
 
29
- def succeed(deferred, response)
48
+ def succeed(request, response)
30
49
  response.status = 200
31
- response.content = compress(JSON.dump(success(deferred)))
50
+ response.content = compress(JSON.dump(success(request)))
32
51
  response.send_response
52
+ storage.delete(request[:uid])
33
53
  end
34
54
 
35
- def success(deferred)
36
- result = deferred[:http]
55
+ def success(request)
56
+ result = request[:deferred]
37
57
  { response: {
38
58
  success: true,
39
59
  body: result.response,
40
60
  headers: result.headers,
41
- status: result.response_header.status
61
+ status: result.response_header.status,
62
+ response_time: request[:response_time]
42
63
  },
43
- uid: deferred[:uid] }
64
+ uid: request[:uid] }
44
65
  end
45
66
 
46
- def fail_(deferred, response)
67
+ def fail_(request, response)
47
68
  response.status = 200
48
- response.content = compress(JSON.dump(failure(deferred)))
69
+ response.content = compress(JSON.dump(failure(request)))
49
70
  response.send_response
71
+ storage.delete(request[:uid])
50
72
  end
51
73
 
52
- def failure(deferred)
53
- result = deferred[:http]
74
+ def failure(request)
75
+ result = request[:deferred]
54
76
  { response: {
55
77
  success: false,
56
78
  body: result.response,
57
79
  headers: result.headers,
58
80
  status: result.response_header.status,
81
+ response_time: request[:response_time],
59
82
  error: (result.error&.inspect)
60
83
  },
61
- uid: deferred[:uid] }
84
+ uid: request[:uid] }
85
+ end
86
+
87
+ def accept_gzip?
88
+ # em-http-request doesn't do us any favours with parsing the HTTP headers
89
+ @http_headers.downcase.include?('accept-encoding: gzip')
62
90
  end
63
91
  end
64
92
  end
@@ -62,13 +62,16 @@ module WebFetch
62
62
  end
63
63
 
64
64
  def new_result(response)
65
+ # XXX: Any changes to this structure need to be reflected by
66
+ # `Client#new_result`
65
67
  Result.new(
66
68
  body: response[:body],
67
69
  headers: response[:headers],
68
70
  status: response[:status],
69
71
  success: @raw_result[:response][:success],
70
72
  error: @raw_result[:response][:error],
71
- uid: @uid
73
+ uid: @uid,
74
+ response_time: @raw_result[:response_time]
72
75
  )
73
76
  end
74
77
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module WebFetch
4
4
  class Result
5
- attr_reader :body, :headers, :status, :error, :uid
5
+ attr_reader :body, :headers, :status, :error, :uid, :response_time
6
6
 
7
7
  def initialize(options = {})
8
8
  @pending = options.fetch(:pending, false)
@@ -14,6 +14,7 @@ module WebFetch
14
14
  @success = options.fetch(:success)
15
15
  @error = options.fetch(:error)
16
16
  @uid = options.fetch(:uid)
17
+ @response_time = options.fetch(:response_time)
17
18
  end
18
19
 
19
20
  def pending?
@@ -17,7 +17,6 @@ module WebFetch
17
17
  def find
18
18
  request = @server.storage.fetch(@uid)
19
19
  return not_found if request.nil?
20
- return not_found if request.nil?
21
20
  return request.merge(pending: true) if pending?(request)
22
21
 
23
22
  request
@@ -42,6 +41,7 @@ module WebFetch
42
41
  def pending?(request)
43
42
  return false if request.nil?
44
43
  return false if request[:succeeded]
44
+ return false if request[:failed]
45
45
  # User requested blocking operation so we will wait until item is ready
46
46
  # rather than return a `pending` status
47
47
  return false if @block
@@ -29,8 +29,10 @@ module WebFetch
29
29
  # #process_http_request and subsequently WebFetch::Router#route
30
30
  def gather(targets)
31
31
  targets.each do |target|
32
- http = request_async(target[:request])
33
- request = { uid: target[:uid], deferred: http }
32
+ http = request_async(target)
33
+ request = { uid: target[:uid],
34
+ start_time: target[:start_time],
35
+ deferred: http }
34
36
  apply_callbacks(request)
35
37
  @storage.store(target[:uid], request)
36
38
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WebFetch
4
- VERSION = '0.2.1'
4
+ VERSION = '0.3.0'
5
5
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  describe WebFetch::Client do
4
- let(:client) { described_class.new('localhost', 8089, log: File::NULL) }
4
+ let(:client) { described_class.new('localhost', 60_085, log: File::NULL) }
5
5
 
6
6
  before(:each) do
7
7
  stub_request(:any, 'http://blah.blah/success')
@@ -63,6 +63,7 @@ describe WebFetch::Client do
63
63
  subject { client.fetch(responses.first.uid) }
64
64
 
65
65
  it { is_expected.to be_a WebFetch::Result }
66
+
66
67
  context 'no matching request found' do
67
68
  subject { proc { client.fetch('not-found') } }
68
69
  it { is_expected.to raise_error WebFetch::RequestNotFoundError }
@@ -80,6 +81,7 @@ describe WebFetch::Client do
80
81
  retrieved = client.retrieve_by_uid(uid)
81
82
  expect(retrieved[:response][:status]).to eql 200
82
83
  expect(retrieved[:response][:body]).to eql 'hello, everybody'
84
+ expect(retrieved[:response][:response_time]).to be_a Float
83
85
  expect(retrieved[:uid]).to eql uid
84
86
  end
85
87
 
@@ -111,7 +113,7 @@ describe WebFetch::Client do
111
113
 
112
114
  describe '#create' do
113
115
  it 'spawns a server and returns a client able to connect' do
114
- client = described_class.create('localhost', 8077, log: File::NULL)
116
+ client = described_class.create('localhost', 60_085, log: File::NULL)
115
117
  expect(client.alive?).to be true
116
118
  client.stop
117
119
  end
@@ -119,7 +121,12 @@ describe WebFetch::Client do
119
121
 
120
122
  describe '#stop' do
121
123
  it 'can spawn a server and stop the process when needed' do
122
- client = described_class.create('localhost', 8077, log: File::NULL)
124
+ pending <<-PENDING.gsub(/\s+/, ' ')
125
+ I can't quite figure out what's going on here but the parent process
126
+ seems to be holding on to the child process' FDs and keeping the server
127
+ alive. `Client#stop` definitely works though ..."
128
+ PENDING
129
+ client = described_class.create('localhost', 60_085, log: File::NULL)
123
130
  expect(client.alive?).to be true
124
131
  client.stop
125
132
  expect(client.alive?).to be false
@@ -5,7 +5,7 @@ describe WebFetch::Gatherer do
5
5
 
6
6
  let(:valid_params) do
7
7
  { requests: [
8
- { url: 'http://localhost:8089' },
8
+ { url: 'http://localhost:60085' },
9
9
  { url: 'http://remotehost:8089' }
10
10
  ] }
11
11
  end
@@ -9,7 +9,8 @@ RSpec.describe WebFetch::Result do
9
9
  pending: false,
10
10
  success: false,
11
11
  error: 'foo error happened',
12
- uid: 'uid123'
12
+ uid: 'uid123',
13
+ response_time: 123.45
13
14
  )
14
15
  end
15
16
 
@@ -24,4 +25,5 @@ RSpec.describe WebFetch::Result do
24
25
  its(:success?) { is_expected.to be false }
25
26
  its(:error) { is_expected.to eql 'foo error happened' }
26
27
  its(:uid) { is_expected.to eql 'uid123' }
28
+ its(:response_time) { is_expected.to eql 123.45 }
27
29
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  describe WebFetch::Server do
4
- let(:port) { 8089 }
4
+ let(:port) { 60_085 }
5
5
  let(:host) { 'localhost' }
6
6
  let(:host_uri) { "http://#{host}:#{port}" }
7
7
 
@@ -14,7 +14,7 @@ WebFetch::Logger.logger(File::NULL)
14
14
 
15
15
  Thread.new do
16
16
  EM.run do
17
- EM.start_server 'localhost', 8089, WebFetch::Server
17
+ EM.start_server 'localhost', 60_085, WebFetch::Server
18
18
  end
19
19
  end
20
20
  waiting = true
@@ -117,6 +117,8 @@ definitions:
117
117
  type: object
118
118
  status:
119
119
  type: integer
120
+ response_time:
121
+ type: float
120
122
  Found:
121
123
  type: object
122
124
  properties:
@@ -134,6 +136,8 @@ definitions:
134
136
  type: integer
135
137
  pending:
136
138
  type: boolean
139
+ response_time:
140
+ type: float
137
141
 
138
142
  GatherResponse:
139
143
  type: array
@@ -21,7 +21,6 @@ Gem::Specification.new do |s|
21
21
  s.executables << 'web_fetch_control'
22
22
 
23
23
  s.add_dependency 'activesupport', '>= 4.0'
24
- s.add_dependency 'childprocess', '~> 0.5'
25
24
  s.add_dependency 'daemons', '~> 1.2'
26
25
  s.add_dependency 'em-http-request', '~> 1.1'
27
26
  s.add_dependency 'em-logger', '~> 0.1'
@@ -32,6 +31,7 @@ Gem::Specification.new do |s|
32
31
  s.add_dependency 'hanami-utils', '~> 1.0'
33
32
  s.add_dependency 'i18n', '>= 0.7'
34
33
  s.add_dependency 'rack', '>= 1.6'
34
+ s.add_dependency 'subprocess', '~> 1.3'
35
35
 
36
36
  s.add_development_dependency 'byebug', '~> 9.0'
37
37
  s.add_development_dependency 'rspec', '~> 3.5'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: web_fetch
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bob Farrell
@@ -24,20 +24,6 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '4.0'
27
- - !ruby/object:Gem::Dependency
28
- name: childprocess
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '0.5'
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '0.5'
41
27
  - !ruby/object:Gem::Dependency
42
28
  name: daemons
43
29
  requirement: !ruby/object:Gem::Requirement
@@ -178,6 +164,20 @@ dependencies:
178
164
  - - ">="
179
165
  - !ruby/object:Gem::Version
180
166
  version: '1.6'
167
+ - !ruby/object:Gem::Dependency
168
+ name: subprocess
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: '1.3'
174
+ type: :runtime
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: '1.3'
181
181
  - !ruby/object:Gem::Dependency
182
182
  name: byebug
183
183
  requirement: !ruby/object:Gem::Requirement
@@ -277,13 +277,12 @@ files:
277
277
  - lib/web_fetch.rb
278
278
  - lib/web_fetch/client.rb
279
279
  - lib/web_fetch/concerns/client_http.rb
280
+ - lib/web_fetch/concerns/event_machine_helpers.rb
280
281
  - lib/web_fetch/concerns/http_helpers.rb
281
282
  - lib/web_fetch/concerns/validatable.rb
282
283
  - lib/web_fetch/errors.rb
283
- - lib/web_fetch/event_machine_helpers.rb
284
284
  - lib/web_fetch/gatherer.rb
285
285
  - lib/web_fetch/helpers.rb
286
- - lib/web_fetch/http_helpers.rb
287
286
  - lib/web_fetch/logger.rb
288
287
  - lib/web_fetch/promise.rb
289
288
  - lib/web_fetch/request.rb
@@ -1,90 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module WebFetch
4
- # Convenience methods for WebFetch HTTP layer
5
- module HTTPHelpers
6
- def respond_immediately(result, response)
7
- response.status = result[:status]
8
- response.content = compress(result[:payload].to_json)
9
- response.send_response
10
- end
11
-
12
- def pending(result, response)
13
- respond_immediately({
14
- payload: {
15
- uid: result[:request][:uid],
16
- pending: true,
17
- message: I18n.t(:pending)
18
- }
19
- }, response)
20
- end
21
-
22
- def compress(string)
23
- return string unless accept_gzip?
24
-
25
- ActiveSupport::Gzip.compress(string)
26
- end
27
-
28
- def default_headers(response)
29
- response.headers['Content-Type'] = 'application/json; charset=utf-8'
30
- response.headers['Cache-Control'] = 'max-age=0, private, must-revalidate'
31
- response.headers['Content-Encoding'] = 'gzip' if accept_gzip?
32
- response.headers['Vary'] = 'Accept-Encoding'
33
- end
34
-
35
- def request_params
36
- { method: @http_request_method,
37
- query_string: @http_query_string,
38
- post_data: post_data,
39
- server: self }
40
- end
41
-
42
- def post_data
43
- return nil unless @http_post_content
44
-
45
- JSON.parse(@http_post_content, symbolize_names: true)
46
- end
47
-
48
- def succeed(request, response)
49
- response.status = 200
50
- response.content = compress(JSON.dump(success(request)))
51
- response.send_response
52
- storage.delete(request[:uid])
53
- end
54
-
55
- def success(request)
56
- result = request[:deferred]
57
- { response: {
58
- success: true,
59
- body: result.response,
60
- headers: result.headers,
61
- status: result.response_header.status
62
- },
63
- uid: request[:uid] }
64
- end
65
-
66
- def fail_(request, response)
67
- response.status = 200
68
- response.content = compress(JSON.dump(failure(request)))
69
- response.send_response
70
- storage.delete(request[:uid])
71
- end
72
-
73
- def failure(request)
74
- result = request[:deferred]
75
- { response: {
76
- success: false,
77
- body: result.response,
78
- headers: result.headers,
79
- status: result.response_header.status,
80
- error: (result.error&.inspect)
81
- },
82
- uid: request[:uid] }
83
- end
84
-
85
- def accept_gzip?
86
- # em-http-request doesn't do us any favours with parsing the HTTP headers
87
- @http_headers.downcase.include?('accept-encoding: gzip')
88
- end
89
- end
90
- end