restify 2.1.0 → 3.0.0.rc2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +48 -0
- data/README.md +30 -4
- data/lib/restify/adapter/base.rb +10 -1
- data/lib/restify/adapter/ethon.rb +462 -0
- data/lib/restify/adapter/loop_lock.rb +124 -0
- data/lib/restify/adapter/telemetry.rb +6 -4
- data/lib/restify/adapter/typhoeus.rb +48 -23
- data/lib/restify/context.rb +5 -5
- data/lib/restify/error.rb +13 -0
- data/lib/restify/global.rb +9 -30
- data/lib/restify/logging.rb +10 -4
- data/lib/restify/processors/base/parsing.rb +13 -1
- data/lib/restify/promise.rb +20 -2
- data/lib/restify/registry.rb +2 -2
- data/lib/restify/relation.rb +8 -8
- data/lib/restify/request.rb +2 -2
- data/lib/restify/resource.rb +20 -2
- data/lib/restify/response.rb +39 -3
- data/lib/restify/timeout.rb +11 -5
- data/lib/restify/version.rb +3 -3
- data/lib/restify/webmock.rb +126 -0
- data/lib/restify.rb +5 -1
- data/spec/restify/adapter/ethon_spec.rb +263 -0
- data/spec/restify/adapter/loop_lock_spec.rb +144 -0
- data/spec/restify/adapter/telemetry_spec.rb +61 -0
- data/spec/restify/adapter/typhoeus_spec.rb +26 -0
- data/spec/restify/cache_spec.rb +1 -3
- data/spec/restify/context_spec.rb +55 -0
- data/spec/restify/features/protocols_spec.rb +28 -0
- data/spec/restify/features/redirects_spec.rb +45 -0
- data/spec/restify/features/request_errors_spec.rb +1 -1
- data/spec/restify/features/response_bodies_spec.rb +45 -0
- data/spec/restify/features/response_encoding_spec.rb +82 -0
- data/spec/restify/features/response_errors_spec.rb +22 -1
- data/spec/restify/features/webmock_spec.rb +92 -1
- data/spec/restify/global_spec.rb +78 -10
- data/spec/restify/processors/json_spec.rb +36 -0
- data/spec/restify/processors/msgpack_spec.rb +36 -0
- data/spec/restify/promise_spec.rb +82 -0
- data/spec/restify/relation_spec.rb +2 -6
- data/spec/restify/request_spec.rb +26 -0
- data/spec/restify/timeout_spec.rb +11 -0
- data/spec/spec_helper.rb +10 -6
- data/spec/support/stub_server.rb +4 -4
- metadata +26 -209
- data/vendor/bundle/ruby/4.0.0/bundler/gems/my-rubocop-2b861962124a/LICENSE +0 -21
- data/vendor/bundle/ruby/4.0.0/bundler/gems/my-rubocop-2b861962124a/README.md +0 -23
- data/vendor/bundle/ruby/4.0.0/gems/activesupport-8.1.3/CHANGELOG.md +0 -561
- data/vendor/bundle/ruby/4.0.0/gems/activesupport-8.1.3/MIT-LICENSE +0 -20
- data/vendor/bundle/ruby/4.0.0/gems/activesupport-8.1.3/README.rdoc +0 -40
- data/vendor/bundle/ruby/4.0.0/gems/addressable-2.9.0/CHANGELOG.md +0 -326
- data/vendor/bundle/ruby/4.0.0/gems/addressable-2.9.0/LICENSE.txt +0 -202
- data/vendor/bundle/ruby/4.0.0/gems/addressable-2.9.0/README.md +0 -121
- data/vendor/bundle/ruby/4.0.0/gems/ast-2.4.3/LICENSE.MIT +0 -20
- data/vendor/bundle/ruby/4.0.0/gems/ast-2.4.3/README.YARD.md +0 -12
- data/vendor/bundle/ruby/4.0.0/gems/base64-0.3.0/README.md +0 -48
- data/vendor/bundle/ruby/4.0.0/gems/bigdecimal-4.1.2/LICENSE +0 -56
- data/vendor/bundle/ruby/4.0.0/gems/concurrent-ruby-1.3.7/CHANGELOG.md +0 -615
- data/vendor/bundle/ruby/4.0.0/gems/concurrent-ruby-1.3.7/LICENSE.txt +0 -21
- data/vendor/bundle/ruby/4.0.0/gems/concurrent-ruby-1.3.7/README.md +0 -409
- data/vendor/bundle/ruby/4.0.0/gems/connection_pool-3.0.2/LICENSE +0 -20
- data/vendor/bundle/ruby/4.0.0/gems/connection_pool-3.0.2/README.md +0 -182
- data/vendor/bundle/ruby/4.0.0/gems/crack-1.0.1/LICENSE +0 -20
- data/vendor/bundle/ruby/4.0.0/gems/crack-1.0.1/README.md +0 -43
- data/vendor/bundle/ruby/4.0.0/gems/diff-lcs-1.6.2/CHANGELOG.md +0 -518
- data/vendor/bundle/ruby/4.0.0/gems/diff-lcs-1.6.2/README.md +0 -92
- data/vendor/bundle/ruby/4.0.0/gems/docile-1.4.1/LICENSE +0 -21
- data/vendor/bundle/ruby/4.0.0/gems/docile-1.4.1/README.md +0 -409
- data/vendor/bundle/ruby/4.0.0/gems/drb-2.2.3/LICENSE.txt +0 -22
- data/vendor/bundle/ruby/4.0.0/gems/ethon-0.18.0/CHANGELOG.md +0 -417
- data/vendor/bundle/ruby/4.0.0/gems/ethon-0.18.0/LICENSE +0 -20
- data/vendor/bundle/ruby/4.0.0/gems/ethon-0.18.0/README.md +0 -118
- data/vendor/bundle/ruby/4.0.0/gems/ffi-1.17.4-x86_64-linux-gnu/CHANGELOG.md +0 -507
- data/vendor/bundle/ruby/4.0.0/gems/ffi-1.17.4-x86_64-linux-gnu/LICENSE +0 -24
- data/vendor/bundle/ruby/4.0.0/gems/ffi-1.17.4-x86_64-linux-gnu/LICENSE.SPECS +0 -22
- data/vendor/bundle/ruby/4.0.0/gems/ffi-1.17.4-x86_64-linux-gnu/README.md +0 -138
- data/vendor/bundle/ruby/4.0.0/gems/hashdiff-1.2.1/LICENSE +0 -19
- data/vendor/bundle/ruby/4.0.0/gems/hashdiff-1.2.1/README.md +0 -323
- data/vendor/bundle/ruby/4.0.0/gems/hashdiff-1.2.1/changelog.md +0 -127
- data/vendor/bundle/ruby/4.0.0/gems/hitimes-3.2.0/LICENSE.txt +0 -16
- data/vendor/bundle/ruby/4.0.0/gems/hitimes-3.2.0/README.md +0 -187
- data/vendor/bundle/ruby/4.0.0/gems/i18n-1.15.2/MIT-LICENSE +0 -20
- data/vendor/bundle/ruby/4.0.0/gems/i18n-1.15.2/README.md +0 -139
- data/vendor/bundle/ruby/4.0.0/gems/json-2.19.9/README.md +0 -310
- data/vendor/bundle/ruby/4.0.0/gems/language_server-protocol-3.17.0.5/LICENSE.txt +0 -21
- data/vendor/bundle/ruby/4.0.0/gems/language_server-protocol-3.17.0.5/README.md +0 -88
- data/vendor/bundle/ruby/4.0.0/gems/lint_roller-1.1.0/CHANGELOG.md +0 -15
- data/vendor/bundle/ruby/4.0.0/gems/lint_roller-1.1.0/LICENSE.txt +0 -21
- data/vendor/bundle/ruby/4.0.0/gems/lint_roller-1.1.0/README.md +0 -173
- data/vendor/bundle/ruby/4.0.0/gems/little-plugger-1.1.4/README.rdoc +0 -53
- data/vendor/bundle/ruby/4.0.0/gems/logger-1.7.0/README.md +0 -104
- data/vendor/bundle/ruby/4.0.0/gems/logging-2.4.0/LICENSE +0 -22
- data/vendor/bundle/ruby/4.0.0/gems/logging-2.4.0/README.md +0 -140
- data/vendor/bundle/ruby/4.0.0/gems/minitest-6.0.6/README.rdoc +0 -763
- data/vendor/bundle/ruby/4.0.0/gems/msgpack-1.8.3/ChangeLog +0 -383
- data/vendor/bundle/ruby/4.0.0/gems/msgpack-1.8.3/LICENSE +0 -177
- data/vendor/bundle/ruby/4.0.0/gems/msgpack-1.8.3/README.md +0 -302
- data/vendor/bundle/ruby/4.0.0/gems/multi_json-1.21.1/LICENSE.md +0 -20
- data/vendor/bundle/ruby/4.0.0/gems/multi_json-1.21.1/README.md +0 -281
- data/vendor/bundle/ruby/4.0.0/gems/nio4r-2.7.5/ext/libev/LICENSE +0 -37
- data/vendor/bundle/ruby/4.0.0/gems/nio4r-2.7.5/ext/libev/README +0 -59
- data/vendor/bundle/ruby/4.0.0/gems/nio4r-2.7.5/license.md +0 -80
- data/vendor/bundle/ruby/4.0.0/gems/nio4r-2.7.5/readme.md +0 -91
- data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-api-1.10.0/CHANGELOG.md +0 -219
- data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-api-1.10.0/LICENSE +0 -201
- data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-api-1.10.0/README.md +0 -68
- data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-common-0.25.0/CHANGELOG.md +0 -110
- data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-common-0.25.0/LICENSE +0 -201
- data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-common-0.25.0/README.md +0 -62
- data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-instrumentation-base-0.26.1/CHANGELOG.md +0 -97
- data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-instrumentation-base-0.26.1/LICENSE +0 -201
- data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-instrumentation-base-0.26.1/README.md +0 -155
- data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-instrumentation-ethon-0.29.0/CHANGELOG.md +0 -192
- data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-instrumentation-ethon-0.29.0/LICENSE +0 -201
- data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-instrumentation-ethon-0.29.0/README.md +0 -66
- data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-registry-0.6.0/CHANGELOG.md +0 -31
- data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-registry-0.6.0/LICENSE +0 -201
- data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-registry-0.6.0/README.md +0 -40
- data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-sdk-1.12.0/CHANGELOG.md +0 -302
- data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-sdk-1.12.0/LICENSE +0 -201
- data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-sdk-1.12.0/README.md +0 -101
- data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-semantic_conventions-1.41.0/CHANGELOG.md +0 -65
- data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-semantic_conventions-1.41.0/LICENSE +0 -201
- data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-semantic_conventions-1.41.0/README.md +0 -115
- data/vendor/bundle/ruby/4.0.0/gems/parallel-2.1.0/MIT-LICENSE.txt +0 -20
- data/vendor/bundle/ruby/4.0.0/gems/parser-3.3.11.1/LICENSE.txt +0 -26
- data/vendor/bundle/ruby/4.0.0/gems/prism-1.9.0/CHANGELOG.md +0 -786
- data/vendor/bundle/ruby/4.0.0/gems/prism-1.9.0/LICENSE.md +0 -7
- data/vendor/bundle/ruby/4.0.0/gems/prism-1.9.0/README.md +0 -143
- data/vendor/bundle/ruby/4.0.0/gems/public_suffix-7.0.5/CHANGELOG.md +0 -649
- data/vendor/bundle/ruby/4.0.0/gems/public_suffix-7.0.5/LICENSE.txt +0 -22
- data/vendor/bundle/ruby/4.0.0/gems/public_suffix-7.0.5/README.md +0 -231
- data/vendor/bundle/ruby/4.0.0/gems/puma-8.0.2/LICENSE +0 -29
- data/vendor/bundle/ruby/4.0.0/gems/puma-8.0.2/README.md +0 -484
- data/vendor/bundle/ruby/4.0.0/gems/puma-8.0.2/docs/jungle/README.md +0 -9
- data/vendor/bundle/ruby/4.0.0/gems/puma-8.0.2/docs/jungle/rc.d/README.md +0 -74
- data/vendor/bundle/ruby/4.0.0/gems/racc-1.8.1/ChangeLog +0 -846
- data/vendor/bundle/ruby/4.0.0/gems/racc-1.8.1/README.ja.rdoc +0 -58
- data/vendor/bundle/ruby/4.0.0/gems/racc-1.8.1/README.rdoc +0 -60
- data/vendor/bundle/ruby/4.0.0/gems/rack-3.2.6/CHANGELOG.md +0 -1346
- data/vendor/bundle/ruby/4.0.0/gems/rack-3.2.6/MIT-LICENSE +0 -20
- data/vendor/bundle/ruby/4.0.0/gems/rack-3.2.6/README.md +0 -384
- data/vendor/bundle/ruby/4.0.0/gems/rainbow-3.1.1/Changelog.md +0 -101
- data/vendor/bundle/ruby/4.0.0/gems/rainbow-3.1.1/LICENSE +0 -20
- data/vendor/bundle/ruby/4.0.0/gems/rainbow-3.1.1/README.markdown +0 -227
- data/vendor/bundle/ruby/4.0.0/gems/rake-13.4.2/MIT-LICENSE +0 -21
- data/vendor/bundle/ruby/4.0.0/gems/rake-13.4.2/README.rdoc +0 -155
- data/vendor/bundle/ruby/4.0.0/gems/rake-release-1.4.0/LICENSE +0 -21
- data/vendor/bundle/ruby/4.0.0/gems/rake-release-1.4.0/README.md +0 -107
- data/vendor/bundle/ruby/4.0.0/gems/regexp_parser-2.12.0/LICENSE +0 -22
- data/vendor/bundle/ruby/4.0.0/gems/rexml-3.4.4/LICENSE.txt +0 -22
- data/vendor/bundle/ruby/4.0.0/gems/rexml-3.4.4/README.md +0 -57
- data/vendor/bundle/ruby/4.0.0/gems/rspec-3.13.2/LICENSE.md +0 -27
- data/vendor/bundle/ruby/4.0.0/gems/rspec-3.13.2/README.md +0 -47
- data/vendor/bundle/ruby/4.0.0/gems/rspec-core-3.13.6/Changelog.md +0 -2447
- data/vendor/bundle/ruby/4.0.0/gems/rspec-core-3.13.6/LICENSE.md +0 -26
- data/vendor/bundle/ruby/4.0.0/gems/rspec-core-3.13.6/README.md +0 -389
- data/vendor/bundle/ruby/4.0.0/gems/rspec-expectations-3.13.5/Changelog.md +0 -1366
- data/vendor/bundle/ruby/4.0.0/gems/rspec-expectations-3.13.5/LICENSE.md +0 -25
- data/vendor/bundle/ruby/4.0.0/gems/rspec-expectations-3.13.5/README.md +0 -326
- data/vendor/bundle/ruby/4.0.0/gems/rspec-mocks-3.13.8/Changelog.md +0 -1351
- data/vendor/bundle/ruby/4.0.0/gems/rspec-mocks-3.13.8/LICENSE.md +0 -25
- data/vendor/bundle/ruby/4.0.0/gems/rspec-mocks-3.13.8/README.md +0 -465
- data/vendor/bundle/ruby/4.0.0/gems/rspec-support-3.13.7/Changelog.md +0 -444
- data/vendor/bundle/ruby/4.0.0/gems/rspec-support-3.13.7/LICENSE.md +0 -23
- data/vendor/bundle/ruby/4.0.0/gems/rspec-support-3.13.7/README.md +0 -40
- data/vendor/bundle/ruby/4.0.0/gems/rubocop-1.88.0/LICENSE.txt +0 -20
- data/vendor/bundle/ruby/4.0.0/gems/rubocop-1.88.0/README.md +0 -255
- data/vendor/bundle/ruby/4.0.0/gems/rubocop-ast-1.49.1/LICENSE.txt +0 -20
- data/vendor/bundle/ruby/4.0.0/gems/rubocop-ast-1.49.1/README.md +0 -54
- data/vendor/bundle/ruby/4.0.0/gems/rubocop-capybara-2.23.0/CHANGELOG.md +0 -116
- data/vendor/bundle/ruby/4.0.0/gems/rubocop-capybara-2.23.0/MIT-LICENSE.md +0 -21
- data/vendor/bundle/ruby/4.0.0/gems/rubocop-capybara-2.23.0/README.md +0 -91
- data/vendor/bundle/ruby/4.0.0/gems/rubocop-factory_bot-2.28.0/CHANGELOG.md +0 -135
- data/vendor/bundle/ruby/4.0.0/gems/rubocop-factory_bot-2.28.0/MIT-LICENSE.md +0 -21
- data/vendor/bundle/ruby/4.0.0/gems/rubocop-factory_bot-2.28.0/README.md +0 -91
- data/vendor/bundle/ruby/4.0.0/gems/rubocop-performance-1.26.1/LICENSE.txt +0 -20
- data/vendor/bundle/ruby/4.0.0/gems/rubocop-performance-1.26.1/README.md +0 -100
- data/vendor/bundle/ruby/4.0.0/gems/rubocop-rspec-3.10.2/CHANGELOG.md +0 -1136
- data/vendor/bundle/ruby/4.0.0/gems/rubocop-rspec-3.10.2/MIT-LICENSE.md +0 -21
- data/vendor/bundle/ruby/4.0.0/gems/rubocop-rspec-3.10.2/README.md +0 -112
- data/vendor/bundle/ruby/4.0.0/gems/rubocop-rspec_rails-2.32.0/CHANGELOG.md +0 -98
- data/vendor/bundle/ruby/4.0.0/gems/rubocop-rspec_rails-2.32.0/MIT-LICENSE.md +0 -21
- data/vendor/bundle/ruby/4.0.0/gems/rubocop-rspec_rails-2.32.0/README.md +0 -93
- data/vendor/bundle/ruby/4.0.0/gems/ruby-progressbar-1.13.0/LICENSE.txt +0 -19
- data/vendor/bundle/ruby/4.0.0/gems/ruby-progressbar-1.13.0/README.md +0 -131
- data/vendor/bundle/ruby/4.0.0/gems/securerandom-0.4.1/README.md +0 -72
- data/vendor/bundle/ruby/4.0.0/gems/simplecov-0.22.0/CHANGELOG.md +0 -191
- data/vendor/bundle/ruby/4.0.0/gems/simplecov-0.22.0/LICENSE +0 -20
- data/vendor/bundle/ruby/4.0.0/gems/simplecov-0.22.0/README.md +0 -974
- data/vendor/bundle/ruby/4.0.0/gems/simplecov-cobertura-3.2.0/LICENSE +0 -202
- data/vendor/bundle/ruby/4.0.0/gems/simplecov-cobertura-3.2.0/README.md +0 -65
- data/vendor/bundle/ruby/4.0.0/gems/simplecov-html-0.13.2/CHANGELOG.md +0 -114
- data/vendor/bundle/ruby/4.0.0/gems/simplecov-html-0.13.2/LICENSE +0 -20
- data/vendor/bundle/ruby/4.0.0/gems/simplecov-html-0.13.2/README.md +0 -30
- data/vendor/bundle/ruby/4.0.0/gems/simplecov_json_formatter-0.1.4/CHANGELOG.md +0 -13
- data/vendor/bundle/ruby/4.0.0/gems/simplecov_json_formatter-0.1.4/README.md +0 -29
- data/vendor/bundle/ruby/4.0.0/gems/typhoeus-1.6.0/CHANGELOG.md +0 -465
- data/vendor/bundle/ruby/4.0.0/gems/typhoeus-1.6.0/LICENSE +0 -22
- data/vendor/bundle/ruby/4.0.0/gems/typhoeus-1.6.0/README.md +0 -588
- data/vendor/bundle/ruby/4.0.0/gems/tzinfo-2.0.6/LICENSE +0 -19
- data/vendor/bundle/ruby/4.0.0/gems/tzinfo-2.0.6/README.md +0 -406
- data/vendor/bundle/ruby/4.0.0/gems/unicode-display_width-3.2.0/CHANGELOG.md +0 -299
- data/vendor/bundle/ruby/4.0.0/gems/unicode-display_width-3.2.0/MIT-LICENSE.txt +0 -22
- data/vendor/bundle/ruby/4.0.0/gems/unicode-display_width-3.2.0/README.md +0 -194
- data/vendor/bundle/ruby/4.0.0/gems/unicode-emoji-4.2.0/CHANGELOG.md +0 -202
- data/vendor/bundle/ruby/4.0.0/gems/unicode-emoji-4.2.0/MIT-LICENSE.txt +0 -20
- data/vendor/bundle/ruby/4.0.0/gems/unicode-emoji-4.2.0/README.md +0 -205
- data/vendor/bundle/ruby/4.0.0/gems/uri-1.1.1/README.md +0 -55
- data/vendor/bundle/ruby/4.0.0/gems/webmock-3.26.2/CHANGELOG.md +0 -2148
- data/vendor/bundle/ruby/4.0.0/gems/webmock-3.26.2/LICENSE +0 -20
- data/vendor/bundle/ruby/4.0.0/gems/webmock-3.26.2/README.md +0 -1229
data/lib/restify/response.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'rack/media_type'
|
|
3
4
|
require 'rack/utils'
|
|
4
5
|
require 'json'
|
|
5
6
|
|
|
@@ -30,6 +31,10 @@ module Restify
|
|
|
30
31
|
|
|
31
32
|
# Response body as string.
|
|
32
33
|
#
|
|
34
|
+
# The body is encoded according to the charset from the response's
|
|
35
|
+
# `Content-Type` header. Without a charset, or with one unknown to
|
|
36
|
+
# Ruby, the body is returned as binary.
|
|
37
|
+
#
|
|
33
38
|
# @return [String] Response body.
|
|
34
39
|
#
|
|
35
40
|
attr_reader :body
|
|
@@ -81,7 +86,7 @@ module Restify
|
|
|
81
86
|
@code = code
|
|
82
87
|
@status = STATUS_CODE_TO_SYMBOL[code]
|
|
83
88
|
@headers = convert_headers(headers)
|
|
84
|
-
@body = body
|
|
89
|
+
@body = encode_body(body)
|
|
85
90
|
@message = Rack::Utils::HTTP_STATUS_CODES[code]
|
|
86
91
|
end
|
|
87
92
|
|
|
@@ -112,6 +117,24 @@ module Restify
|
|
|
112
117
|
headers['CONTENT_TYPE']
|
|
113
118
|
end
|
|
114
119
|
|
|
120
|
+
# Return the encoding from the response's content type, if any.
|
|
121
|
+
#
|
|
122
|
+
# @return [Encoding, nil] Encoding, or nil if the content type
|
|
123
|
+
# carries no charset or an encoding unknown to Ruby.
|
|
124
|
+
#
|
|
125
|
+
def charset
|
|
126
|
+
return @charset if defined?(@charset)
|
|
127
|
+
|
|
128
|
+
charset = Rack::MediaType.params(content_type.to_s)['charset']
|
|
129
|
+
|
|
130
|
+
@charset = begin
|
|
131
|
+
Encoding.find(charset) if charset
|
|
132
|
+
rescue ArgumentError
|
|
133
|
+
# Unknown: keep the body as binary rather than guessing.
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
115
138
|
# Check if response is successful e.g. the status code
|
|
116
139
|
# is on of 2XX.
|
|
117
140
|
#
|
|
@@ -130,13 +153,20 @@ module Restify
|
|
|
130
153
|
(400...600).cover? code
|
|
131
154
|
end
|
|
132
155
|
|
|
156
|
+
# Decoded body for error messages, or nil if the body is empty or
|
|
157
|
+
# could not be parsed.
|
|
158
|
+
#
|
|
133
159
|
# @api private
|
|
134
160
|
def decoded_body
|
|
135
|
-
@decoded_body
|
|
161
|
+
return @decoded_body if defined?(@decoded_body)
|
|
162
|
+
|
|
163
|
+
@decoded_body = begin
|
|
136
164
|
case content_type
|
|
137
165
|
when %r{\Aapplication/json($|;)}
|
|
138
|
-
::JSON.parse(body)
|
|
166
|
+
::JSON.parse(body) unless body.nil? || body.empty?
|
|
139
167
|
end
|
|
168
|
+
rescue ::JSON::ParserError
|
|
169
|
+
nil
|
|
140
170
|
end
|
|
141
171
|
end
|
|
142
172
|
|
|
@@ -147,6 +177,12 @@ module Restify
|
|
|
147
177
|
|
|
148
178
|
private
|
|
149
179
|
|
|
180
|
+
def encode_body(body)
|
|
181
|
+
return body unless body && charset
|
|
182
|
+
|
|
183
|
+
(+body).force_encoding(charset)
|
|
184
|
+
end
|
|
185
|
+
|
|
150
186
|
def convert_headers(headers)
|
|
151
187
|
headers.each.to_h do |pair|
|
|
152
188
|
[pair[0].upcase, pair[1]]
|
data/lib/restify/timeout.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require '
|
|
3
|
+
require 'timeout'
|
|
4
4
|
|
|
5
5
|
module Restify
|
|
6
6
|
class Timeout
|
|
@@ -14,9 +14,7 @@ module Restify
|
|
|
14
14
|
def initialize(timeout, target = nil)
|
|
15
15
|
@timeout = parse_timeout(timeout)
|
|
16
16
|
@target = target
|
|
17
|
-
|
|
18
|
-
@interval = ::Hitimes::Interval.new
|
|
19
|
-
@interval.start
|
|
17
|
+
@deadline = now + @timeout
|
|
20
18
|
end
|
|
21
19
|
|
|
22
20
|
def wait_on!(ivar)
|
|
@@ -31,6 +29,10 @@ module Restify
|
|
|
31
29
|
raise self if wait_interval <= 0
|
|
32
30
|
end
|
|
33
31
|
|
|
32
|
+
def remaining
|
|
33
|
+
wait_interval
|
|
34
|
+
end
|
|
35
|
+
|
|
34
36
|
def exception
|
|
35
37
|
Error.new(@target)
|
|
36
38
|
end
|
|
@@ -38,7 +40,11 @@ module Restify
|
|
|
38
40
|
private
|
|
39
41
|
|
|
40
42
|
def wait_interval
|
|
41
|
-
@
|
|
43
|
+
@deadline - now
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def now
|
|
47
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
42
48
|
end
|
|
43
49
|
|
|
44
50
|
def parse_timeout(value)
|
data/lib/restify/version.rb
CHANGED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'restify'
|
|
4
|
+
require 'webmock'
|
|
5
|
+
|
|
6
|
+
module WebMock
|
|
7
|
+
module HttpLibAdapters
|
|
8
|
+
#
|
|
9
|
+
# WebMock support for Restify (independent of the adapter)
|
|
10
|
+
#
|
|
11
|
+
# Requests are intercepted before they are handed over to an
|
|
12
|
+
# adapter. Stubbed responses are returned as they are, including
|
|
13
|
+
# redirects, like WebMock does for other libcurl-based libraries.
|
|
14
|
+
# Requests not stubbed are passed on to the adapter, if allowed.
|
|
15
|
+
#
|
|
16
|
+
# Must be loaded before WebMock is enabled, e.g. in
|
|
17
|
+
# `spec_helper.rb`:
|
|
18
|
+
#
|
|
19
|
+
# require 'webmock/rspec'
|
|
20
|
+
# require 'restify/webmock'
|
|
21
|
+
#
|
|
22
|
+
class RestifyAdapter < HttpLibAdapter
|
|
23
|
+
adapter_for :restify
|
|
24
|
+
|
|
25
|
+
class << self
|
|
26
|
+
def enable!
|
|
27
|
+
@enabled = true
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def disable!
|
|
31
|
+
@enabled = false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def enabled?
|
|
35
|
+
@enabled == true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Respond to a request with a stubbed response, or pass it on to
|
|
39
|
+
# the given block if allowed.
|
|
40
|
+
#
|
|
41
|
+
# @api private
|
|
42
|
+
def call(request, &)
|
|
43
|
+
signature = signature(request)
|
|
44
|
+
RequestRegistry.instance.requested_signatures.put(signature)
|
|
45
|
+
|
|
46
|
+
stub = StubRegistry.instance.response_for_request(signature)
|
|
47
|
+
return passthrough(signature, &) unless stub
|
|
48
|
+
|
|
49
|
+
CallbackRegistry.invoke_callbacks({lib: :restify}, signature, stub)
|
|
50
|
+
|
|
51
|
+
Restify::Promise.fulfilled(respond(request, stub))
|
|
52
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
53
|
+
# e.g. `NetConnectNotAllowedError`
|
|
54
|
+
Restify::Promise.rejected(e)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def signature(request)
|
|
60
|
+
RequestSignature.new(
|
|
61
|
+
request.method.to_sym,
|
|
62
|
+
request.uri.to_s,
|
|
63
|
+
body: request.body,
|
|
64
|
+
headers: request.headers,
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def passthrough(signature)
|
|
69
|
+
unless ::WebMock.net_connect_allowed?(signature.uri)
|
|
70
|
+
raise NetConnectNotAllowedError.new(signature)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
yield.tap do |promise|
|
|
74
|
+
# Observers run when the request is complete, even when
|
|
75
|
+
# nobody waits on the promise.
|
|
76
|
+
promise.add_observer do |_, response, _|
|
|
77
|
+
next unless response
|
|
78
|
+
|
|
79
|
+
CallbackRegistry.invoke_callbacks(
|
|
80
|
+
{lib: :restify, real_request: true},
|
|
81
|
+
signature,
|
|
82
|
+
webmock_response(response),
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def respond(request, stub)
|
|
89
|
+
raise Restify::NetworkError.new(request, 'Timeout was reached') if stub.should_timeout
|
|
90
|
+
|
|
91
|
+
stub.raise_error_if_any
|
|
92
|
+
|
|
93
|
+
headers = (stub.headers || {}).to_h do |name, value|
|
|
94
|
+
[name.upcase.tr('-', '_'), value]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
Restify::Response.new(request, request.uri, stub.status[0], headers, stub.body)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def webmock_response(response)
|
|
101
|
+
Response.new.tap do |webmock|
|
|
102
|
+
webmock.status = [response.code, response.message.to_s]
|
|
103
|
+
webmock.headers = response.headers.to_h do |name, value|
|
|
104
|
+
[name.split('_').map(&:capitalize).join('-'), value]
|
|
105
|
+
end
|
|
106
|
+
webmock.body = response.body.to_s
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Intercept requests after `Adapter::Telemetry` but before
|
|
112
|
+
# `Adapter::Base#call` so that telemetry is included.
|
|
113
|
+
#
|
|
114
|
+
# @api private
|
|
115
|
+
module Intercept
|
|
116
|
+
def call(request)
|
|
117
|
+
return super unless RestifyAdapter.enabled?
|
|
118
|
+
|
|
119
|
+
RestifyAdapter.call(request) { super }
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
::Restify::Adapter::Telemetry.include(Intercept)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
data/lib/restify.rb
CHANGED
|
@@ -28,7 +28,11 @@ module Restify
|
|
|
28
28
|
|
|
29
29
|
module Adapter
|
|
30
30
|
require 'restify/adapter/base'
|
|
31
|
-
require 'restify/adapter/
|
|
31
|
+
require 'restify/adapter/loop_lock'
|
|
32
|
+
require 'restify/adapter/ethon'
|
|
33
|
+
|
|
34
|
+
# Optional, requires the `typhoeus` gem.
|
|
35
|
+
autoload :Typhoeus, 'restify/adapter/typhoeus'
|
|
32
36
|
end
|
|
33
37
|
|
|
34
38
|
module Processors
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe Restify::Adapter::Ethon do
|
|
6
|
+
describe 'completed transfers' do
|
|
7
|
+
subject(:value) do
|
|
8
|
+
Restify::Promise.create do |writer|
|
|
9
|
+
described_class.new.send(
|
|
10
|
+
:complete,
|
|
11
|
+
easy,
|
|
12
|
+
request,
|
|
13
|
+
writer,
|
|
14
|
+
)
|
|
15
|
+
end.value!
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
let(:request) { Restify::Request.new(uri: 'http://example.org/base') }
|
|
19
|
+
let(:return_code) { :ok }
|
|
20
|
+
let(:response_code) { 200 }
|
|
21
|
+
let(:effective_url) { 'http://example.org/base' }
|
|
22
|
+
|
|
23
|
+
# A transfer is handed back from libcurl with its return code and,
|
|
24
|
+
# for HTTP, the status code of the response.
|
|
25
|
+
def easy
|
|
26
|
+
instance_double(
|
|
27
|
+
described_class::Easy,
|
|
28
|
+
return_code:,
|
|
29
|
+
response_code:,
|
|
30
|
+
effective_url:,
|
|
31
|
+
response_headers: "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n",
|
|
32
|
+
response_body: '{}',
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'fulfills the promise with the response' do
|
|
37
|
+
expect(value.code).to eq 200
|
|
38
|
+
expect(value.uri.to_s).to eq 'http://example.org/base'
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
context 'when redirected' do
|
|
42
|
+
let(:effective_url) { 'http://example.org/other/base' }
|
|
43
|
+
|
|
44
|
+
it 'uses the effective URL as the response URI' do
|
|
45
|
+
expect(value.uri.to_s).to eq 'http://example.org/other/base'
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
context 'when the transfer failed' do
|
|
50
|
+
let(:return_code) { :couldnt_connect }
|
|
51
|
+
let(:response_code) { 0 }
|
|
52
|
+
|
|
53
|
+
it 'rejects the promise' do
|
|
54
|
+
expect { value }.to raise_error Restify::NetworkError, /connect/i
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Non-HTTP protocols are refused before a transfer is started, but
|
|
59
|
+
# libcurl still reports success with a zero status code whenever no
|
|
60
|
+
# HTTP status was received, and no status code at all when it cannot
|
|
61
|
+
# be read back. Neither has a response to process.
|
|
62
|
+
context 'without an HTTP status' do
|
|
63
|
+
let(:response_code) { 0 }
|
|
64
|
+
|
|
65
|
+
it 'rejects the promise' do
|
|
66
|
+
expect { value }.to raise_error Restify::NetworkError, /without HTTP status/
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
context 'with an unavailable HTTP status' do
|
|
71
|
+
let(:response_code) { nil }
|
|
72
|
+
|
|
73
|
+
it 'rejects the promise' do
|
|
74
|
+
expect { value }.to raise_error Restify::NetworkError, /without HTTP status/
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Unexpected errors must reject the promise too, as the caller would
|
|
79
|
+
# otherwise keep waiting for it until it times out.
|
|
80
|
+
context 'when building the response fails' do
|
|
81
|
+
def easy
|
|
82
|
+
super.tap do |double|
|
|
83
|
+
allow(double).to receive(:response_headers).and_raise('kaboom')
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it 'rejects the promise' do
|
|
88
|
+
expect { value }.to raise_error 'kaboom'
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
describe 'waiting on requests' do
|
|
94
|
+
let(:adapter) { described_class.new }
|
|
95
|
+
|
|
96
|
+
# Threads that completed a transfer, i.e. ran the event loop.
|
|
97
|
+
let(:completed_by) { Queue.new }
|
|
98
|
+
|
|
99
|
+
before do
|
|
100
|
+
stub_request(:get, 'http://stubserver/fast')
|
|
101
|
+
.to_return(status: 200, body: '{}')
|
|
102
|
+
stub_request(:get, 'http://stubserver/slow')
|
|
103
|
+
.to_return { sleep 0.5 and {status: 200, body: '{}'} }
|
|
104
|
+
|
|
105
|
+
allow(adapter).to receive(:complete).and_wrap_original do |m, *args|
|
|
106
|
+
completed_by << Thread.current
|
|
107
|
+
m.call(*args)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def request(path)
|
|
112
|
+
Restify::Request.new(uri: "http://localhost:9292/#{path}")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def completed_by_threads
|
|
116
|
+
Array.new(completed_by.size) { completed_by.pop }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it 'runs the event loop in the waiting thread' do
|
|
120
|
+
expect(adapter.call(request('fast')).value!.code).to eq 200
|
|
121
|
+
expect(completed_by_threads).to eq [Thread.current]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it 'completes requests nobody waits on in the background' do
|
|
125
|
+
adapter.call(request('fast'))
|
|
126
|
+
|
|
127
|
+
# Poll without waiting on the promise:
|
|
128
|
+
Timeout.timeout(1) { sleep 0.01 while completed_by.empty? }
|
|
129
|
+
|
|
130
|
+
expect(completed_by_threads).not_to include Thread.current
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it 'times out without blocking other requests' do
|
|
134
|
+
expect { adapter.call(request('slow')).value!(0.1) }.to raise_error Timeout::Error
|
|
135
|
+
expect(adapter.call(request('fast')).value!.code).to eq 200
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it 'processes requests from many threads' do
|
|
139
|
+
threads = Array.new(8) do
|
|
140
|
+
Thread.new do
|
|
141
|
+
Array.new(5) { adapter.call(request('fast')).value!.code }
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
expect(threads.flat_map(&:value)).to all eq 200
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Only the adapter's own promises are completed by whichever thread
|
|
149
|
+
# runs the event loop. Callbacks chained to them must still run in
|
|
150
|
+
# the thread waiting on them, and only when waiting on them.
|
|
151
|
+
describe 'chained callbacks' do
|
|
152
|
+
it 'does not run them before waiting on them' do
|
|
153
|
+
ran_in = Queue.new
|
|
154
|
+
promise = adapter.call(request('fast'))
|
|
155
|
+
chained = promise.then { ran_in << Thread.current }
|
|
156
|
+
|
|
157
|
+
promise.value!
|
|
158
|
+
expect(ran_in).to be_empty
|
|
159
|
+
|
|
160
|
+
chained.value!
|
|
161
|
+
expect(ran_in.pop(timeout: 1)).to eq Thread.current
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it 'runs them in the waiting thread when another thread runs the loop' do
|
|
165
|
+
# Keep the loop busy in another thread, until after the fast
|
|
166
|
+
# request completed.
|
|
167
|
+
driver = Thread.new { adapter.call(request('slow')).value! }
|
|
168
|
+
sleep 0.1
|
|
169
|
+
|
|
170
|
+
chained = adapter.call(request('fast')).then do |response|
|
|
171
|
+
[Thread.current, response.code]
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
expect(chained.value!).to eq [Thread.current, 200]
|
|
175
|
+
expect(completed_by_threads.first).to eq driver
|
|
176
|
+
ensure
|
|
177
|
+
driver&.join
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
context 'when waiting from within the event loop' do
|
|
182
|
+
it 'does not run the loop again' do
|
|
183
|
+
adapter.instance_variable_get(:@loop).acquire(0.1)
|
|
184
|
+
|
|
185
|
+
expect(adapter.drive(Restify::Promise.new, Restify::Timeout.new(0.1))).to be false
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
describe 'after fork' do
|
|
191
|
+
let(:adapter) { described_class.new }
|
|
192
|
+
let(:connects) { Queue.new }
|
|
193
|
+
|
|
194
|
+
before do
|
|
195
|
+
stub_request(:get, 'http://stubserver/fast')
|
|
196
|
+
.to_return(status: 200, body: '{}')
|
|
197
|
+
stub_request(:get, 'http://stubserver/slow')
|
|
198
|
+
.to_return { sleep 0.5 and {status: 200, body: '{}'} }
|
|
199
|
+
|
|
200
|
+
allow(adapter).to receive(:complete).and_wrap_original do |m, easy, *args|
|
|
201
|
+
connects << Ethon::Curl.get_info_long(:num_connects, easy.handle)
|
|
202
|
+
m.call(easy, *args)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def request(path)
|
|
207
|
+
Restify::Request.new(uri: "http://localhost:9292/#{path}")
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def in_child
|
|
211
|
+
reader, writer = IO.pipe
|
|
212
|
+
|
|
213
|
+
pid = fork do
|
|
214
|
+
reader.close
|
|
215
|
+
result = begin
|
|
216
|
+
yield
|
|
217
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
218
|
+
e
|
|
219
|
+
end
|
|
220
|
+
writer.write(Marshal.dump(result))
|
|
221
|
+
ensure
|
|
222
|
+
# Skip at exit handlers of the parent
|
|
223
|
+
exit!(0)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
writer.close
|
|
227
|
+
Marshal.load(reader.read) # rubocop:disable Security/MarshalLoad
|
|
228
|
+
ensure
|
|
229
|
+
reader&.close
|
|
230
|
+
Process.wait(pid) if pid
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
it 'does not use connections of the parent process' do
|
|
234
|
+
# The parent process pools and reuses its connection.
|
|
235
|
+
2.times { adapter.call(request('fast')).value! }
|
|
236
|
+
expect(Array.new(2) { connects.pop(timeout: 1) }).to eq [1, 0]
|
|
237
|
+
|
|
238
|
+
result = in_child do
|
|
239
|
+
code = adapter.call(request('fast')).value!.code
|
|
240
|
+
[code, connects.pop(timeout: 1)]
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# A new connection, not the one pooled by the parent process
|
|
244
|
+
expect(result).to eq [200, 1]
|
|
245
|
+
|
|
246
|
+
# The parent process can still use its connections
|
|
247
|
+
expect(adapter.call(request('fast')).value!.code).to eq 200
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
it 'rejects requests started before the fork in the child process' do
|
|
251
|
+
promise = adapter.call(request('slow'))
|
|
252
|
+
|
|
253
|
+
result = in_child do
|
|
254
|
+
promise.value!
|
|
255
|
+
rescue Restify::NetworkError => e
|
|
256
|
+
e.message
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
expect(result).to include 'before fork'
|
|
260
|
+
expect(promise.value!.code).to eq 200
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe Restify::Adapter::LoopLock do
|
|
6
|
+
subject(:lock) { described_class.new { interrupts << Thread.current } }
|
|
7
|
+
|
|
8
|
+
let(:interrupts) { Queue.new }
|
|
9
|
+
let(:threads) { [] }
|
|
10
|
+
|
|
11
|
+
after { threads.each(&:kill).each(&:join) }
|
|
12
|
+
|
|
13
|
+
# Run the block in another thread, and wait until it has returned.
|
|
14
|
+
# The thread is kept alive until the example finished.
|
|
15
|
+
def other(&)
|
|
16
|
+
done = Concurrent::Event.new
|
|
17
|
+
threads << Thread.new do
|
|
18
|
+
yield
|
|
19
|
+
done.set
|
|
20
|
+
sleep
|
|
21
|
+
end
|
|
22
|
+
expect(done.wait(1)).to be true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def elapsed
|
|
26
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
27
|
+
yield
|
|
28
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe '#acquire' do
|
|
32
|
+
it 'acquires a free loop' do
|
|
33
|
+
expect(lock.acquire(1)).to be true
|
|
34
|
+
expect(lock).to be_owned
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'does not acquire the loop when done' do
|
|
38
|
+
expect(lock.acquire(1) { true }).to be false
|
|
39
|
+
expect(lock).not_to be_owned
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'times out while another thread holds the loop' do
|
|
43
|
+
other { lock.acquire(1) }
|
|
44
|
+
|
|
45
|
+
expect(elapsed { expect(lock.acquire(0.1)).to be false }).to be >= 0.1
|
|
46
|
+
expect(lock).not_to be_owned
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'waits for another thread to release the loop' do
|
|
50
|
+
acquired = Concurrent::Event.new
|
|
51
|
+
released = false
|
|
52
|
+
threads << Thread.new do
|
|
53
|
+
lock.acquire(1)
|
|
54
|
+
acquired.set
|
|
55
|
+
sleep 0.1
|
|
56
|
+
released = true
|
|
57
|
+
lock.release
|
|
58
|
+
end
|
|
59
|
+
expect(acquired.wait(1)).to be true
|
|
60
|
+
|
|
61
|
+
expect(lock.acquire(1)).to be true
|
|
62
|
+
expect(released).to be true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'stops waiting when done after being notified' do
|
|
66
|
+
done = false
|
|
67
|
+
other { lock.acquire(1) }
|
|
68
|
+
threads << Thread.new do
|
|
69
|
+
sleep 0.1
|
|
70
|
+
done = true
|
|
71
|
+
lock.notify
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
expect(elapsed { expect(lock.acquire(1) { done }).to be false }).to be < 0.5
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'treats a dead thread as not holding the loop' do
|
|
78
|
+
Thread.new { lock.acquire(1) }.join
|
|
79
|
+
|
|
80
|
+
expect(lock.acquire(0.1)).to be true
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
describe '#release' do
|
|
85
|
+
it 'lets a waiting thread acquire the loop' do
|
|
86
|
+
lock.acquire(1)
|
|
87
|
+
waiter = Thread.new { lock.acquire(1) }
|
|
88
|
+
sleep 0.05
|
|
89
|
+
|
|
90
|
+
lock.release
|
|
91
|
+
|
|
92
|
+
expect(waiter.join(1)&.value).to be true
|
|
93
|
+
expect(lock).not_to be_owned
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
describe 'with a background thread' do
|
|
98
|
+
let(:background) { Queue.new }
|
|
99
|
+
|
|
100
|
+
# A background thread running the loop until interrupted, like an
|
|
101
|
+
# event loop waiting for sockets. Blocks on `interrupts` on purpose,
|
|
102
|
+
# and is killed after each example.
|
|
103
|
+
def start_background
|
|
104
|
+
threads << Thread.new do
|
|
105
|
+
loop do
|
|
106
|
+
lock.acquire_background
|
|
107
|
+
background << :acquired
|
|
108
|
+
interrupts.pop
|
|
109
|
+
lock.release
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
expect(background.pop(timeout: 1)).to eq :acquired
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it 'interrupts the background thread to release the loop' do
|
|
117
|
+
start_background
|
|
118
|
+
|
|
119
|
+
expect(lock.acquire(1)).to be true
|
|
120
|
+
expect(background).to be_empty
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it 'blocks the background thread while a thread holds the loop' do
|
|
124
|
+
start_background
|
|
125
|
+
lock.acquire(1)
|
|
126
|
+
|
|
127
|
+
sleep 0.1
|
|
128
|
+
expect(background).to be_empty
|
|
129
|
+
|
|
130
|
+
lock.release
|
|
131
|
+
expect(background.pop(timeout: 1)).to eq :acquired
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
describe '#reset!' do
|
|
136
|
+
it 'forgets threads holding the loop' do
|
|
137
|
+
other { lock.acquire(1) }
|
|
138
|
+
|
|
139
|
+
lock.reset!
|
|
140
|
+
|
|
141
|
+
expect(lock.acquire(0.1)).to be true
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|