Class: Basecamp::Config
- Inherits:
-
Object
- Object
- Basecamp::Config
- Defined in:
- lib/basecamp/config.rb
Overview
Configuration for the Basecamp API client.
Constant Summary collapse
- DEFAULT_BASE_URL =
Default values
"https://3.basecampapi.com"- DEFAULT_TIMEOUT =
30- DEFAULT_MAX_RETRIES =
3- DEFAULT_BASE_DELAY =
1.0- DEFAULT_MAX_JITTER =
0.1- DEFAULT_MAX_PAGES =
10_000- MAX_BACKOFF_DELAY =
Ceiling on the backoff term (SPEC §7, "Backoff Ceiling"), in seconds. Jitter is added after the clamp, so the longest single backoff sleep is this plus
max_jitter. 30.0
Instance Attribute Summary collapse
-
#base_delay ⇒ Float
Initial backoff delay in seconds.
-
#base_url ⇒ String
API base URL.
-
#max_jitter ⇒ Float
Maximum jitter to add to delays in seconds.
-
#max_pages ⇒ Integer
Maximum pages to fetch in paginated requests.
-
#max_retries ⇒ Integer
Total request attempts for GET requests, including the initial request (0 sends no requests at all and raises).
-
#timeout ⇒ Integer
Request timeout in seconds.
Class Method Summary collapse
-
.from_env ⇒ Config
Creates a Config from environment variables.
-
.from_file(path) ⇒ Config
Loads configuration from a JSON file, with environment overrides.
-
.global_config_dir ⇒ String
Returns the default global config directory.
-
.saturating_backoff(base_delay, attempt) ⇒ Float
Exponential backoff for a 1-based attempt, saturating at MAX_BACKOFF_DELAY.
-
.saturating_exponent(base_delay) ⇒ Integer
Smallest exponent
ewith base_delay * 2**e >= MAX_BACKOFF_DELAY.
Instance Method Summary collapse
-
#initialize(base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT, max_retries: DEFAULT_MAX_RETRIES, base_delay: DEFAULT_BASE_DELAY, max_jitter: DEFAULT_MAX_JITTER, max_pages: DEFAULT_MAX_PAGES) ⇒ Config
constructor
Creates a new configuration with the given options.
-
#load_from_env ⇒ self
Loads environment variable overrides into this config.
Constructor Details
#initialize(base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT, max_retries: DEFAULT_MAX_RETRIES, base_delay: DEFAULT_BASE_DELAY, max_jitter: DEFAULT_MAX_JITTER, max_pages: DEFAULT_MAX_PAGES) ⇒ Config
Creates a new configuration with the given options.
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/basecamp/config.rb', line 125 def initialize( base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT, max_retries: DEFAULT_MAX_RETRIES, base_delay: DEFAULT_BASE_DELAY, max_jitter: DEFAULT_MAX_JITTER, max_pages: DEFAULT_MAX_PAGES ) @base_url = normalize_url(base_url) @timeout = timeout @max_retries = max_retries @base_delay = base_delay @max_jitter = max_jitter @max_pages = max_pages unless @base_url == normalize_url(DEFAULT_BASE_URL) || localhost?(@base_url) Basecamp::Security.require_https!(@base_url, "base URL") end validate! end |
Instance Attribute Details
#base_delay ⇒ Float
Returns initial backoff delay in seconds.
32 33 34 |
# File 'lib/basecamp/config.rb', line 32 def base_delay @base_delay end |
#base_url ⇒ String
Returns API base URL.
22 23 24 |
# File 'lib/basecamp/config.rb', line 22 def base_url @base_url end |
#max_jitter ⇒ Float
Returns maximum jitter to add to delays in seconds.
35 36 37 |
# File 'lib/basecamp/config.rb', line 35 def max_jitter @max_jitter end |
#max_pages ⇒ Integer
Returns maximum pages to fetch in paginated requests.
38 39 40 |
# File 'lib/basecamp/config.rb', line 38 def max_pages @max_pages end |
#max_retries ⇒ Integer
Returns total request attempts for GET requests, including the initial request (0 sends no requests at all and raises).
29 30 31 |
# File 'lib/basecamp/config.rb', line 29 def max_retries @max_retries end |
#timeout ⇒ Integer
Returns request timeout in seconds.
25 26 27 |
# File 'lib/basecamp/config.rb', line 25 def timeout @timeout end |
Class Method Details
.from_env ⇒ Config
Creates a Config from environment variables.
Environment variables:
- BASECAMP_BASE_URL: API base URL
- BASECAMP_TIMEOUT: Request timeout in seconds
- BASECAMP_MAX_RETRIES: Total request attempts for GET requests, including the initial request
154 155 156 157 158 159 160 |
# File 'lib/basecamp/config.rb', line 154 def self.from_env new( base_url: ENV.fetch("BASECAMP_BASE_URL", DEFAULT_BASE_URL), timeout: ENV.fetch("BASECAMP_TIMEOUT", DEFAULT_TIMEOUT).to_i, max_retries: ENV.fetch("BASECAMP_MAX_RETRIES", DEFAULT_MAX_RETRIES).to_i ) end |
.from_file(path) ⇒ Config
Loads configuration from a JSON file, with environment overrides.
166 167 168 169 170 171 172 173 174 175 176 177 |
# File 'lib/basecamp/config.rb', line 166 def self.from_file(path) data = JSON.parse(File.read(path)) config = new( base_url: data["base_url"] || DEFAULT_BASE_URL, timeout: data["timeout"] || DEFAULT_TIMEOUT, max_retries: data["max_retries"] || DEFAULT_MAX_RETRIES ) config.load_from_env config rescue Errno::ENOENT from_env end |
.global_config_dir ⇒ String
Returns the default global config directory.
192 193 194 195 |
# File 'lib/basecamp/config.rb', line 192 def self.global_config_dir config_dir = ENV["XDG_CONFIG_HOME"] || File.join(Dir.home, ".config") File.join(config_dir, "basecamp") end |
.saturating_backoff(base_delay, attempt) ⇒ Float
Exponential backoff for a 1-based attempt, saturating at MAX_BACKOFF_DELAY.
The clamp is load-bearing rather than defensive. Ruby's ** promotes
instead of overflowing, so base_delay * (2**(attempt - 1)) on a long
failure streak coerces to Float::INFINITY — and sleep(Float::INFINITY)
never returns. A retry that never happens is not backoff.
The exponent is compared against the point where the term reaches the ceiling before the power is evaluated, so no intermediate leaves the Float range and the term saturates AT the ceiling for every positive base — the same contract Go, Kotlin and Swift get from comparing their multiplier against MAX_BACKOFF_DELAY / base before multiplying.
Below that point the term is scaled with Math.ldexp, which computes
base * 2**e directly. 2**e would be an unbounded Integer that
coerces to Float::INFINITY long before the product leaves the Float
range, which is what forced the fixed cap this replaces.
104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/basecamp/config.rb', line 104 def self.(base_delay, attempt) if base_delay <= 0 0.0 else exponent = [ attempt - 1, 0 ].max if exponent >= (base_delay) MAX_BACKOFF_DELAY else [ Math.ldexp(base_delay, exponent), MAX_BACKOFF_DELAY ].min.to_f end end end |
.saturating_exponent(base_delay) ⇒ Integer
Smallest exponent e with base_delay * 2**e >= MAX_BACKOFF_DELAY.
Derived from the configured base rather than assumed. A fixed exponent
cap plus a trailing min(..., MAX_BACKOFF_DELAY) looks equivalent and is
not: for a small enough base the capped product never reaches the ceiling,
so the delay plateaus below it forever. At base_delay = 1e-30 a cap of
64 pins every attempt from 65 on at ~1.84e-11s — a tight retry loop, which
is the failure SPEC §7's ceiling exists to prevent, not an instance of it.
Computed in the LOG domain rather than as
MAX_BACKOFF_DELAY / base_delay. That ratio coerces to
Float::INFINITY for any base below ~1.67e-307, and falling back to a
fixed 1023 then saturates early: base_delay = 1e-307 reaches only
~8.99s at exponent 1023, so returning the 30s ceiling there overstates the
specified term instead of tracking it. The log form has no such cliff, so
the numeric backstop is gone entirely rather than merely made rarer.
72 73 74 75 76 77 78 79 80 81 |
# File 'lib/basecamp/config.rb', line 72 def self.(base_delay) # log2 is correctly rounded but the subtraction is not, so the estimate can # land one either side of the true boundary. Both corrections are bounded # and evaluate the term with Math.ldexp, which scales directly and never # forms 2**e. exponent = [ (Math.log2(MAX_BACKOFF_DELAY) - Math.log2(base_delay)).ceil, 0 ].max exponent -= 1 while exponent > 0 && Math.ldexp(base_delay, exponent - 1) >= MAX_BACKOFF_DELAY exponent += 1 while Math.ldexp(base_delay, exponent) < MAX_BACKOFF_DELAY exponent end |
Instance Method Details
#load_from_env ⇒ self
Loads environment variable overrides into this config.
181 182 183 184 185 186 187 188 |
# File 'lib/basecamp/config.rb', line 181 def load_from_env @base_url = normalize_url(ENV["BASECAMP_BASE_URL"]) if ENV["BASECAMP_BASE_URL"] @timeout = ENV["BASECAMP_TIMEOUT"].to_i if ENV["BASECAMP_TIMEOUT"] @max_retries = ENV["BASECAMP_MAX_RETRIES"].to_i if ENV["BASECAMP_MAX_RETRIES"] Basecamp::Security.require_https!(@base_url, "base URL") unless localhost?(@base_url) validate! self end |