Class: HTTPX::Session
- Inherits:
-
Object
- Object
- HTTPX::Session
- Includes:
- Chainable, Loggable, ForkTracker
- Defined in:
- lib/httpx/session.rb,
sig/session.rbs
Overview
Class implementing the APIs being used publicly.
HTTPX.get(..) #=> delegating to an internal HTTPX::Session object.
HTTPX.plugin(..).get(..) #=> creating an intermediate HTTPX::Session with plugin, then sending the GET request
Defined Under Namespace
Modules: ForkTracker
Constant Summary collapse
- INSTANCES =
nil- EMPTY_HASH =
Constants included from Loggable
Loggable::COLORS, Loggable::USE_DEBUG_LOG
Class Attribute Summary collapse
-
.default_options ⇒ Options
readonly
Returns the value of attribute default_options.
Class Method Summary collapse
- .after_fork ⇒ void
- .inherited(klass) ⇒ Object
-
.plugin(pl, options = nil, &block) ⇒ void
returns a new HTTPX::Session instance, with the plugin pointed by
plloaded.
Instance Method Summary collapse
-
#build_request(verb, uri, params = EMPTY_HASH, options = @options) ⇒ Request
returns a HTTP::Request instance built from the HTTP
verb, the requesturi, and the optional set of request-specificoptions. -
#build_requests(*args, params) ⇒ Object
returns a set of HTTPX::Request objects built from the given
argsandoptions. -
#close(selector = Selector.new) ⇒ void
closes all the active connections from the session.
-
#coalesce_connections(conn1, conn2, selector, from_pool) ⇒ void
coalesces
conn2intoconn1. -
#deactivate(selector) ⇒ void
tries deactivating connections in the
selector, deregistering the ones that have been deactivated. - #deselect_connection(connection, selector, cloned = false) ⇒ void
- #deselect_resolver(resolver, selector) ⇒ void
- #do_init_connection(connection, selector) ⇒ void
- #early_resolve(resolver, connection) ⇒ Boolean
-
#fetch_response(request, _selector, _options) ⇒ response?
returns the corresponding HTTP::Response to the given
requestif it has been received. -
#find_connection(request_uri, selector, options) ⇒ Connection
returns the HTTPX::Connection through which the
requestshould be sent through. - #find_resolver_for(connection, selector) ⇒ resolver
- #get_current_selector { ... } ⇒ Object
-
#initialize(options = EMPTY_HASH) {|arg0| ... } ⇒ Session
constructor
initializes the session with a set of
options, which will be shared by all requests sent from it. -
#on_promise(_, stream) ⇒ void
callback executed when an HTTP/2 promise frame has been received.
- #on_resolver_close ⇒ void
- #on_resolver_connection(connection, selector) ⇒ void
- #pin(conn_or_resolver, selector) ⇒ void
-
#receive_requests(requests, selector) ⇒ Array[response]
returns the array of HTTPX::Response objects corresponding to the array of HTTPX::Request
requests. -
#request(*args, **params) ⇒ Object
performs one, or multple requests; it accepts:.
- #resolve_connection(connection, selector) ⇒ void
- #select_connection(connection, selector) ⇒ void (also: #select_resolver)
- #selector_close(selector) ⇒ void
- #selector_store ⇒ Hash[instance, Selector]
-
#send_request(request, selector, options = request.options) ⇒ void
sends the
requestto the corresponding HTTPX::Connection. -
#send_requests(*requests) ⇒ Array[response]
sends an array of HTTPX::Request
requests, returns the respective array of HTTPX::Response objects. - #set_current_selector(selector) ⇒ void
- #set_request_callbacks(request) ⇒ void
- #thread_selector_store(th) ⇒ Hash[instance, Selector]?
- #try_clone_connection(connection, selector, family) ⇒ Connection
-
#wrap {|arg0| ... } ⇒ void
Yields itself the block, then closes it after the block is evaluated.
Methods included from Chainable
#accept, #branch, #default_options, #plugin, #with
Methods included from Loggable
#log, #log_exception, log_identifiers, #log_redact, #log_redact_body, #log_redact_headers
Methods included from ForkTracker
Constructor Details
#initialize(arg0) ⇒ void #initialize(arg0) ⇒ void
initializes the session with a set of options, which will be shared by all
requests sent from it.
When pass a block, it'll yield itself to it, then closes after the block is evaluated.
16 17 18 19 20 21 22 23 24 |
# File 'lib/httpx/session.rb', line 16 def initialize( = EMPTY_HASH, &blk) @options = self.class..merge() @persistent = @options.persistent @pool = @options.pool_class.new(@options.) @wrapped = false @closing = false INSTANCES[self] = self if @persistent && @options.close_on_fork && INSTANCES wrap(&blk) if blk end |
Dynamic Method Handling
This class handles dynamic methods through the method_missing method in the class HTTPX::Chainable
Class Attribute Details
.default_options ⇒ Options (readonly)
Returns the value of attribute default_options.
483 484 485 |
# File 'lib/httpx/session.rb', line 483 def @default_options end |
Class Method Details
.after_fork ⇒ void
This method returns an undefined value.
577 578 579 580 |
# File 'lib/httpx/session.rb', line 577 def self.after_fork INSTANCES.each_value(&:close) nil end |
.inherited(klass) ⇒ Object
485 486 487 488 489 490 |
# File 'lib/httpx/session.rb', line 485 def inherited(klass) super klass.instance_variable_set(:@default_options, @default_options) klass.instance_variable_set(:@plugins, @plugins.dup) klass.instance_variable_set(:@callbacks, @callbacks.dup) end |
.plugin(pl, options = nil, &block) ⇒ void
This method returns an undefined value.
returns a new HTTPX::Session instance, with the plugin pointed by pl loaded.
session_with_retries = session.plugin(:retries)
session_with_custom = session.plugin(CustomPlugin)
497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 |
# File 'lib/httpx/session.rb', line 497 def plugin(pl, = nil, &block) label = pl pl = Plugins.load_plugin(pl) if pl.is_a?(Symbol) raise ArgumentError, "Invalid plugin type: #{pl.class.inspect}" unless pl.is_a?(Module) if !@plugins.include?(pl) @plugins << pl pl.load_dependencies(self, &block) if pl.respond_to?(:load_dependencies) @default_options = @default_options.dup include(pl::InstanceMethods) if defined?(pl::InstanceMethods) extend(pl::ClassMethods) if defined?(pl::ClassMethods) opts = @default_options opts.extend_with_plugin_classes(pl) if defined?(pl::OptionsMethods) # when a class gets dup'ed, the #initialize_dup callbacks isn't triggered. # moreover, and because #method_added does not get triggered on mixin include, # the callback is also forcefully manually called here. opts..instance_variable_set(:@options_names, opts...dup) (pl::OptionsMethods.instance_methods + pl::OptionsMethods.private_instance_methods - Object.instance_methods).each do |meth| opts..method_added(meth) end @default_options = opts..new(opts) end @default_options = pl.(@default_options) if pl.respond_to?(:extra_options) @default_options = @default_options.merge() if if pl.respond_to?(:subplugins) pl.subplugins.transform_keys(&Plugins.method(:load_plugin)).each do |main_pl, sub_pl| # in case the main plugin has already been loaded, then apply subplugin functionality # immediately next unless @plugins.include?(main_pl) plugin(sub_pl, , &block) end end pl.configure(self, &block) if pl.respond_to?(:configure) if label.is_a?(Symbol) # in case an already-loaded plugin complements functionality of # the plugin currently being loaded, loaded it now @plugins.each do |registered_pl| next if registered_pl == pl next unless registered_pl.respond_to?(:subplugins) sub_pl = registered_pl.subplugins[label] next unless sub_pl plugin(sub_pl, , &block) end end @default_options.freeze set_temporary_name("#{superclass}/#{pl}") if respond_to?(:set_temporary_name) # ruby 3.4 only elsif # this can happen when two plugins are loaded, an one of them calls the other under the hood, # albeit changing some default. @default_options = pl.(@default_options) if pl.respond_to?(:extra_options) @default_options = @default_options.merge() if @default_options.freeze end self end |
Instance Method Details
#build_request(verb, uri, params = EMPTY_HASH, options = @options) ⇒ Request
returns a HTTP::Request instance built from the HTTP verb, the request uri, and
the optional set of request-specific options. This request must be sent through
the same session it was built from.
req = session.build_request("GET", "https://server.com")
resp = session.request(req)
114 115 116 117 118 119 120 |
# File 'lib/httpx/session.rb', line 114 def build_request(verb, uri, params = EMPTY_HASH, = @options) rklass = .request_class request = rklass.new(verb, uri, , params) request.persistent = @persistent set_request_callbacks(request) request end |
#build_requests(arg0, arg1, arg2) ⇒ Array[Request] #build_requests(arg0, arg1) ⇒ Array[Request] #build_requests(arg0, arg1, arg2) ⇒ Array[Request]
returns a set of HTTPX::Request objects built from the given args and options.
266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 |
# File 'lib/httpx/session.rb', line 266 def build_requests(*args, params) requests = if args.size == 1 reqs = args.first reqs.map do |verb, uri, ps = EMPTY_HASH| request_params = params request_params = request_params.merge(ps) unless ps.empty? build_request(verb, uri, request_params) end else verb, uris = args if uris.respond_to?(:each) uris.enum_for(:each).map do |uri, ps = EMPTY_HASH| request_params = params request_params = request_params.merge(ps) unless ps.empty? build_request(verb, uri, request_params) end else [build_request(verb, uris, params)] end end raise ArgumentError, "wrong number of URIs (given 0, expect 1..+1)" if requests.empty? requests end |
#close(selector = Selector.new) ⇒ void
This method returns an undefined value.
closes all the active connections from the session.
when called directly without specifying selector, all available connections
will be picked up from the connection pool and closed. Connections in use
by other sessions, or same session in a different thread, will not be reaped.
64 65 66 67 68 69 70 71 72 73 74 75 76 |
# File 'lib/httpx/session.rb', line 64 def close(selector = Selector.new) # throw resolvers away from the pool @pool.reset_resolvers # preparing to throw away connections while (connection = @pool.pop_connection) next if connection.state == :closed select_connection(connection, selector) end selector_close(selector) end |
#coalesce_connections(conn1, conn2, selector, from_pool) ⇒ void
This method returns an undefined value.
coalesces conn2 into conn1. if conn1 was loaded from the connection pool
(it is known via from_pool), then it adds its to the selector.
434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 |
# File 'lib/httpx/session.rb', line 434 def coalesce_connections(conn1, conn2, selector, from_pool) unless conn1.coalescable?(conn2) conn2.log(level: 2) { "not coalescing with conn##{conn1.object_id}[#{conn1.origin}])" } select_connection(conn2, selector) if from_pool conn1.log(level: 2) { "check-in connection##{conn1.object_id}(#{conn1.state}) in pool##{@pool.object_id}" } @pool.checkin_connection(conn1) end return end conn2.log(level: 2) { "coalescing with connection##{conn1.object_id}[#{conn1.origin}])" } select_connection(conn1, selector) if from_pool conn2.coalesce!(conn1) conn2.disconnect end |
#deactivate(selector) ⇒ void
This method returns an undefined value.
tries deactivating connections in the selector, deregistering the ones that have been deactivated.
224 225 226 |
# File 'lib/httpx/session.rb', line 224 def deactivate(selector) selector.each_connection.to_a.each(&:deactivate) end |
#deselect_connection(connection, selector, cloned = false) ⇒ void
This method returns an undefined value.
137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'lib/httpx/session.rb', line 137 def deselect_connection(connection, selector, cloned = false) connection.log(level: 2) do "deregistering connection##{connection.object_id}(#{connection.state}) from selector##{selector.object_id}" end selector.deregister(connection) # do not check-in connections only created for Happy Eyeballs return if cloned return if @closing && connection.state == :closed && !connection.used? connection.log(level: 2) { "check-in connection##{connection.object_id}(#{connection.state}) in pool##{@pool.object_id}" } @pool.checkin_connection(connection) end |
#deselect_resolver(resolver, selector) ⇒ void
This method returns an undefined value.
152 153 154 155 156 157 158 159 160 161 162 |
# File 'lib/httpx/session.rb', line 152 def deselect_resolver(resolver, selector) resolver.log(level: 2) do "deregistering resolver##{resolver.object_id}(#{resolver.state}) from selector##{selector.object_id}" end selector.deregister(resolver) return if @closing && resolver.closed? resolver.log(level: 2) { "check-in resolver##{resolver.object_id}(#{resolver.state}) in pool##{@pool.object_id}" } @pool.checkin_resolver(resolver) end |
#do_init_connection(connection, selector) ⇒ void
This method returns an undefined value.
295 296 297 |
# File 'lib/httpx/session.rb', line 295 def do_init_connection(connection, selector) resolve_connection(connection, selector) unless connection.family end |
#early_resolve(resolver, connection) ⇒ Boolean
395 396 397 |
# File 'lib/httpx/session.rb', line 395 def early_resolve(resolver, connection) resolver.early_resolve(connection) end |
#fetch_response(request, _selector, _options) ⇒ response?
returns the corresponding HTTP::Response to the given request if it has been received.
235 236 237 238 239 240 241 242 243 |
# File 'lib/httpx/session.rb', line 235 def fetch_response(request, _selector, ) response = request.response return unless response && response.finished? request.log(level: 2) { "response##{response.object_id} fetched" } response end |
#find_connection(request_uri, selector, options) ⇒ Connection
returns the HTTPX::Connection through which the request should be sent through.
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 |
# File 'lib/httpx/session.rb', line 180 def find_connection(request_uri, selector, ) if (connection = selector.find_connection(request_uri, )) connection.idling if connection.state == :closed log(level: 2) { "found connection##{connection.object_id}(#{connection.state}) in selector##{selector.object_id}" } return connection end connection = @pool.checkout_connection(request_uri, ) log(level: 2) { "found connection##{connection.object_id}(#{connection.state}) in pool##{@pool.object_id}" } case connection.state when :idle do_init_connection(connection, selector) when :open # external io select_connection(connection, selector) when :closing, :closed connection.idling if connection.addresses? select_connection(connection, selector) else # if addresses expired, resolve again resolve_connection(connection, selector) end else pin(connection, selector) end connection end |
#find_resolver_for(connection, selector) ⇒ resolver
419 420 421 422 423 424 425 426 427 428 429 430 |
# File 'lib/httpx/session.rb', line 419 def find_resolver_for(connection, selector) if (resolver = selector.find_resolver(connection.)) resolver.log(level: 2) { "found resolver##{resolver.object_id}(#{resolver.state}) in selector##{selector.object_id}" } return resolver end resolver = @pool.checkout_resolver(connection.) resolver.log(level: 2) { "found resolver##{resolver.object_id}(#{resolver.state}) in pool##{@pool.object_id}" } pin(resolver, selector) resolver end |
#get_current_selector ⇒ Selector? #get_current_selector ⇒ Selector
451 452 453 |
# File 'lib/httpx/session.rb', line 451 def get_current_selector selector_store[self] || (yield if block_given?) end |
#on_promise(_, stream) ⇒ void
This method returns an undefined value.
callback executed when an HTTP/2 promise frame has been received.
229 230 231 232 |
# File 'lib/httpx/session.rb', line 229 def on_promise(_, stream) log(level: 2) { "#{stream.id}: refusing stream!" } stream.refuse end |
#on_resolver_close ⇒ void
This method returns an undefined value.
73 |
# File 'sig/session.rbs', line 73
def on_resolver_close: (Resolver::Resolver resolver, Selector selector) -> void
|
#on_resolver_connection(connection, selector) ⇒ void
This method returns an undefined value.
399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 |
# File 'lib/httpx/session.rb', line 399 def on_resolver_connection(connection, selector) from_pool = false found_connection = selector.find_mergeable_connection(connection) || begin from_pool = true connection.log(level: 2) do "try finding a mergeable connection in pool##{@pool.object_id}" end @pool.checkout_mergeable_connection(connection) end return select_connection(connection, selector) unless found_connection connection.log(level: 2) do "try coalescing from #{from_pool ? "pool##{@pool.object_id}" : "selector##{selector.object_id}"} " \ "(connection##{found_connection.object_id}[#{found_connection.origin}])" end coalesce_connections(found_connection, connection, selector, from_pool) end |
#pin(conn_or_resolver, selector) ⇒ void
This method returns an undefined value.
130 131 132 133 |
# File 'lib/httpx/session.rb', line 130 def pin(conn_or_resolver, selector) conn_or_resolver.current_session = self conn_or_resolver.current_selector = selector end |
#receive_requests(requests, selector) ⇒ Array[response]
returns the array of HTTPX::Response objects corresponding to the array of HTTPX::Request requests.
316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 |
# File 'lib/httpx/session.rb', line 316 def receive_requests(requests, selector) pending_idxs = [] #: Array[Integer] pending = 0 waiting = false requests.each do |request| send_request(request, selector) end # do work first selector.initial_call responses = requests.each_with_index.map do |request, idx| fetch_response(request, selector, request.).tap do |response| if response.nil? pending += 1 request.on_response_arrived = lambda do pending_idxs << idx if waiting end end end end log(level: 2) { "waiting to receive #{pending} pending requests..." } until pending.zero? || selector.empty? # loop on selector until at least one response has been received. waiting = true catch(:coalesced) { selector.next_tick } waiting = false while (idx = pending_idxs.shift) request = requests[idx] response = fetch_response(request, selector, request.) # stop on first pending response. this avoids traversing pending idxs all the way # (which is more expensive in the beginning, when the array is larger and N) while # making the next loop cheaper (because we're dropping). next unless response request.complete!(response) responses[idx] = response request.on_response_arrived = nil pending -= 1 end end raise Error, "something went wrong, #{pending} responses not found " \ "and requests not resent" unless pending.zero? responses end |
#request(*args, **params) ⇒ Object
performs one, or multple requests; it accepts:
- one or multiple HTTPX::Request objects;
- an HTTP verb, then a sequence of URIs or URI/options tuples;
- one or multiple HTTP verb / uri / (optional) options tuples;
when present, the set of options kwargs is applied to all of the
sent requests.
respectively returns a single HTTPX::Response response, or all of them in an Array, in the same order.
resp1 = session.request(req1) resp1, resp2 = session.request(req1, req2) resp1 = session.request("GET", "https://server.org/a") resp1, resp2 = session.request("GET", ["https://server.org/a", "https://server.org/b"]) resp1, resp2 = session.request(["GET", "https://server.org/a"], ["GET", "https://server.org/b"]) resp1 = session.request("POST", "https://server.org/a", form: { "foo" => "bar" }) resp1, resp2 = session.request(["POST", "https://server.org/a", form: { "foo" => "bar" }], ["GET", "https://server.org/b"]) resp1, resp2 = session.request("GET", ["https://server.org/a", "https://server.org/b"], headers: { "x-api-token" => "TOKEN" })
98 99 100 101 102 103 104 105 106 |
# File 'lib/httpx/session.rb', line 98 def request(*args, **params) raise ArgumentError, "must perform at least one request" if args.empty? requests = args.first.is_a?(Request) ? args : build_requests(*args, params) responses = send_requests(*requests) return responses.first if responses.size == 1 responses end |
#resolve_connection(connection, selector) ⇒ void
This method returns an undefined value.
371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 |
# File 'lib/httpx/session.rb', line 371 def resolve_connection(connection, selector) if connection.addresses? || connection.open? # # there are two cases in which we want to activate initialization of # connection immediately: # # 1. when the connection already has addresses, i.e. it doesn't need to # resolve a name (not the same as name being an IP, yet) # 2. when the connection is initialized with an external already open IO. # on_resolver_connection(connection, selector) return end resolver = find_resolver_for(connection, selector) pin(connection, selector) if early_resolve(resolver, connection) @pool.checkin_resolver(resolver) else resolver.lazy_resolve(connection) end end |
#select_connection(connection, selector) ⇒ void Also known as: select_resolver
This method returns an undefined value.
122 123 124 125 126 127 128 |
# File 'lib/httpx/session.rb', line 122 def select_connection(connection, selector) pin(connection, selector) connection.log(level: 2) do "registering into selector##{selector.object_id}" end selector.register(connection) end |
#selector_close(selector) ⇒ void
This method returns an undefined value.
214 215 216 217 218 219 220 221 |
# File 'lib/httpx/session.rb', line 214 def selector_close(selector) begin @closing = true selector.terminate ensure @closing = false end end |
#selector_store ⇒ Hash[instance, Selector]
463 464 465 466 467 468 469 470 471 |
# File 'lib/httpx/session.rb', line 463 def selector_store th_current = Thread.current thread_selector_store(th_current) || begin {}.compare_by_identity.tap do |store| th_current.thread_variable_set(:httpx_persistent_selector_store, store) end end end |
#send_request(request, selector, options = request.options) ⇒ void
This method returns an undefined value.
sends the request to the corresponding HTTPX::Connection
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 |
# File 'lib/httpx/session.rb', line 246 def send_request(request, selector, = request.) error = begin catch(:resolve_error) do log(level: 2) { "finding connection for request##{request.object_id}..." } connection = find_connection(request.uri, selector, ) connection.send(request) end rescue StandardError => e e end return unless error && error.is_a?(Exception) raise error unless error.is_a?(Error) response = ErrorResponse.new(request, error) request.response = response request.emit_response(response) end |
#send_requests(*requests) ⇒ Array[response]
sends an array of HTTPX::Request requests, returns the respective array of HTTPX::Response objects.
300 301 302 303 304 305 306 307 308 309 310 311 312 313 |
# File 'lib/httpx/session.rb', line 300 def send_requests(*requests) selector = get_current_selector { Selector.new } begin receive_requests(requests, selector) ensure unless @wrapped if @persistent deactivate(selector) else close(selector) end end end end |
#set_current_selector(selector) ⇒ void
This method returns an undefined value.
455 456 457 458 459 460 461 |
# File 'lib/httpx/session.rb', line 455 def set_current_selector(selector) if selector selector_store[self] = selector else selector_store.delete(self) end end |
#set_request_callbacks(request) ⇒ void
This method returns an undefined value.
291 292 293 |
# File 'lib/httpx/session.rb', line 291 def set_request_callbacks(request) request.on(:promise, &method(:on_promise)) end |
#thread_selector_store(th) ⇒ Hash[instance, Selector]?
473 474 475 |
# File 'lib/httpx/session.rb', line 473 def thread_selector_store(th) th.thread_variable_get(:httpx_persistent_selector_store) end |
#try_clone_connection(connection, selector, family) ⇒ Connection
164 165 166 167 168 169 170 171 172 173 174 175 176 177 |
# File 'lib/httpx/session.rb', line 164 def try_clone_connection(connection, selector, family) connection.family ||= family return connection if connection.family == family new_connection = connection.class.new(connection.origin, connection.) new_connection.family = family connection.sibling = new_connection do_init_connection(new_connection, selector) new_connection end |
#wrap {|arg0| ... } ⇒ void
This method returns an undefined value.
Yields itself the block, then closes it after the block is evaluated.
session.wrap do |http|
http.get("https://wikipedia.com")
end # wikipedia connection closes here
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
# File 'lib/httpx/session.rb', line 31 def wrap prev_wrapped = @wrapped @wrapped = true was_initialized = false current_selector = get_current_selector do selector = Selector.new set_current_selector(selector) was_initialized = true selector end begin yield self ensure unless prev_wrapped if @persistent deactivate(current_selector) else close(current_selector) end end @wrapped = prev_wrapped set_current_selector(nil) if was_initialized end end |