restify 2.1.0 → 3.0.0.rc3

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.
Files changed (222) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +75 -0
  3. data/README.md +30 -5
  4. data/lib/restify/adapter/base.rb +10 -1
  5. data/lib/restify/adapter/ethon/easy.rb +147 -0
  6. data/lib/restify/adapter/ethon/event_loop.rb +255 -0
  7. data/lib/restify/adapter/ethon/options.rb +81 -0
  8. data/lib/restify/adapter/ethon/pool.rb +43 -0
  9. data/lib/restify/adapter/ethon.rb +211 -0
  10. data/lib/restify/adapter/loop_lock.rb +124 -0
  11. data/lib/restify/adapter/telemetry.rb +7 -5
  12. data/lib/restify/adapter/typhoeus.rb +52 -23
  13. data/lib/restify/context.rb +16 -6
  14. data/lib/restify/error.rb +13 -0
  15. data/lib/restify/global.rb +9 -30
  16. data/lib/restify/logging.rb +10 -4
  17. data/lib/restify/processors/base/parsing.rb +43 -26
  18. data/lib/restify/promise.rb +194 -42
  19. data/lib/restify/registry.rb +2 -2
  20. data/lib/restify/relation.rb +27 -13
  21. data/lib/restify/request.rb +2 -2
  22. data/lib/restify/resource.rb +20 -2
  23. data/lib/restify/response.rb +39 -3
  24. data/lib/restify/timeout.rb +23 -34
  25. data/lib/restify/version.rb +3 -3
  26. data/lib/restify/webmock.rb +126 -0
  27. data/lib/restify.rb +5 -2
  28. data/spec/restify/adapter/ethon/easy_spec.rb +111 -0
  29. data/spec/restify/adapter/ethon/event_loop_spec.rb +96 -0
  30. data/spec/restify/adapter/ethon/options_spec.rb +104 -0
  31. data/spec/restify/adapter/ethon/pool_spec.rb +81 -0
  32. data/spec/restify/adapter/ethon_spec.rb +307 -0
  33. data/spec/restify/adapter/loop_lock_spec.rb +144 -0
  34. data/spec/restify/adapter/telemetry_spec.rb +61 -0
  35. data/spec/restify/adapter/typhoeus_spec.rb +26 -0
  36. data/spec/restify/cache_spec.rb +1 -3
  37. data/spec/restify/context_spec.rb +69 -0
  38. data/spec/restify/features/opentelemetry_spec.rb +2 -0
  39. data/spec/restify/features/protocols_spec.rb +28 -0
  40. data/spec/restify/features/redirects_spec.rb +45 -0
  41. data/spec/restify/features/request_errors_spec.rb +1 -1
  42. data/spec/restify/features/response_bodies_spec.rb +45 -0
  43. data/spec/restify/features/response_encoding_spec.rb +82 -0
  44. data/spec/restify/features/response_errors_spec.rb +22 -1
  45. data/spec/restify/features/webmock_spec.rb +92 -1
  46. data/spec/restify/global_spec.rb +78 -10
  47. data/spec/restify/processors/json_spec.rb +103 -0
  48. data/spec/restify/processors/msgpack_spec.rb +36 -0
  49. data/spec/restify/promise_spec.rb +288 -9
  50. data/spec/restify/relation_spec.rb +25 -6
  51. data/spec/restify/request_spec.rb +26 -0
  52. data/spec/restify/timeout_spec.rb +43 -33
  53. data/spec/spec_helper.rb +10 -6
  54. data/spec/support/stub_server.rb +4 -4
  55. metadata +32 -221
  56. data/vendor/bundle/ruby/4.0.0/bundler/gems/my-rubocop-2b861962124a/LICENSE +0 -21
  57. data/vendor/bundle/ruby/4.0.0/bundler/gems/my-rubocop-2b861962124a/README.md +0 -23
  58. data/vendor/bundle/ruby/4.0.0/gems/activesupport-8.1.3/CHANGELOG.md +0 -561
  59. data/vendor/bundle/ruby/4.0.0/gems/activesupport-8.1.3/MIT-LICENSE +0 -20
  60. data/vendor/bundle/ruby/4.0.0/gems/activesupport-8.1.3/README.rdoc +0 -40
  61. data/vendor/bundle/ruby/4.0.0/gems/addressable-2.9.0/CHANGELOG.md +0 -326
  62. data/vendor/bundle/ruby/4.0.0/gems/addressable-2.9.0/LICENSE.txt +0 -202
  63. data/vendor/bundle/ruby/4.0.0/gems/addressable-2.9.0/README.md +0 -121
  64. data/vendor/bundle/ruby/4.0.0/gems/ast-2.4.3/LICENSE.MIT +0 -20
  65. data/vendor/bundle/ruby/4.0.0/gems/ast-2.4.3/README.YARD.md +0 -12
  66. data/vendor/bundle/ruby/4.0.0/gems/base64-0.3.0/README.md +0 -48
  67. data/vendor/bundle/ruby/4.0.0/gems/bigdecimal-4.1.2/LICENSE +0 -56
  68. data/vendor/bundle/ruby/4.0.0/gems/concurrent-ruby-1.3.7/CHANGELOG.md +0 -615
  69. data/vendor/bundle/ruby/4.0.0/gems/concurrent-ruby-1.3.7/LICENSE.txt +0 -21
  70. data/vendor/bundle/ruby/4.0.0/gems/concurrent-ruby-1.3.7/README.md +0 -409
  71. data/vendor/bundle/ruby/4.0.0/gems/connection_pool-3.0.2/LICENSE +0 -20
  72. data/vendor/bundle/ruby/4.0.0/gems/connection_pool-3.0.2/README.md +0 -182
  73. data/vendor/bundle/ruby/4.0.0/gems/crack-1.0.1/LICENSE +0 -20
  74. data/vendor/bundle/ruby/4.0.0/gems/crack-1.0.1/README.md +0 -43
  75. data/vendor/bundle/ruby/4.0.0/gems/diff-lcs-1.6.2/CHANGELOG.md +0 -518
  76. data/vendor/bundle/ruby/4.0.0/gems/diff-lcs-1.6.2/README.md +0 -92
  77. data/vendor/bundle/ruby/4.0.0/gems/docile-1.4.1/LICENSE +0 -21
  78. data/vendor/bundle/ruby/4.0.0/gems/docile-1.4.1/README.md +0 -409
  79. data/vendor/bundle/ruby/4.0.0/gems/drb-2.2.3/LICENSE.txt +0 -22
  80. data/vendor/bundle/ruby/4.0.0/gems/ethon-0.18.0/CHANGELOG.md +0 -417
  81. data/vendor/bundle/ruby/4.0.0/gems/ethon-0.18.0/LICENSE +0 -20
  82. data/vendor/bundle/ruby/4.0.0/gems/ethon-0.18.0/README.md +0 -118
  83. data/vendor/bundle/ruby/4.0.0/gems/ffi-1.17.4-x86_64-linux-gnu/CHANGELOG.md +0 -507
  84. data/vendor/bundle/ruby/4.0.0/gems/ffi-1.17.4-x86_64-linux-gnu/LICENSE +0 -24
  85. data/vendor/bundle/ruby/4.0.0/gems/ffi-1.17.4-x86_64-linux-gnu/LICENSE.SPECS +0 -22
  86. data/vendor/bundle/ruby/4.0.0/gems/ffi-1.17.4-x86_64-linux-gnu/README.md +0 -138
  87. data/vendor/bundle/ruby/4.0.0/gems/hashdiff-1.2.1/LICENSE +0 -19
  88. data/vendor/bundle/ruby/4.0.0/gems/hashdiff-1.2.1/README.md +0 -323
  89. data/vendor/bundle/ruby/4.0.0/gems/hashdiff-1.2.1/changelog.md +0 -127
  90. data/vendor/bundle/ruby/4.0.0/gems/hitimes-3.2.0/LICENSE.txt +0 -16
  91. data/vendor/bundle/ruby/4.0.0/gems/hitimes-3.2.0/README.md +0 -187
  92. data/vendor/bundle/ruby/4.0.0/gems/i18n-1.15.2/MIT-LICENSE +0 -20
  93. data/vendor/bundle/ruby/4.0.0/gems/i18n-1.15.2/README.md +0 -139
  94. data/vendor/bundle/ruby/4.0.0/gems/json-2.19.9/README.md +0 -310
  95. data/vendor/bundle/ruby/4.0.0/gems/language_server-protocol-3.17.0.5/LICENSE.txt +0 -21
  96. data/vendor/bundle/ruby/4.0.0/gems/language_server-protocol-3.17.0.5/README.md +0 -88
  97. data/vendor/bundle/ruby/4.0.0/gems/lint_roller-1.1.0/CHANGELOG.md +0 -15
  98. data/vendor/bundle/ruby/4.0.0/gems/lint_roller-1.1.0/LICENSE.txt +0 -21
  99. data/vendor/bundle/ruby/4.0.0/gems/lint_roller-1.1.0/README.md +0 -173
  100. data/vendor/bundle/ruby/4.0.0/gems/little-plugger-1.1.4/README.rdoc +0 -53
  101. data/vendor/bundle/ruby/4.0.0/gems/logger-1.7.0/README.md +0 -104
  102. data/vendor/bundle/ruby/4.0.0/gems/logging-2.4.0/LICENSE +0 -22
  103. data/vendor/bundle/ruby/4.0.0/gems/logging-2.4.0/README.md +0 -140
  104. data/vendor/bundle/ruby/4.0.0/gems/minitest-6.0.6/README.rdoc +0 -763
  105. data/vendor/bundle/ruby/4.0.0/gems/msgpack-1.8.3/ChangeLog +0 -383
  106. data/vendor/bundle/ruby/4.0.0/gems/msgpack-1.8.3/LICENSE +0 -177
  107. data/vendor/bundle/ruby/4.0.0/gems/msgpack-1.8.3/README.md +0 -302
  108. data/vendor/bundle/ruby/4.0.0/gems/multi_json-1.21.1/LICENSE.md +0 -20
  109. data/vendor/bundle/ruby/4.0.0/gems/multi_json-1.21.1/README.md +0 -281
  110. data/vendor/bundle/ruby/4.0.0/gems/nio4r-2.7.5/ext/libev/LICENSE +0 -37
  111. data/vendor/bundle/ruby/4.0.0/gems/nio4r-2.7.5/ext/libev/README +0 -59
  112. data/vendor/bundle/ruby/4.0.0/gems/nio4r-2.7.5/license.md +0 -80
  113. data/vendor/bundle/ruby/4.0.0/gems/nio4r-2.7.5/readme.md +0 -91
  114. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-api-1.10.0/CHANGELOG.md +0 -219
  115. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-api-1.10.0/LICENSE +0 -201
  116. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-api-1.10.0/README.md +0 -68
  117. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-common-0.25.0/CHANGELOG.md +0 -110
  118. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-common-0.25.0/LICENSE +0 -201
  119. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-common-0.25.0/README.md +0 -62
  120. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-instrumentation-base-0.26.1/CHANGELOG.md +0 -97
  121. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-instrumentation-base-0.26.1/LICENSE +0 -201
  122. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-instrumentation-base-0.26.1/README.md +0 -155
  123. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-instrumentation-ethon-0.29.0/CHANGELOG.md +0 -192
  124. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-instrumentation-ethon-0.29.0/LICENSE +0 -201
  125. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-instrumentation-ethon-0.29.0/README.md +0 -66
  126. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-registry-0.6.0/CHANGELOG.md +0 -31
  127. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-registry-0.6.0/LICENSE +0 -201
  128. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-registry-0.6.0/README.md +0 -40
  129. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-sdk-1.12.0/CHANGELOG.md +0 -302
  130. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-sdk-1.12.0/LICENSE +0 -201
  131. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-sdk-1.12.0/README.md +0 -101
  132. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-semantic_conventions-1.41.0/CHANGELOG.md +0 -65
  133. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-semantic_conventions-1.41.0/LICENSE +0 -201
  134. data/vendor/bundle/ruby/4.0.0/gems/opentelemetry-semantic_conventions-1.41.0/README.md +0 -115
  135. data/vendor/bundle/ruby/4.0.0/gems/parallel-2.1.0/MIT-LICENSE.txt +0 -20
  136. data/vendor/bundle/ruby/4.0.0/gems/parser-3.3.11.1/LICENSE.txt +0 -26
  137. data/vendor/bundle/ruby/4.0.0/gems/prism-1.9.0/CHANGELOG.md +0 -786
  138. data/vendor/bundle/ruby/4.0.0/gems/prism-1.9.0/LICENSE.md +0 -7
  139. data/vendor/bundle/ruby/4.0.0/gems/prism-1.9.0/README.md +0 -143
  140. data/vendor/bundle/ruby/4.0.0/gems/public_suffix-7.0.5/CHANGELOG.md +0 -649
  141. data/vendor/bundle/ruby/4.0.0/gems/public_suffix-7.0.5/LICENSE.txt +0 -22
  142. data/vendor/bundle/ruby/4.0.0/gems/public_suffix-7.0.5/README.md +0 -231
  143. data/vendor/bundle/ruby/4.0.0/gems/puma-8.0.2/LICENSE +0 -29
  144. data/vendor/bundle/ruby/4.0.0/gems/puma-8.0.2/README.md +0 -484
  145. data/vendor/bundle/ruby/4.0.0/gems/puma-8.0.2/docs/jungle/README.md +0 -9
  146. data/vendor/bundle/ruby/4.0.0/gems/puma-8.0.2/docs/jungle/rc.d/README.md +0 -74
  147. data/vendor/bundle/ruby/4.0.0/gems/racc-1.8.1/ChangeLog +0 -846
  148. data/vendor/bundle/ruby/4.0.0/gems/racc-1.8.1/README.ja.rdoc +0 -58
  149. data/vendor/bundle/ruby/4.0.0/gems/racc-1.8.1/README.rdoc +0 -60
  150. data/vendor/bundle/ruby/4.0.0/gems/rack-3.2.6/CHANGELOG.md +0 -1346
  151. data/vendor/bundle/ruby/4.0.0/gems/rack-3.2.6/MIT-LICENSE +0 -20
  152. data/vendor/bundle/ruby/4.0.0/gems/rack-3.2.6/README.md +0 -384
  153. data/vendor/bundle/ruby/4.0.0/gems/rainbow-3.1.1/Changelog.md +0 -101
  154. data/vendor/bundle/ruby/4.0.0/gems/rainbow-3.1.1/LICENSE +0 -20
  155. data/vendor/bundle/ruby/4.0.0/gems/rainbow-3.1.1/README.markdown +0 -227
  156. data/vendor/bundle/ruby/4.0.0/gems/rake-13.4.2/MIT-LICENSE +0 -21
  157. data/vendor/bundle/ruby/4.0.0/gems/rake-13.4.2/README.rdoc +0 -155
  158. data/vendor/bundle/ruby/4.0.0/gems/rake-release-1.4.0/LICENSE +0 -21
  159. data/vendor/bundle/ruby/4.0.0/gems/rake-release-1.4.0/README.md +0 -107
  160. data/vendor/bundle/ruby/4.0.0/gems/regexp_parser-2.12.0/LICENSE +0 -22
  161. data/vendor/bundle/ruby/4.0.0/gems/rexml-3.4.4/LICENSE.txt +0 -22
  162. data/vendor/bundle/ruby/4.0.0/gems/rexml-3.4.4/README.md +0 -57
  163. data/vendor/bundle/ruby/4.0.0/gems/rspec-3.13.2/LICENSE.md +0 -27
  164. data/vendor/bundle/ruby/4.0.0/gems/rspec-3.13.2/README.md +0 -47
  165. data/vendor/bundle/ruby/4.0.0/gems/rspec-core-3.13.6/Changelog.md +0 -2447
  166. data/vendor/bundle/ruby/4.0.0/gems/rspec-core-3.13.6/LICENSE.md +0 -26
  167. data/vendor/bundle/ruby/4.0.0/gems/rspec-core-3.13.6/README.md +0 -389
  168. data/vendor/bundle/ruby/4.0.0/gems/rspec-expectations-3.13.5/Changelog.md +0 -1366
  169. data/vendor/bundle/ruby/4.0.0/gems/rspec-expectations-3.13.5/LICENSE.md +0 -25
  170. data/vendor/bundle/ruby/4.0.0/gems/rspec-expectations-3.13.5/README.md +0 -326
  171. data/vendor/bundle/ruby/4.0.0/gems/rspec-mocks-3.13.8/Changelog.md +0 -1351
  172. data/vendor/bundle/ruby/4.0.0/gems/rspec-mocks-3.13.8/LICENSE.md +0 -25
  173. data/vendor/bundle/ruby/4.0.0/gems/rspec-mocks-3.13.8/README.md +0 -465
  174. data/vendor/bundle/ruby/4.0.0/gems/rspec-support-3.13.7/Changelog.md +0 -444
  175. data/vendor/bundle/ruby/4.0.0/gems/rspec-support-3.13.7/LICENSE.md +0 -23
  176. data/vendor/bundle/ruby/4.0.0/gems/rspec-support-3.13.7/README.md +0 -40
  177. data/vendor/bundle/ruby/4.0.0/gems/rubocop-1.88.0/LICENSE.txt +0 -20
  178. data/vendor/bundle/ruby/4.0.0/gems/rubocop-1.88.0/README.md +0 -255
  179. data/vendor/bundle/ruby/4.0.0/gems/rubocop-ast-1.49.1/LICENSE.txt +0 -20
  180. data/vendor/bundle/ruby/4.0.0/gems/rubocop-ast-1.49.1/README.md +0 -54
  181. data/vendor/bundle/ruby/4.0.0/gems/rubocop-capybara-2.23.0/CHANGELOG.md +0 -116
  182. data/vendor/bundle/ruby/4.0.0/gems/rubocop-capybara-2.23.0/MIT-LICENSE.md +0 -21
  183. data/vendor/bundle/ruby/4.0.0/gems/rubocop-capybara-2.23.0/README.md +0 -91
  184. data/vendor/bundle/ruby/4.0.0/gems/rubocop-factory_bot-2.28.0/CHANGELOG.md +0 -135
  185. data/vendor/bundle/ruby/4.0.0/gems/rubocop-factory_bot-2.28.0/MIT-LICENSE.md +0 -21
  186. data/vendor/bundle/ruby/4.0.0/gems/rubocop-factory_bot-2.28.0/README.md +0 -91
  187. data/vendor/bundle/ruby/4.0.0/gems/rubocop-performance-1.26.1/LICENSE.txt +0 -20
  188. data/vendor/bundle/ruby/4.0.0/gems/rubocop-performance-1.26.1/README.md +0 -100
  189. data/vendor/bundle/ruby/4.0.0/gems/rubocop-rspec-3.10.2/CHANGELOG.md +0 -1136
  190. data/vendor/bundle/ruby/4.0.0/gems/rubocop-rspec-3.10.2/MIT-LICENSE.md +0 -21
  191. data/vendor/bundle/ruby/4.0.0/gems/rubocop-rspec-3.10.2/README.md +0 -112
  192. data/vendor/bundle/ruby/4.0.0/gems/rubocop-rspec_rails-2.32.0/CHANGELOG.md +0 -98
  193. data/vendor/bundle/ruby/4.0.0/gems/rubocop-rspec_rails-2.32.0/MIT-LICENSE.md +0 -21
  194. data/vendor/bundle/ruby/4.0.0/gems/rubocop-rspec_rails-2.32.0/README.md +0 -93
  195. data/vendor/bundle/ruby/4.0.0/gems/ruby-progressbar-1.13.0/LICENSE.txt +0 -19
  196. data/vendor/bundle/ruby/4.0.0/gems/ruby-progressbar-1.13.0/README.md +0 -131
  197. data/vendor/bundle/ruby/4.0.0/gems/securerandom-0.4.1/README.md +0 -72
  198. data/vendor/bundle/ruby/4.0.0/gems/simplecov-0.22.0/CHANGELOG.md +0 -191
  199. data/vendor/bundle/ruby/4.0.0/gems/simplecov-0.22.0/LICENSE +0 -20
  200. data/vendor/bundle/ruby/4.0.0/gems/simplecov-0.22.0/README.md +0 -974
  201. data/vendor/bundle/ruby/4.0.0/gems/simplecov-cobertura-3.2.0/LICENSE +0 -202
  202. data/vendor/bundle/ruby/4.0.0/gems/simplecov-cobertura-3.2.0/README.md +0 -65
  203. data/vendor/bundle/ruby/4.0.0/gems/simplecov-html-0.13.2/CHANGELOG.md +0 -114
  204. data/vendor/bundle/ruby/4.0.0/gems/simplecov-html-0.13.2/LICENSE +0 -20
  205. data/vendor/bundle/ruby/4.0.0/gems/simplecov-html-0.13.2/README.md +0 -30
  206. data/vendor/bundle/ruby/4.0.0/gems/simplecov_json_formatter-0.1.4/CHANGELOG.md +0 -13
  207. data/vendor/bundle/ruby/4.0.0/gems/simplecov_json_formatter-0.1.4/README.md +0 -29
  208. data/vendor/bundle/ruby/4.0.0/gems/typhoeus-1.6.0/CHANGELOG.md +0 -465
  209. data/vendor/bundle/ruby/4.0.0/gems/typhoeus-1.6.0/LICENSE +0 -22
  210. data/vendor/bundle/ruby/4.0.0/gems/typhoeus-1.6.0/README.md +0 -588
  211. data/vendor/bundle/ruby/4.0.0/gems/tzinfo-2.0.6/LICENSE +0 -19
  212. data/vendor/bundle/ruby/4.0.0/gems/tzinfo-2.0.6/README.md +0 -406
  213. data/vendor/bundle/ruby/4.0.0/gems/unicode-display_width-3.2.0/CHANGELOG.md +0 -299
  214. data/vendor/bundle/ruby/4.0.0/gems/unicode-display_width-3.2.0/MIT-LICENSE.txt +0 -22
  215. data/vendor/bundle/ruby/4.0.0/gems/unicode-display_width-3.2.0/README.md +0 -194
  216. data/vendor/bundle/ruby/4.0.0/gems/unicode-emoji-4.2.0/CHANGELOG.md +0 -202
  217. data/vendor/bundle/ruby/4.0.0/gems/unicode-emoji-4.2.0/MIT-LICENSE.txt +0 -20
  218. data/vendor/bundle/ruby/4.0.0/gems/unicode-emoji-4.2.0/README.md +0 -205
  219. data/vendor/bundle/ruby/4.0.0/gems/uri-1.1.1/README.md +0 -55
  220. data/vendor/bundle/ruby/4.0.0/gems/webmock-3.26.2/CHANGELOG.md +0 -2148
  221. data/vendor/bundle/ruby/4.0.0/gems/webmock-3.26.2/LICENSE +0 -20
  222. data/vendor/bundle/ruby/4.0.0/gems/webmock-3.26.2/README.md +0 -1229
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Restify
4
+ module Adapter
5
+ class Ethon < Base
6
+ #
7
+ # Idle libcurl easy handles, kept for reuse.
8
+ #
9
+ # Handles can be taken and released from any thread. A handle must
10
+ # only be released once libcurl is done with it.
11
+ #
12
+ # @api private
13
+ #
14
+ class Pool
15
+ def initialize(size:, &factory)
16
+ @size = size
17
+ @factory = factory
18
+ @mutex = Mutex.new
19
+ @idle = []
20
+ end
21
+
22
+ def checkout
23
+ @mutex.synchronize { @idle.pop } || @factory.call
24
+ end
25
+
26
+ def release(easy)
27
+ easy.reset
28
+
29
+ @mutex.synchronize { @idle << easy if @idle.size < @size }
30
+ end
31
+
32
+ # Forget all handles without releasing them as they belong to
33
+ # the parent process.
34
+ def forked!
35
+ @mutex.synchronize do
36
+ @idle.each {|easy| easy.handle.autorelease = false }
37
+ @idle.clear
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ethon'
4
+ require 'nio'
5
+
6
+ require 'restify/adapter/ethon/options'
7
+ require 'restify/adapter/ethon/easy'
8
+ require 'restify/adapter/ethon/event_loop'
9
+ require 'restify/adapter/ethon/pool'
10
+
11
+ module Restify
12
+ module Adapter
13
+ #
14
+ # An adapter using libcurl's multi interface via `Ethon` in
15
+ # `socket_action` mode, driven by a nio4r-based event loop.
16
+ #
17
+ # Requests are handed over to the event loop using a queue and the
18
+ # loop is woken up via `NIO::Selector#wakeup`. Both are safe to call
19
+ # from any thread.
20
+ #
21
+ # This is important because the event loop is run from either any
22
+ # waiting thread or a dedicated background thread.
23
+ #
24
+ # The background thread that is spawned on demand and restarted if
25
+ # it ever dies. A thread waiting on a request's promise takes over
26
+ # the loop and runs it itself until the promise is complete, see
27
+ # `#drive`. This avoids handing each request and response between
28
+ # threads, which is expensive (thread wakeup). Only one thread runs
29
+ # the loop at a time, therefore all libcurl and selector interaction
30
+ # is still serialized.
31
+ #
32
+ class Ethon < Base
33
+ include Logging
34
+
35
+ # Restify follows relations from URLs in server responses, and
36
+ # libcurl supports far more than HTTP, e.g. `file://` or `scp://`.
37
+ # Therefore only HTTP-like protocols must ever be used, both for
38
+ # the request itself and when following redirects.
39
+ #
40
+ # This is intentionally not configurable.
41
+ PROTOCOLS = %i[http https].freeze
42
+
43
+ # Set headers bypassing the wait for 100-continue responses many
44
+ # servers do not send correctly.
45
+ DEFAULT_HEADERS = {
46
+ 'Expect' => '',
47
+ 'Transfer-Encoding' => '',
48
+ }.freeze
49
+
50
+ DEFAULT_OPTIONS = {
51
+ followlocation: true,
52
+ tcp_keepalive: true,
53
+ tcp_keepidle: 5,
54
+ tcp_keepintvl: 5,
55
+ }.freeze
56
+
57
+ ENFORCED_OPTIONS = {
58
+ # Do not use signal handlers in libcurl, as it would interfere
59
+ # with the threads.
60
+ nosignal: true,
61
+
62
+ # Do not allow using and redirecting to unexpected (e.g. local)
63
+ # protocols.
64
+ protocols: PROTOCOLS,
65
+ redir_protocols: PROTOCOLS,
66
+ }.freeze
67
+
68
+ # Maximum number of idle easy handles kept for reuse.
69
+ POOL_SIZE = 64
70
+
71
+ def initialize(options: {}, **multi)
72
+ @options = Options.new({
73
+ **DEFAULT_OPTIONS,
74
+ **options,
75
+ **ENFORCED_OPTIONS,
76
+ })
77
+ @multi_options = multi
78
+
79
+ @mutex = Mutex.new
80
+ @thread = nil
81
+
82
+ setup
83
+
84
+ super()
85
+ end
86
+
87
+ def call_native(request, writer)
88
+ check_fork!
89
+
90
+ easy = convert(request, writer)
91
+
92
+ debug 'request:add',
93
+ tag: request.object_id,
94
+ method: request.method.upcase,
95
+ url: request.uri,
96
+ timeout: request.timeout
97
+
98
+ # Ensure the event loop is running and let it pick up the new
99
+ # request.
100
+ thread
101
+
102
+ @events.enqueue(easy)
103
+ end
104
+
105
+ # Run the event loop in the calling thread until the promise is
106
+ # complete or the timeout expires, see `Promise#wait` and
107
+ # `EventLoop#drive`.
108
+ #
109
+ # This only processes transfers, i.e. fulfills or rejects the
110
+ # adapter's promises. Callbacks chained with `Promise#then` still
111
+ # only run in the thread waiting on them.
112
+ #
113
+ # Returns false when the loop cannot be run in the calling thread,
114
+ # i.e. when waiting from within the loop itself.
115
+ #
116
+ def drive(promise, timeout)
117
+ check_fork!
118
+
119
+ @events.drive(promise, timeout)
120
+ end
121
+
122
+ private
123
+
124
+ def driver
125
+ self
126
+ end
127
+
128
+ def setup
129
+ @pid = Process.pid
130
+ @pool = Pool.new(size: POOL_SIZE) { Easy.new }
131
+ @events = EventLoop.new(**@multi_options)
132
+ end
133
+
134
+ def check_fork!
135
+ return if @pid == Process.pid
136
+
137
+ @mutex.synchronize { forked! if @pid != Process.pid }
138
+ end
139
+
140
+ # A forked child process inherits the parent's event loop,
141
+ # connections and sockets. They must neither be used nor cleaned
142
+ # up here, as the parent still uses them.
143
+ #
144
+ # Therefore, abandon the state without releasing it, and set up
145
+ # everything again.
146
+ def forked!
147
+ @events.forked!
148
+ @pool.forked!
149
+
150
+ @thread = nil
151
+ setup
152
+ end
153
+
154
+ def convert(request, writer)
155
+ @pool.checkout.tap do |easy|
156
+ easy._otel_span = OpenTelemetry::Trace.current_span
157
+ easy._restify_request = request
158
+ easy._restify_writer = writer
159
+
160
+ easy.prepare(request, @options)
161
+
162
+ easy.on_complete do |completed|
163
+ complete(completed, request, writer)
164
+
165
+ @pool.release(completed)
166
+
167
+ # Wake up threads waiting on the loop to check if their
168
+ # result is available now.
169
+ @events.notify
170
+ end
171
+ end
172
+ end
173
+
174
+ def complete(easy, request, writer)
175
+ writer.set do
176
+ if logger&.debug?
177
+ debug 'request:complete',
178
+ tag: request.object_id,
179
+ status: easy.response_code,
180
+ message: easy.return_code
181
+ end
182
+
183
+ easy.response(request)
184
+ end
185
+ rescue StandardError => e
186
+ # This runs inside a libcurl callback, therefore no exception
187
+ # must ever escape from here. Anything reaching this point could
188
+ # not be handed to the promise anymore.
189
+ error(e)
190
+ end
191
+
192
+ def thread
193
+ @mutex.synchronize do
194
+ # Spawn thread if not yet started, or recreate it if it died.
195
+ if @thread.nil? || !@thread.status
196
+ debug 'loop:spawn'
197
+
198
+ events = @events
199
+ @thread = Thread.new { events.run }
200
+ end
201
+
202
+ @thread
203
+ end
204
+ end
205
+
206
+ def _log_prefix
207
+ "[#{object_id}/#{Thread.current.object_id}]"
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Restify
4
+ module Adapter
5
+ #
6
+ # Manages coordination for exclusive ownership of an event loop,
7
+ # shared between a background thread and threads waiting on results.
8
+ # This way, the event loop can run on waiting threads directly,
9
+ # avoiding costly context switches to the background thread.
10
+ #
11
+ # Waiting threads have priority: the background thread only acquires
12
+ # the loop while no thread is waiting for it, and is interrupted to
13
+ # release it as soon as one is.
14
+ #
15
+ # Waiting threads can give up waiting once their result is
16
+ # available, e.g. because another thread running the loop produced
17
+ # it. Whoever runs the loop must call `#notify` after producing a
18
+ # result.
19
+ #
20
+ class LoopLock
21
+ # @yield Called when a waiting thread needs the background thread to
22
+ # release the loop, e.g. to wake it up from `select`.
23
+ #
24
+ def initialize(&interrupt)
25
+ @interrupt = interrupt
26
+
27
+ reset!
28
+ end
29
+
30
+ # Acquire the loop for a thread waiting on a result.
31
+ #
32
+ # @param timeout [Restify::Timeout, Numeric] Maximum time to wait,
33
+ # derived from the promise timeout of the waiting thread.
34
+ #
35
+ # @yieldreturn [Boolean] Whether the result is available, and
36
+ # the loop is not needed anymore.
37
+ #
38
+ # @return [Boolean] true when the calling thread holds the loop,
39
+ # false when the result is available or the timeout expired
40
+ # first.
41
+ #
42
+ def acquire(timeout, &done)
43
+ timeout = Timeout.new(timeout)
44
+
45
+ background = @mutex.synchronize do
46
+ @waiters += 1
47
+ @background
48
+ end
49
+
50
+ @interrupt&.call if background
51
+
52
+ @mutex.synchronize do
53
+ loop do
54
+ return false if done&.call
55
+
56
+ remaining = timeout.remaining
57
+ return false unless remaining.positive?
58
+
59
+ if free?
60
+ @owner = Thread.current
61
+ return true
62
+ end
63
+
64
+ @changed.wait(@mutex, remaining)
65
+ end
66
+ ensure
67
+ @waiters -= 1
68
+
69
+ # Let the background thread continue.
70
+ @changed.broadcast if @waiters.zero?
71
+ end
72
+ end
73
+
74
+ # Acquire the loop for the background thread. Blocks while any
75
+ # other thread holds the loop or waits for it.
76
+ def acquire_background
77
+ @mutex.synchronize do
78
+ @changed.wait(@mutex) until free? && @waiters.zero?
79
+
80
+ @owner = Thread.current
81
+ @background = true
82
+ end
83
+ end
84
+
85
+ def release
86
+ @mutex.synchronize do
87
+ @owner = nil
88
+ @background = false
89
+ @changed.broadcast
90
+ end
91
+ end
92
+
93
+ def owned?
94
+ @owner.equal?(Thread.current)
95
+ end
96
+
97
+ def notify
98
+ return unless @waiters.positive?
99
+
100
+ @mutex.synchronize { @changed.broadcast }
101
+ end
102
+
103
+ # Forget all threads holding or waiting for the loop, e.g. in a
104
+ # forked child process, where these threads do not exist.
105
+ #
106
+ # Must not be called while other threads use the lock.
107
+ def reset!
108
+ @mutex = Mutex.new
109
+ @changed = ConditionVariable.new
110
+ @owner = nil
111
+ @background = false
112
+ @waiters = 0
113
+ end
114
+
115
+ private
116
+
117
+ # A thread holding the loop can be gone without releasing it, e.g.
118
+ # when killed.
119
+ def free?
120
+ @owner.nil? || !@owner.alive?
121
+ end
122
+ end
123
+ end
124
+ end
@@ -8,23 +8,25 @@ module Restify
8
8
  module Telemetry
9
9
  def call(request)
10
10
  method = request.method.to_s.upcase
11
- uri = URI.parse(request.uri)
12
- name = "#{method} #{uri.scheme}://#{uri.host}:#{uri.port}"
11
+ uri = request.uri
12
+ port = uri.inferred_port
13
+ name = "#{method} #{uri.scheme}://#{uri.host}:#{port}"
13
14
 
15
+ # Remove `nil` as OTEL spans must not contain any nil attributes
14
16
  attributes = {
15
17
  'http.request.method' => method,
16
18
  'server.address' => uri.host,
17
- 'server.port' => uri.port,
19
+ 'server.port' => port,
18
20
  'url.full' => uri.to_s,
19
21
  'url.scheme' => uri.scheme,
20
- }
22
+ }.compact
21
23
 
22
24
  span = tracer.start_span(name, attributes:, kind: :client)
23
25
  OpenTelemetry::Trace.with_span(span) do
24
26
  OpenTelemetry.propagation.inject(request.headers)
25
27
 
26
28
  super.tap do |x|
27
- x.add_observer do |_, response, err|
29
+ x.add_observer do |response, err|
28
30
  if response
29
31
  span.set_attribute('http.response.status_code', response&.code)
30
32
  span.status = OpenTelemetry::Trace::Status.error unless (100..399).cover?(response&.code)
@@ -1,8 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'typhoeus'
4
-
5
- Ethon.logger = Logging.logger[Ethon]
3
+ begin
4
+ require 'typhoeus'
5
+ rescue LoadError => e
6
+ raise LoadError.new(
7
+ "Restify::Adapter::Typhoeus requires the `typhoeus` gem, add it to your Gemfile (#{e.message})",
8
+ )
9
+ end
6
10
 
7
11
  module Restify
8
12
  module Adapter
@@ -23,6 +27,14 @@ module Restify
23
27
  tcp_keepintvl: 5,
24
28
  }.freeze
25
29
 
30
+ # Restify follows relations from URLs in server responses, and
31
+ # libcurl supports far more than HTTP, e.g. `file://` or `scp://`.
32
+ # Therefore only HTTP-like protocols must ever be used, both for
33
+ # the request itself and when following redirects.
34
+ #
35
+ # This is intentionally not configurable.
36
+ PROTOCOLS = %i[http https].freeze
37
+
26
38
  # Patch Hydra to restore the correct OpenTelemetry span when
27
39
  # adding the request, so that the Ethon instrumentation can
28
40
  # properly pick up the context where the Restify request
@@ -41,8 +53,8 @@ module Restify
41
53
  end
42
54
  end
43
55
 
44
- def initialize(sync: false, options: {}, **kwargs)
45
- @hydra = ::Typhoeus::Hydra.new(**kwargs)
56
+ def initialize(sync: false, options: {}, **)
57
+ @hydra = ::Typhoeus::Hydra.new(**)
46
58
  @hydra.extend(EasyOverride)
47
59
 
48
60
  @mutex = Mutex.new
@@ -62,8 +74,11 @@ module Restify
62
74
  req = convert(request, writer)
63
75
 
64
76
  if sync?
65
- @hydra.queue(req)
66
- @hydra.run
77
+ begin
78
+ req.run
79
+ rescue Exception => e # rubocop:disable Lint/RescueException
80
+ writer.reject(e)
81
+ end
67
82
  else
68
83
  debug 'request:add',
69
84
  tag: request.object_id,
@@ -71,7 +86,7 @@ module Restify
71
86
  url: request.uri,
72
87
  timeout: request.timeout
73
88
 
74
- @queue << convert(request, writer)
89
+ @queue << req
75
90
 
76
91
  thread.run unless thread.status
77
92
  end
@@ -88,33 +103,38 @@ module Restify
88
103
  body: request.body,
89
104
  timeout: request.timeout,
90
105
  connecttimeout: request.timeout,
106
+
107
+ # Block accessing and redirecting to non-HTTP protocols:
108
+ protocols: PROTOCOLS,
109
+ redir_protocols: PROTOCOLS,
91
110
  ).tap do |req|
92
111
  req._otel_span = OpenTelemetry::Trace.current_span
93
112
  req._restify_writer = writer
94
113
 
95
114
  req.on_complete do |response|
96
- debug 'request:complete',
97
- tag: request.object_id,
98
- status: response.code,
99
- message: response.return_message,
100
- timeout: response.timed_out?
101
-
102
- if response.timed_out? || response.code.zero?
103
- writer.reject \
104
- Restify::NetworkError.new(request, response.return_message)
105
- else
106
- writer.fulfill convert_back(response, request)
115
+ writer.set do
116
+ debug 'request:complete',
117
+ tag: request.object_id,
118
+ status: response.code,
119
+ message: response.return_message,
120
+ timeout: response.timed_out?
121
+
122
+ if response.timed_out? || response.code.zero?
123
+ raise Restify::NetworkError.new(request, response.return_message)
124
+ end
125
+
126
+ convert_back(response, request)
107
127
  end
108
128
 
109
- # Add all newly queued requests to active hydra, e.g. requests
110
- # queued in a completion callback.
129
+ # Add all newly queued requests to active hydra, e.g.
130
+ # requests queued in a completion callback.
111
131
  dequeue_all
112
132
  end
113
133
  end
114
134
  end
115
135
 
116
136
  def convert_back(response, request)
117
- uri = request.uri
137
+ uri = effective_uri(response, request)
118
138
  status = response.code
119
139
  body = response.body
120
140
  headers = convert_headers(response.headers)
@@ -122,6 +142,15 @@ module Restify
122
142
  ::Restify::Response.new(request, uri, status, headers, body)
123
143
  end
124
144
 
145
+ def effective_uri(response, request)
146
+ url = response.effective_url
147
+
148
+ # Only parse the URL after redirects, as parsing is expensive.
149
+ return request.uri if url.nil? || url == request.uri.to_s
150
+
151
+ Addressable::URI.parse(url)
152
+ end
153
+
125
154
  def convert_headers(headers)
126
155
  return {} unless headers.respond_to?(:each_pair)
127
156
 
@@ -165,7 +194,7 @@ module Restify
165
194
  runs += 1
166
195
  debug 'hydra:completed'
167
196
  rescue StandardError => e
168
- logger.error(e)
197
+ error(e)
169
198
  end
170
199
  ensure
171
200
  debug 'hydra:exit'
@@ -30,13 +30,23 @@ module Restify
30
30
  @options = kwargs
31
31
  end
32
32
 
33
+ # Resolve a URI against the context URI. Absolute URIs are returned
34
+ # as they are, e.g. when already joined by a relation.
35
+ #
36
+ # @param uri [Addressable::URI, String] URI to resolve.
37
+ # @return [Addressable::URI] Absolute URI.
38
+ #
33
39
  def join(uri)
34
- self.uri.join uri
40
+ if uri.is_a?(Addressable::URI) && uri.absolute?
41
+ uri
42
+ else
43
+ self.uri.join(uri)
44
+ end
35
45
  end
36
46
 
37
- def inherit(uri, **kwargs)
47
+ def inherit(uri, **)
38
48
  uri ||= self.uri
39
- Context.new(uri, **kwargs, **options)
49
+ Context.new(uri, **, **options)
40
50
  end
41
51
 
42
52
  def process(response)
@@ -47,16 +57,16 @@ module Restify
47
57
  processor.new(context, response).resource
48
58
  end
49
59
 
50
- def request(method, uri, data: nil, headers: {}, **kwargs)
60
+ def request(method, uri, data: nil, headers: {}, **)
51
61
  request = Request.new(
52
62
  headers: default_headers.merge(headers),
53
- **kwargs,
63
+ **,
54
64
  method:,
55
65
  uri: join(uri),
56
66
  data:,
57
67
  )
58
68
 
59
- ret = cache.call(request) {|req| adapter.call(req) }
69
+ ret = cache ? cache.call(request) {|req| adapter.call(req) } : adapter.call(request)
60
70
  ret.then do |response|
61
71
  raise ResponseError.from_code(response) if response.errored?
62
72
 
data/lib/restify/error.rb CHANGED
@@ -13,6 +13,19 @@ module Restify
13
13
  end
14
14
  end
15
15
 
16
+ # A {ParseError} is raised when accessing the data of a resource,
17
+ # whose response body could not be parsed, e.g. invalid JSON. The
18
+ # original error is available as {#cause}.
19
+ #
20
+ class ParseError < StandardError
21
+ attr_reader :response
22
+
23
+ def initialize(response, message)
24
+ @response = response
25
+ super("Could not parse response body for `#{response&.uri}': #{message}")
26
+ end
27
+ end
28
+
16
29
  # A {ResponseError} is returned on a non-successful
17
30
  # response from server. Usually it will either be a
18
31
  # {ClientError} or a {ServerError}.
@@ -2,47 +2,26 @@
2
2
 
3
3
  module Restify
4
4
  module Global
5
- def new(uri, **opts)
6
- context = resolve_context uri, **opts
5
+ def new(uri, **)
6
+ context = resolve_context(uri, **)
7
7
 
8
8
  Relation.new context, context.uri
9
9
  end
10
10
 
11
- def adapter
12
- @adapter ||= begin
13
- Restify::Adapter::Typhoeus.new
14
- end
15
- end
16
-
17
- def adapter=(adapter)
18
- @adapter = adapter
19
- end
20
-
21
- def cache
22
- @cache ||= begin
23
- require 'active_support'
24
- require 'active_support/cache'
25
- Restify::Cache.new store: ActiveSupport::Cache::MemoryStore.new
26
- end
27
- end
28
-
29
- def cache=(cache)
30
- @cache = cache
31
- end
11
+ attr_writer :adapter
12
+ attr_accessor :cache, :logger
32
13
 
33
- def logger
34
- ::Logging.logger[Restify]
14
+ def adapter
15
+ @adapter ||= Restify::Adapter::Ethon.new
35
16
  end
36
17
 
37
- def logger=(logger); end
38
-
39
18
  private
40
19
 
41
- def resolve_context(uri, **opts)
20
+ def resolve_context(uri, **)
42
21
  if uri.is_a? Symbol
43
- Restify::Registry.fetch(uri).inherit(nil, **opts)
22
+ Restify::Registry.fetch(uri).inherit(nil, **)
44
23
  else
45
- Context.new(uri, **opts)
24
+ Context.new(uri, **)
46
25
  end
47
26
  end
48
27
  end