rack-timeout-0.5.1/0000755000004100000410000000000013376045222014155 5ustar www-datawww-datarack-timeout-0.5.1/test/0000755000004100000410000000000013376045222015134 5ustar www-datawww-datarack-timeout-0.5.1/test/basic_test.rb0000644000004100000410000000104413376045222017600 0ustar www-datawww-datarequire "test_helper" class BasicTest < RackTimeoutTest def test_ok self.settings = { service_timeout: 1 } get "/" assert last_response.ok? end def test_timeout self.settings = { service_timeout: 1 } assert_raises(Rack::Timeout::RequestTimeoutError) do get "/sleep" end end def test_wait_timeout self.settings = { service_timeout: 1, wait_timeout: 15 } assert_raises(Rack::Timeout::RequestExpiryError) do get "/", "", 'HTTP_X_REQUEST_START' => time_in_msec(Time.now - 100) end end end rack-timeout-0.5.1/test/env_settings_test.rb0000644000004100000410000000067413376045222021237 0ustar www-datawww-datarequire 'test_helper' class EnvSettingsTest < RackTimeoutTest def test_service_timeout with_env(RACK_TIMEOUT_SERVICE_TIMEOUT: 1) do assert_raises(Rack::Timeout::RequestTimeoutError) do get "/sleep" end end end def test_zero_wait_timeout with_env(RACK_TIMEOUT_WAIT_TIMEOUT: 0) do get "/", "", 'HTTP_X_REQUEST_START' => time_in_msec(Time.now - 100) assert last_response.ok? end end end rack-timeout-0.5.1/test/test_helper.rb0000644000004100000410000000163213376045222020001 0ustar www-datawww-datarequire "test/unit" require "rack/test" require "rack-timeout" class RackTimeoutTest < Test::Unit::TestCase include Rack::Test::Methods attr_accessor :settings def initialize(*args) self.settings ||= {} super(*args) end def app settings = self.settings Rack::Builder.new do use Rack::Timeout, settings map "/" do run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['OK']] } end map "/sleep" do run lambda { |env| sleep } end end end # runs the test with the given environment, but doesnt restore the original # environment afterwards. This should be sufficient for rack-timeout testing. def with_env(hash) hash.each_pair do |k, v| ENV[k.to_s] = v.to_s end yield hash.each_key do |k| ENV[k.to_s] = nil end end def time_in_msec(t = Time.now) "#{t.tv_sec}#{t.tv_usec/1000}" end end rack-timeout-0.5.1/Rakefile0000644000004100000410000000047413376045222015627 0ustar www-datawww-datarequire 'rake/testtask' require 'bundler/gem_tasks' Rake::TestTask.new do |t| t.libs << "test" t.test_files = FileList['test/**/*_test.rb'] t.verbose = true end task :fix_permissions do FileUtils.chmod_R("a+rX", File.dirname(__FILE__)) end task(:build).enhance([:fix_permissions]) task :default => :test rack-timeout-0.5.1/lib/0000755000004100000410000000000013376045222014723 5ustar www-datawww-datarack-timeout-0.5.1/lib/rack/0000755000004100000410000000000013376045222015643 5ustar www-datawww-datarack-timeout-0.5.1/lib/rack/timeout/0000755000004100000410000000000013376045222017331 5ustar www-datawww-datarack-timeout-0.5.1/lib/rack/timeout/logging-observer.rb0000644000004100000410000000317213376045222023134 0ustar www-datawww-datarequire "logger" require_relative "core" class Rack::Timeout::StateChangeLoggingObserver STATE_LOG_LEVEL = { :expired => :error, :ready => :info, :active => :debug, :timed_out => :error, :completed => :info, } def initialize @logger = nil end # returns the Proc to be used as the observer callback block def callback method(:log_state_change) end SIMPLE_FORMATTER = ->(severity, timestamp, progname, msg) { "#{msg} at=#{severity.downcase}\n" } def self.mk_logger(device, level = ::Logger::INFO) ::Logger.new(device).tap do |logger| logger.level = level logger.formatter = SIMPLE_FORMATTER end end attr_writer :logger private def logger(env = nil) @logger || (defined?(::Rails) && ::Rails.logger) || (env && !env["rack.logger"].is_a?(::Rack::NullLogger) && env["rack.logger"]) || (env && env["rack.errors"] && self.class.mk_logger(env["rack.errors"])) || (@fallback_logger ||= self.class.mk_logger($stderr)) end # generates the actual log string def log_state_change(env) info = env[::Rack::Timeout::ENV_INFO_KEY] level = STATE_LOG_LEVEL[info.state] logger(env).send(level) do s = "source=rack-timeout" s << " id=" << info.id if info.id s << " wait=" << info.ms(:wait) if info.wait s << " timeout=" << info.ms(:timeout) if info.timeout s << " service=" << info.ms(:service) if info.service s << " state=" << info.state.to_s if info.state s end end end rack-timeout-0.5.1/lib/rack/timeout/base.rb0000644000004100000410000000011613376045222020566 0ustar www-datawww-datarequire_relative "core" require_relative "logger" Rack::Timeout::Logger.init rack-timeout-0.5.1/lib/rack/timeout/rails.rb0000644000004100000410000000056313376045222020774 0ustar www-datawww-datarequire_relative "base" class Rack::Timeout::Railtie < Rails::Railtie initializer("rack-timeout.prepend") do |app| next if Rails.env.test? if defined?(ActionDispatch::RequestId) app.config.middleware.insert_after(ActionDispatch::RequestId, Rack::Timeout) else app.config.middleware.insert_before(Rack::Runtime, Rack::Timeout) end end endrack-timeout-0.5.1/lib/rack/timeout/rollbar.rb0000644000004100000410000000020613376045222021311 0ustar www-datawww-datawarn 'DEPRECATION WARNING: The Rollbar module was removed from rack-timeout. For more details check the README on heroku/rack-timeout'rack-timeout-0.5.1/lib/rack/timeout/core.rb0000644000004100000410000003103313376045222020606 0ustar www-datawww-data# encoding: utf-8 require "securerandom" require_relative "support/monotonic_time" require_relative "support/scheduler" require_relative "support/timeout" module Rack class Timeout include Rack::Timeout::MonotonicTime # gets us the #fsecs method module ExceptionWithEnv # shared by the following exceptions, allows them to receive the current env attr :env def initialize(env) @env = env end end class Error < RuntimeError include ExceptionWithEnv end class RequestExpiryError < Error; end # raised when a request is dropped without being given a chance to run (because too old) class RequestTimeoutError < Error; end # raised when a request has run for too long class RequestTimeoutException < Exception # This is first raised to help prevent an application from inadvertently catching the above. It's then caught by rack-timeout and replaced with RequestTimeoutError to bubble up to wrapping middlewares and the web server include ExceptionWithEnv end RequestDetails = Struct.new( :id, # a unique identifier for the request. informative-only. :wait, # seconds the request spent in the web server before being serviced by rack :service, # time rack spent processing the request (updated ~ every second) :timeout, # the actual computed timeout to be used for this request :state, # the request's current state, see VALID_STATES below ) { def ms(k) # helper method used for formatting values in milliseconds "%.fms" % (self[k] * 1000) if self[k] end } VALID_STATES = [ :expired, # The request was too old by the time it reached rack (see wait_timeout, wait_overtime) :ready, # We're about to start processing this request :active, # This request is currently being handled :timed_out, # This request has run for too long and we're raising a timeout error in it :completed, # We're done with this request (also set after having timed out a request) ] ENV_INFO_KEY = "rack-timeout.info".freeze # key under which each request's RequestDetails instance is stored in its env. HTTP_X_REQUEST_ID = "HTTP_X_REQUEST_ID".freeze # key where request id is stored if generated by upstream client/proxy ACTION_DISPATCH_REQUEST_ID = "action_dispatch.request_id".freeze # key where request id is stored if generated by action dispatch # helper methods to read timeout properties. Ensure they're always positive numbers or false. When set to false (or 0), their behaviour is disabled. def read_timeout_property value, default case value when nil ; read_timeout_property default, default when false ; false when 0 ; false else value.is_a?(Numeric) && value > 0 or raise ArgumentError, "value #{value.inspect} should be false, zero, or a positive number." value end end attr_reader \ :service_timeout, # How long the application can take to complete handling the request once it's passed down to it. :wait_timeout, # How long the request is allowed to have waited before reaching rack. If exceeded, the request is 'expired', i.e. dropped entirely without being passed down to the application. :wait_overtime, # Additional time over @wait_timeout for requests with a body, like POST requests. These may take longer to be received by the server before being passed down to the application, but should not be expired. :service_past_wait # when false, reduces the request's computed timeout from the service_timeout value if the complete request lifetime (wait + service) would have been longer than wait_timeout (+ wait_overtime when applicable). When true, always uses the service_timeout value. we default to false under the assumption that the router would drop a request that's not responded within wait_timeout, thus being there no point in servicing beyond seconds_service_left (see code further down) up until service_timeout. def initialize(app, service_timeout:nil, wait_timeout:nil, wait_overtime:nil, service_past_wait:"not_specified") @service_timeout = read_timeout_property service_timeout, ENV.fetch("RACK_TIMEOUT_SERVICE_TIMEOUT", 15).to_i @wait_timeout = read_timeout_property wait_timeout, ENV.fetch("RACK_TIMEOUT_WAIT_TIMEOUT", 30).to_i @wait_overtime = read_timeout_property wait_overtime, ENV.fetch("RACK_TIMEOUT_WAIT_OVERTIME", 60).to_i @service_past_wait = service_past_wait == "not_specified" ? ENV.fetch("RACK_TIMEOUT_SERVICE_PAST_WAIT", false).to_s != "false" : service_past_wait @app = app end RT = self # shorthand reference def call(env) info = (env[ENV_INFO_KEY] ||= RequestDetails.new) info.id ||= env[HTTP_X_REQUEST_ID] || env[ACTION_DISPATCH_REQUEST_ID] || SecureRandom.uuid time_started_service = Time.now # The wall time the request started being processed by rack ts_started_service = fsecs # The monotonic time the request started being processed by rack time_started_wait = RT._read_x_request_start(env) # The time the request was initially received by the web server (if available) effective_overtime = (wait_overtime && RT._request_has_body?(env)) ? wait_overtime : 0 # additional wait timeout (if set and applicable) seconds_service_left = nil # if X-Request-Start is present and wait_timeout is set, expire requests older than wait_timeout (+wait_overtime when applicable) if time_started_wait && wait_timeout seconds_waited = time_started_service - time_started_wait # how long it took between the web server first receiving the request and rack being able to handle it seconds_waited = 0 if seconds_waited < 0 # make up for potential time drift between the routing server and the application server final_wait_timeout = wait_timeout + effective_overtime # how long the request will be allowed to have waited seconds_service_left = final_wait_timeout - seconds_waited # first calculation of service timeout (relevant if request doesn't get expired, may be overriden later) info.wait, info.timeout = seconds_waited, final_wait_timeout # updating the info properties; info.timeout will be the wait timeout at this point if seconds_service_left <= 0 # expire requests that have waited for too long in the queue (as they are assumed to have been dropped by the web server / routing layer at this point) RT._set_state! env, :expired raise RequestExpiryError.new(env), "Request older than #{info.ms(:timeout)}." end end # pass request through if service_timeout is false (i.e., don't time it out at all.) return @app.call(env) unless service_timeout # compute actual timeout to be used for this request; if service_past_wait is true, this is just service_timeout. If false (the default), and wait time was determined, we'll use the shortest value between seconds_service_left and service_timeout. See comment above at service_past_wait for justification. info.timeout = service_timeout # nice and simple, when service_past_wait is true, not so much otherwise: info.timeout = seconds_service_left if !service_past_wait && seconds_service_left && seconds_service_left > 0 && seconds_service_left < service_timeout RT._set_state! env, :ready # we're good to go, but have done nothing yet heartbeat_event = nil # init var so it's in scope for following proc register_state_change = ->(status = :active) { # updates service time and state; will run every second heartbeat_event.cancel! if status != :active # if the request is no longer active we should stop updating every second info.service = fsecs - ts_started_service # update service time RT._set_state! env, status # update status } heartbeat_event = RT::Scheduler.run_every(1) { register_state_change.call :active } # start updating every second while active; if log level is debug, this will log every sec timeout = RT::Scheduler::Timeout.new do |app_thread| # creates a timeout instance responsible for timing out the request. the given block runs if timed out register_state_change.call :timed_out app_thread.raise(RequestTimeoutException.new(env), "Request #{"waited #{info.ms(:wait)}, then " if info.wait}ran for longer than #{info.ms(:timeout)}") end response = timeout.timeout(info.timeout) do # perform request with timeout begin @app.call(env) # boom, send request down the middleware chain rescue RequestTimeoutException => e # will actually hardly ever get to this point because frameworks tend to catch this. see README for more raise RequestTimeoutError.new(env), e.message, e.backtrace # but in case it does get here, re-raise RequestTimeoutException as RequestTimeoutError ensure register_state_change.call :completed end end response end ### following methods are used internally (called by instances, so can't be private. _ marker should discourage people from calling them) # X-Request-Start contains the time the request was first seen by the server. Format varies wildly amongst servers, yay! # - nginx gives the time since epoch as seconds.milliseconds[1]. New Relic documentation recommends preceding it with t=[2], so might as well detect it. # - Heroku gives the time since epoch in milliseconds. [3] # - Apache uses t=microseconds[4], so we're not even going there. # # The sane way to handle this would be by knowing the server being used, instead let's just hack around with regular expressions and ignore apache entirely. # [1]: http://nginx.org/en/docs/http/ngx_http_log_module.html#var_msec # [2]: https://docs.newrelic.com/docs/apm/other-features/request-queueing/request-queue-server-configuration-examples#nginx # [3]: https://devcenter.heroku.com/articles/http-routing#heroku-headers # [4]: http://httpd.apache.org/docs/current/mod/mod_headers.html#header # # This is a code extraction for readability, this method is only called from a single point. RX_NGINX_X_REQUEST_START = /^(?:t=)?(\d+)\.(\d{3})$/ RX_HEROKU_X_REQUEST_START = /^(\d+)$/ HTTP_X_REQUEST_START = "HTTP_X_REQUEST_START".freeze def self._read_x_request_start(env) return unless s = env[HTTP_X_REQUEST_START] return unless m = s.match(RX_HEROKU_X_REQUEST_START) || s.match(RX_NGINX_X_REQUEST_START) Time.at(m[1,2].join.to_f / 1000) end # This method determines if a body is present. requests with a body (generally POST, PUT) can have a lengthy body which may have taken a while to be received by the web server, inflating their computed wait time. This in turn could lead to unwanted expirations. See wait_overtime property as a way to overcome those. # This is a code extraction for readability, this method is only called from a single point. def self._request_has_body?(env) return true if env["HTTP_TRANSFER_ENCODING"] == "chunked" return false if env["CONTENT_LENGTH"].nil? return false if env["CONTENT_LENGTH"].to_i.zero? true end def self._set_state!(env, state) raise "Invalid state: #{state.inspect}" unless VALID_STATES.include? state env[ENV_INFO_KEY].state = state notify_state_change_observers(env) end ### state change notification-related methods @state_change_observers = {} # Registers a block to be called back when a request changes state in rack-timeout. The block will receive the request's env. # # `id` is anything that uniquely identifies this particular callback, mostly so it may be removed via `unregister_state_change_observer`. def self.register_state_change_observer(id, &callback) raise RuntimeError, "An observer with the id #{id.inspect} is already set." if @state_change_observers.key? id raise ArgumentError, "A callback block is required." unless callback @state_change_observers[id] = callback end # Removes the observer with the given id def self.unregister_state_change_observer(id) @state_change_observers.delete(id) end private # Sends out the notifications. Called internally at the end of `_set_state!` def self.notify_state_change_observers(env) @state_change_observers.values.each { |observer| observer.call(env) } end end end rack-timeout-0.5.1/lib/rack/timeout/logger.rb0000644000004100000410000000163013376045222021135 0ustar www-datawww-datarequire "logger" require_relative "core" require_relative "logging-observer" module Rack::Timeout::Logger extend self attr :device, :level, :logger def device=(new_device) update(new_device, level) end def level=(new_level) update(device, new_level) end def logger=(new_logger) @logger = @observer.logger = new_logger end def init @observer = ::Rack::Timeout::StateChangeLoggingObserver.new ::Rack::Timeout.register_state_change_observer(:logger, &@observer.callback) @inited = true end def disable @observer, @logger, @level, @device, @inited = nil ::Rack::Timeout.unregister_state_change_observer(:logger) end def update(new_device, new_level) init unless @inited @device = new_device || $stderr @level = new_level || ::Logger::INFO self.logger = ::Rack::Timeout::StateChangeLoggingObserver.mk_logger(device, level) end end rack-timeout-0.5.1/lib/rack/timeout/support/0000755000004100000410000000000013376045222021045 5ustar www-datawww-datarack-timeout-0.5.1/lib/rack/timeout/support/monotonic_time.rb0000644000004100000410000000136413376045222024421 0ustar www-datawww-datarequire_relative "namespace" # lifted from https://github.com/ruby-concurrency/concurrent-ruby/blob/master/lib/concurrent/utility/monotonic_time.rb module Rack::Timeout::MonotonicTime extend self def fsecs_mono Process.clock_gettime Process::CLOCK_MONOTONIC end def fsecs_java java.lang.System.nanoTime() / 1_000_000_000.0 end mutex = Mutex.new last_time = Time.now.to_f define_method(:fsecs_ruby) do now = Time.now.to_f mutex.synchronize { last_time = last_time < now ? now : last_time + 1e-6 } end case when defined? Process::CLOCK_MONOTONIC ; alias fsecs fsecs_mono when RUBY_PLATFORM == "java" ; alias fsecs fsecs_java else ; alias fsecs fsecs_ruby end end rack-timeout-0.5.1/lib/rack/timeout/support/scheduler.rb0000755000004100000410000001374113376045222023361 0ustar www-datawww-data#!/usr/bin/env ruby require_relative "namespace" require_relative "monotonic_time" # Runs code at a later time # # Basic usage: # # Scheduler.run_in(5) { do_stuff } # <- calls do_stuff 5 seconds from now # # Scheduled events run in sequence in a separate thread, the main thread continues on. # That means you may need to #join the scheduler if the main thread is only waiting on scheduled events to run. # # Scheduler.join # # Basic usage is through a singleton instance, its methods are available as class methods, as shown above. # One could also instantiate separate instances which would get you separate run threads, but generally there's no point in it. class Rack::Timeout::Scheduler MAX_IDLE_SECS = 30 # how long the runner thread is allowed to live doing nothing include Rack::Timeout::MonotonicTime # gets us the #fsecs method # stores a proc to run later, and the time it should run at class RunEvent < Struct.new(:monotime, :proc) def initialize(*args) @cancelled = false super(*args) end def cancel! @cancelled = true end def cancelled? !!@cancelled end def run! return if @cancelled proc.call(self) end end class RepeatEvent < RunEvent def initialize(monotime, proc, every) @start = monotime @every = every @iter = 0 super(monotime, proc) end def run! super ensure self.monotime = @start + @every * (@iter += 1) until monotime >= Rack::Timeout::MonotonicTime.fsecs end end def initialize @runner = nil @events = [] # array of `RunEvent`s @mx_events = Mutex.new # mutex to change said array @mx_runner = Mutex.new # mutex for creating a runner thread end private # returns the runner thread, creating it if needed def runner @mx_runner.synchronize { return @runner unless @runner.nil? || !@runner.alive? @joined = false @runner = Thread.new { run_loop! } } end # the actual runner thread loop def run_loop! Thread.current.abort_on_exception = true # always be aborting sleep_for, run, last_run = nil, nil, fsecs # sleep_for: how long to sleep before next run; last_run: time of last run; run: just initializing it outside of the synchronize scope, will contain events to run now loop do # begin event reader loop @mx_events.synchronize { # @events.reject!(&:cancelled?) # get rid of cancelled events if @events.empty? # if there are no further events … return if @joined # exit the run loop if this runner thread has been joined (the thread will die and the join will return) return if fsecs - last_run > MAX_IDLE_SECS # exit the run loop if done nothing for the past MAX_IDLE_SECS seconds sleep_for = MAX_IDLE_SECS # sleep for MAX_IDLE_SECS (mind it that we get awaken when new events are scheduled) else # sleep_for = [@events.map(&:monotime).min - fsecs, 0].max # if we have events, set to sleep until it's time for the next one to run. (the max bit ensure we don't have negative sleep times) end # @mx_events.sleep sleep_for # do sleep # now = fsecs # run, defer = @events.partition { |ev| ev.monotime <= now } # separate events to run now and events to run later defer += run.select { |ev| ev.is_a? RepeatEvent } # repeat events both run and are deferred @events.replace(defer) # keep only events to run later } # # next if run.empty? # done here if there's nothing to run now run.sort_by(&:monotime).each { |ev| ev.run! } # run the events scheduled to run now last_run = fsecs # store that we did run things at this time, go immediately on to the next loop iteration as it may be time to run more things end end public # waits on the runner thread to finish def join @joined = true runner.join end # adds a RunEvent struct to the run schedule def schedule(event) @mx_events.synchronize { @events << event } runner.run # wakes up the runner thread so it can recalculate sleep length taking this new event into consideration return event end # reschedules an event by the given number of seconds. can be negative to run sooner. # returns nil and does nothing if the event is not already in the queue (might've run already), otherwise updates the event time in-place; returns the updated event. def delay(event, secs) @mx_events.synchronize { return unless @events.include? event event.monotime += secs runner.run return event } end # schedules a block to run in the given number of seconds; returns the created event object def run_in(secs, &block) schedule RunEvent.new(fsecs + secs, block) end # schedules a block to run every x seconds; returns the created event object def run_every(seconds, &block) schedule RepeatEvent.new(fsecs, block, seconds) end ### Singleton access # accessor to the singleton instance def self.singleton @singleton ||= new end # define public instance methods as class methods that delegate to the singleton instance instance_methods(false).each do |m| define_singleton_method(m) { |*a, &b| singleton.send(m, *a, &b) } end end rack-timeout-0.5.1/lib/rack/timeout/support/namespace.rb0000644000004100000410000000020513376045222023323 0ustar www-datawww-data# can be required by other files to prevent them from having to open and nest Rack and Timeout module Rack class Timeout end end rack-timeout-0.5.1/lib/rack/timeout/support/timeout.rb0000644000004100000410000000307013376045222023060 0ustar www-datawww-datarequire_relative "namespace" require_relative "scheduler" class Rack::Timeout::Scheduler::Timeout class Error < RuntimeError; end ON_TIMEOUT = ->thr { thr.raise Error, "execution expired" } # default action to take when a timeout happens # initializes a timeout object with an optional block to handle the timeout differently. the block is passed the thread that's gone overtime. def initialize(&on_timeout) @on_timeout = on_timeout || ON_TIMEOUT @scheduler = Rack::Timeout::Scheduler.singleton end # takes number of seconds to wait before timing out, and code block subject to time out def timeout(secs, &block) return block.call if secs.nil? || secs.zero? # skip timeout flow entirely for zero or nil thr = Thread.current # reference to current thread to be used in timeout thread job = @scheduler.run_in(secs) { @on_timeout.call thr } # schedule this thread to be timed out; should get cancelled if block completes on time return block.call # do what you gotta do ensure # job.cancel! if job # cancel the scheduled timeout job; if the block completed on time, this end # will get called before the timeout code's had a chance to run. # timeout method on singleton instance for when a custom on_timeout is not required def self.timeout(secs, &block) (@singleton ||= new).timeout(secs, &block) end end rack-timeout-0.5.1/lib/rack-timeout.rb0000644000004100000410000000021113376045222017646 0ustar www-datawww-datarequire_relative "rack/timeout/base" require_relative "rack/timeout/rails" if defined?(Rails) && [3,4,5].include?(Rails::VERSION::MAJOR) rack-timeout-0.5.1/doc/0000755000004100000410000000000013376045222014722 5ustar www-datawww-datarack-timeout-0.5.1/doc/rollbar.md0000644000004100000410000000203013376045222016674 0ustar www-datawww-data### Rollbar Because rack-timeout may raise at any point in the codepath of a timed-out request, the stack traces for similar requests may differ, causing rollbar to create separate entries for each timeout. The recommended practice is to configure [Custom Fingerprints][rollbar-customfingerprint] on Rollbar. [rollbar-customfingerprint]: https://docs.rollbar.com/docs/custom-grouping/ Example: ```json [ { "condition": { "eq": "Rack::Timeout::RequestTimeoutException", "path": "body.trace.exception.class" }, "fingerprint": "Rack::Timeout::RequestTimeoutException {{context}}", "title": "Rack::Timeout::RequestTimeoutException {{context}}" } ] ``` This configuration will generate exceptions following the pattern: `Rack::Timeout::RequestTimeoutException controller#action ` On previous versions this configuration was made using `Rack::Timeout::Rollbar` which was removed. [More details on the Issue #122][rollbar-removal-issue]. [rollbar-removal-issue]: https://github.com/heroku/rack-timeout/issues/122 rack-timeout-0.5.1/doc/risks.md0000644000004100000410000000620713376045222016404 0ustar www-datawww-dataRisks and shortcomings of using Rack::Timeout --------------------------------------------- ### Timing Out During IO Blocks Sometimes a request is taking too long to complete because it's blocked waiting on synchronous IO. Such IO does not need to be file operations, it could be, say, network or database operations. If said IO is happening in a C library that's unaware of ruby's interrupt system (i.e. anything written without ruby in mind), calling `Thread#raise` (that's what rack-timeout uses) will not have effect until after the IO block is gone. At the moment rack-timeout does not try to address this issue. As a fail-safe against these cases, a blunter solution that kills the entire process is recommended, such as unicorn's timeouts. More detailed explanations of the issues surrounding timing out in ruby during IO blocks can be found at: - http://redgetan.cc/understanding-timeouts-in-cruby/ ### Timing Out is Inherently Unsafe Raising mid-flight in stateful applications is inherently unsafe. A request can be aborted at any moment in the code flow, and the application can be left in an inconsistent state. There's little way rack-timeout could be aware of ongoing state changes. Applications that rely on a set of globals (like class variables) or any other state that lives beyond a single request may find those left in an unexpected/inconsistent state after an aborted request. Some cleanup code might not have run, or only half of a set of related changes may have been applied. A lot more can go wrong. An intricate explanation of the issue by JRuby's Charles Nutter can be found [here][broken-timeout]. Ruby 2.1 provides a way to defer the result of raising exceptions through the [Thread.handle_interrupt][handle-interrupt] method. This could be used in critical areas of your application code to prevent Rack::Timeout from accidentally wreaking havoc by raising just in the wrong moment. That said, `handle_interrupt` and threads in general are hard to reason about, and detecting all cases where it would be needed in an application is a tall order, and the added code complexity is probably not worth the trouble. Your time is better spent ensuring requests run fast and don't need to timeout. That said, it's something to be aware of, and may explain some eerie wonkiness seen in logs. [broken-timeout]: http://headius.blogspot.de/2008/02/rubys-threadraise-threadkill-timeoutrb.html [handle-interrupt]: http://www.ruby-doc.org/core-2.1.3/Thread.html#method-c-handle_interrupt ### Time Out Early and Often Because of the aforementioned issues, it's recommended you set library-specific timeouts and leave Rack::Timeout as a last resort measure. Library timeouts will generally take care of IO issues and abort the operation safely. See [The Ultimate Guide to Ruby Timeouts][ruby-timeouts]. You'll want to set all relevant timeouts to something lower than Rack::Timeout's `service_timeout`. Generally you want them to be at least 1s lower, so as to account for time spent elsewhere during the request's lifetime while still giving libraries a chance to time out before Rack::Timeout. [ruby-timeouts]: https://github.com/ankane/the-ultimate-guide-to-ruby-timeouts rack-timeout-0.5.1/doc/exceptions.md0000644000004100000410000000453513376045222017434 0ustar www-datawww-dataExceptions ---------- Rack::Timeout can raise three types of exceptions. They are: Two descend from `Rack::Timeout::Error`, which itself descends from `RuntimeError` and is generally caught by an unqualified `rescue`. The third, `RequestTimeoutException`, is more complicated and the most important. * `Rack::Timeout::RequestTimeoutException`: this is raised when a request has run for longer than the specified timeout. This descends from `Exception`, not from `Rack::Timeout::Error` (it has to be rescued from explicitly). It's raised by the rack-timeout timer thread in the application thread, at the point in the stack the app happens to be in when the timeout is triggered. This exception could be caught explicitly within the application, but in doing so you're working past the timeout. This is ok for quick cleanup work but shouldn't be abused as Rack::Timeout will not kick in twice for the same request. Rails will generally intercept `Exception`s, but in plain Rack apps, this exception will be caught by rack-timeout and re-raised as a `Rack::Timeout::RequestTimeoutError`. This is to prevent an `Exception` from bubbling up beyond rack-timeout and to the server. * `Rack::Timeout::RequestTimeoutError` descends from `Rack::Timeout::Error`, but it's only really seen in the case described above. It'll not be seen in a standard Rails app, and will only be seen in Sinatra if rescuing from exceptions is disabled. * `Rack::Timeout::RequestExpiryError`: this is raised when a request is skipped for being too old (see Wait Timeout section). This error cannot generally be rescued from inside a Rails controller action as it happens before the request has a chance to enter Rails. This shouldn't be different for other frameworks, unless you have something above Rack::Timeout in the middleware stack, which you generally shouldn't. You shouldn't rescue from these errors for reporting purposes. Instead, you can subscribe for state change notifications with observers. If you're trying to test that a `Rack::Timeout::RequestTimeoutException` is raised in an action in your Rails application, you **must do so in integration tests**. Please note that Rack::Timeout will not kick in for functional tests as they bypass the rack middleware stack. [More details about testing middleware with Rails here][pablobm]. [pablobm]: http://stackoverflow.com/a/8681208/13989 rack-timeout-0.5.1/doc/observers.md0000644000004100000410000000200713376045222017255 0ustar www-datawww-dataObservers --------- Observers are blocks that are notified about state changes during a request's lifetime. Keep in mind that the `active` state is set every ~1s, so you'll be notified every time. You can register an observer with: ```ruby Rack::Timeout.register_state_change_observer(:a_unique_name) { |env| do_things env } ``` There's currently no way to subscribe to changes into or out of a particular state. To check the actual state we're moving into, read `env['rack-timeout.info'].state`. Handling going out of a state would require some additional logic in the observer. You can remove an observer with `unregister_state_change_observer`: ```ruby Rack::Timeout.unregister_state_change_observer(:a_unique_name) ``` rack-timeout's logging is implemented using an observer; see `Rack::Timeout::StateChangeLoggingObserver` in logging-observer.rb for the implementation. Custom observers might be used to do cleanup, store statistics on request length, timeouts, etc., and potentially do performance tuning on the fly. rack-timeout-0.5.1/doc/settings.md0000644000004100000410000001226313376045222017110 0ustar www-datawww-data# Settings Rack::Timeout has 4 settings, each of which impacts when Rack::Timeout will raise an exception, and which type of exception will be raised. ### Service Timeout `service_timeout` is the most important setting. *Service time* is the time taken from when a request first enters rack to when its response is sent back. When the application takes longer than `service_timeout` to process a request, the request's status is logged as `timed_out` and `Rack::Timeout::RequestTimeoutException` or `Rack::Timeout::RequestTimeoutError` is raised on the application thread. This may be automatically caught by the framework or plugins, so beware. Also, the exception is not guaranteed to be raised in a timely fashion, see section below about IO blocks. Service timeout can be disabled entirely by setting the property to `0` or `false`, at which point the request skips Rack::Timeout's machinery (so no logging will be present). ### Wait Timeout Before a request reaches the rack application, it may have spent some time being received by the web server, or waiting in the application server's queue before being dispatched to rack. The time between when a request is received by the web server and when rack starts handling it is called the *wait time*. On Heroku, a request will be dropped when the routing layer sees no data being transferred for over 30 seconds. (You can read more about the specifics of Heroku routing's timeout [here][heroku-routing] and [here][heroku-timeout].) In this case, it makes no sense to process a request that reaches the application after having waited more than 30 seconds. That's where the `wait_timeout` setting comes in. When a request has a wait time greater than `wait_timeout`, it'll be dropped without ever being sent down to the application, and a `Rack::Timeout::RequestExpiryError` is raised. Such requests are logged as `expired`. [heroku-routing]: https://devcenter.heroku.com/articles/http-routing#timeouts [heroku-timeout]: https://devcenter.heroku.com/articles/request-timeout `wait_timeout` is set at a default of 30 seconds, matching Heroku's router's timeout. Wait timeout can be disabled entirely by setting the property to `0` or `false`. A request's computed wait time may affect the service timeout used for it. Basically, a request's wait time plus service time may not exceed the wait timeout. The reasoning for that is based on Heroku router's behavior, that the request would be dropped anyway after the wait timeout. So, for example, with the default settings of `service_timeout=15`, `wait_timeout=30`, a request that had 20 seconds of wait time will not have a service timeout of 15, but instead of 10, as there are only 10 seconds left before `wait_timeout` is reached. This behavior can be disabled by setting `service_past_wait` to `true`. When set, the `service_timeout` setting will always be honored. Please note that if you're using the `RACK_TIMEOUT_SERVICE_PAST_WAIT` environment variable, any value different than `"false"` will be considered `true`. The way we're able to infer a request's start time, and from that its wait time, is through the availability of the `X-Request-Start` HTTP header, which is expected to contain the time since epoch in milliseconds. (A concession is made for nginx's sec.msec notation.) If the `X-Request-Start` header is not present `wait_timeout` handling is skipped entirely. ### Wait Overtime Relying on `X-Request-Start` is less than ideal, as it computes the time since the request *started* being received by the web server, rather than the time the request *finished* being received by the web server. That poses a problem for lengthy requests. Lengthy requests are requests with a body, such as POST requests. These take time to complete being received by the application server, especially when the client has a slow upload speed, as is common for example with mobile clients or asymmetric connections. While we can infer the time since a request started being received, we can't tell when it completed being received, which would be preferable. We're also unable to tell the time since the last byte was sent in the request, which would be relevant in tracking Heroku's router timeout appropriately. A request that took longer than 30s to be fully received, but that had been uploading data all that while, would be dropped immediately by Rack::Timeout because it'd be considered too old. Heroku's router, however, would not have dropped this request because data was being transmitted all along. As a concession to these shortcomings, for requests that have a body present, we allow some additional wait time on top of `wait_timeout`. This aims to make up for time lost to long uploads. This extra time is called *wait overtime* and can be set via `wait_overtime`. It defaults to 60 seconds. This can be disabled as usual by setting the property to `0` or `false`. When disabled, there's no overtime. If you want lengthy requests to never get expired, set `wait_overtime` to a very high number. Keep in mind that Heroku [recommends][uploads] uploading large files directly to S3, so as to prevent the dyno from being blocked for too long and hence unable to handle further incoming requests. [uploads]: https://devcenter.heroku.com/articles/s3#file-uploads rack-timeout-0.5.1/doc/request-lifecycle.md0000644000004100000410000000514113376045222020672 0ustar www-datawww-dataRequest Lifetime ---------------- Throughout a request's lifetime, Rack::Timeout keeps details about the request in `env[Rack::Timeout::ENV_INFO_KEY]`, or, more explicitly, `env["rack-timeout.info"]`. The value of that entry is an instance of `Rack::Timeout::RequestDetails`, which is a `Struct` consisting of the following fields: * `id`: a unique ID per request. Either the value of the `X-Request-ID` header or a random ID generated internally. * `wait`: time in seconds since `X-Request-Start` at the time the request was initially seen by Rack::Timeout. Only set if `X-Request-Start` is present. * `timeout`: the final timeout value that was used or to be used, in seconds. For `expired` requests, that would be the `wait_timeout`, possibly with `wait_overtime` applied. In all other cases it's the `service_timeout`, potentially reduced to make up for time lost waiting. (See discussion regarding `service_past_wait` above, under the Wait Timeout section.) * `service`: set after a request completes (or times out). The time in seconds it took being processed. This is also updated while a request is still active, around every second, with the time taken so far. * `state`: the possible states, and their log level, are: * `expired` (`ERROR`): the request is considered too old and is skipped entirely. This happens when `X-Request-Start` is present and older than `wait_timeout`. When in this state, `Rack::Timeout::RequestExpiryError` is raised. See earlier discussion about the `wait_overtime` setting, too. * `ready` (`INFO`): this is the state a request is in right before it's passed down the middleware chain. Once it's being processed, it'll move on to `active`, and then on to `timed_out` and/or `completed`. * `active` (`DEBUG`): the request is being actively processed in the application thread. This is signaled repeatedly every ~1s until the request completes or times out. * `timed_out` (`ERROR`): the request ran for longer than the determined timeout and was aborted. `Rack::Timeout::RequestTimeoutException` is raised in the application when this occurs. This state is not the final one, `completed` will be set after the framework is done with it. (If the exception does bubble up, it's caught by rack-timeout and re-raised as `Rack::Timeout::RequestTimeoutError`, which descends from RuntimeError.) * `completed` (`INFO`): the request completed and Rack::Timeout is done with it. This does not mean the request completed *successfully*. Rack::Timeout does not concern itself with that. As mentioned just above, a timed out request will still end up with a `completed` state. rack-timeout-0.5.1/doc/logging.md0000644000004100000410000000370413376045222016676 0ustar www-datawww-dataLogging ------- Rack::Timeout logs a line every time there's a change in state in a request's lifetime. Request state changes into `timed_out` and `expired` are logged at the `ERROR` level, most other things are logged as `INFO`. The `active` state is logged as `DEBUG`, every ~1s while the request is still active. Rack::Timeout will try to use `Rails.logger` if present, otherwise it'll look for a logger in `env['rack.logger']`, and if neither are present, it'll create its own logger, either writing to `env['rack.errors']`, or to `$stderr` if the former is not set. When creating its own logger, rack-timeout will use a log level of `INFO`. Otherwise whatever log level is already set on the logger being used continues in effect. A custom logger can be set via `Rack::Timeout::Logger.logger`. This takes priority over the automatic logger detection: ```ruby Rack::Timeout::Logger.logger = Logger.new ``` There are helper setters that replace the logger: ```ruby Rack::Timeout::Logger.device = $stderr Rack::Timeout::Logger.level = Logger::INFO ``` Although each call replaces the logger, these can be use together and the final logger will retain both properties. (If only one is called, the defaults used above apply.) Logging is enabled by default, but can be removed with: ```ruby Rack::Timeout::Logger.disable ``` Each log line is a set of `key=value` pairs, containing the entries from the `env["rack-timeout.info"]` struct that are not `nil`. See the Request Lifetime section above for a description of each field. Note that while the values for `wait`, `timeout`, and `service` are stored internally as seconds, they are logged as milliseconds for readability. A sample log excerpt might look like: ``` source=rack-timeout id=13793c wait=369ms timeout=10000ms state=ready at=info source=rack-timeout id=13793c wait=369ms timeout=10000ms service=15ms state=completed at=info source=rack-timeout id=ea7bd3 wait=371ms timeout=10000ms state=timed_out at=error ``` rack-timeout-0.5.1/Gemfile0000644000004100000410000000004713376045222015451 0ustar www-datawww-datasource 'https://rubygems.org' gemspec rack-timeout-0.5.1/MIT-LICENSE0000644000004100000410000000203713376045222015613 0ustar www-datawww-dataCopyright © 2010 Caio Chassot Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. rack-timeout-0.5.1/rack-timeout.gemspec0000644000004100000410000000476213376045222020137 0ustar www-datawww-data######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- # stub: rack-timeout 0.5.1 ruby lib Gem::Specification.new do |s| s.name = "rack-timeout".freeze s.version = "0.5.1" s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= s.require_paths = ["lib".freeze] s.authors = ["Caio Chassot".freeze] s.date = "2018-05-31" s.description = "Rack middleware which aborts requests that have been running for longer than a specified timeout.".freeze s.email = "caio@heroku.com".freeze s.files = ["Gemfile".freeze, "MIT-LICENSE".freeze, "Rakefile".freeze, "doc/exceptions.md".freeze, "doc/logging.md".freeze, "doc/observers.md".freeze, "doc/request-lifecycle.md".freeze, "doc/risks.md".freeze, "doc/rollbar.md".freeze, "doc/settings.md".freeze, "lib/rack-timeout.rb".freeze, "lib/rack/timeout/base.rb".freeze, "lib/rack/timeout/core.rb".freeze, "lib/rack/timeout/logger.rb".freeze, "lib/rack/timeout/logging-observer.rb".freeze, "lib/rack/timeout/rails.rb".freeze, "lib/rack/timeout/rollbar.rb".freeze, "lib/rack/timeout/support/monotonic_time.rb".freeze, "lib/rack/timeout/support/namespace.rb".freeze, "lib/rack/timeout/support/scheduler.rb".freeze, "lib/rack/timeout/support/timeout.rb".freeze, "test/basic_test.rb".freeze, "test/env_settings_test.rb".freeze, "test/test_helper.rb".freeze] s.homepage = "http://github.com/heroku/rack-timeout".freeze s.licenses = ["MIT".freeze] s.rubygems_version = "2.5.2.1".freeze s.summary = "Abort requests that are taking too long".freeze s.test_files = ["Gemfile".freeze, "Rakefile".freeze, "test/basic_test.rb".freeze, "test/env_settings_test.rb".freeze, "test/test_helper.rb".freeze] if s.respond_to? :specification_version then s.specification_version = 4 if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then s.add_development_dependency(%q.freeze, [">= 0"]) s.add_development_dependency(%q.freeze, [">= 0"]) s.add_development_dependency(%q.freeze, [">= 0"]) else s.add_dependency(%q.freeze, [">= 0"]) s.add_dependency(%q.freeze, [">= 0"]) s.add_dependency(%q.freeze, [">= 0"]) end else s.add_dependency(%q.freeze, [">= 0"]) s.add_dependency(%q.freeze, [">= 0"]) s.add_dependency(%q.freeze, [">= 0"]) end end