Class: TIMEx::Deadline
- Inherits:
-
Object
- Object
- TIMEx::Deadline
- Defined in:
- lib/timex/deadline.rb
Overview
Immutable deadline: an absolute monotonic expiry (+monotonic_ns+), optional
wall alignment (+wall_ns+), and propagation metadata (+origin+, depth).
Construct via Deadline.in, Deadline.at_wall, Deadline.infinite, Deadline.coerce, or Deadline.from_header; compare and narrow with #min. Instances are frozen at construction.
Constant Summary collapse
- HEADER_NAME =
"X-TIMEx-Deadline"- DEFAULT_SKEW_TOLERANCE_MS =
250- MAX_HEADER_BYTESIZE =
256- MAX_MS_VALUE =
1 year, prevents overflow attacks
365 * 24 * 60 * 60 * 1000
- MAX_BUDGET_SECONDS =
Upper bound for in in seconds, aligned with MAX_MS_VALUE / 1000 so numeric budgets cannot exceed what untrusted headers can express.
MAX_MS_VALUE / 1000.0
- MAX_DEPTH =
64- ORIGIN_MAX_BYTESIZE =
64- ORIGIN_PATTERN =
/\A[A-Za-z0-9_.-]+\z/- MAX_ISO8601_BYTESIZE =
Tight cap on iso8601 timestamp length to bound the cost of Rational math in
check_wall_skew. A canonical TIMEx-emitted stamp is 24 chars (YYYY-MM-DDTHH:MM:SS.sssZ); 40 leaves room for offset variants. 40- INFINITE =
Eagerly initialized after the class is defined (see bottom of file) so concurrent first-callers can't race to construct two distinct sentinels.
nil
Instance Attribute Summary collapse
-
#depth ⇒ Object
readonly
placeholder; overwritten below.
-
#initial_ns ⇒ Object
readonly
placeholder; overwritten below.
-
#monotonic_ns ⇒ Object
readonly
placeholder; overwritten below.
-
#origin ⇒ Object
readonly
placeholder; overwritten below.
-
#wall_ns ⇒ Object
readonly
placeholder; overwritten below.
Class Method Summary collapse
-
.at_wall(time) ⇒ Deadline
Builds a deadline that expires when wall clock reaches
time(nanosecond precision). -
.coerce(value) ⇒ Deadline
Normalizes user input into a Deadline.
-
.from_header(str, skew_tolerance_ms: nil) ⇒ Deadline?
Parses the wire-format deadline header into a Deadline, or
nilwhen malformed, oversized, ambiguous, or rejected for security policy. -
.in(seconds) ⇒ Deadline
Builds a relative deadline from now using the active Clock.
-
.infinite ⇒ Deadline
Shared infinite sentinel (INFINITE).
Instance Method Summary collapse
-
#==(other) ⇒ Boolean
(also: #eql?)
truewhen monotonic instant, wall, and propagation metadata match. - #check!(strategy: nil) ⇒ void
-
#expired? ⇒ Boolean
truewhen already past the monotonic expiry (nevertruewhen infinite). -
#expired_error(strategy: nil, message: "deadline expired") ⇒ Expired
Builds an Expired that consistently reports the original budget as
deadline_ms(positive) and the overshoot/elapsed-past aselapsed_ms. -
#hash ⇒ Integer
Hash of monotonic instant and metadata.
-
#infinite? ⇒ Boolean
truewhen this deadline never fires. -
#initial_ms ⇒ Float?
Original budget in milliseconds, or
nilfor infinite deadlines or when no budget was captured at construction. -
#initialize(monotonic_ns:, wall_ns: nil, origin: nil, depth: 0, infinite: false, initial_ns: nil) ⇒ void
constructor
Creates a frozen deadline.
-
#inspect ⇒ String
Short debug representation.
-
#min(other) ⇒ Deadline
Earliest-expiring of
selfandother(finite vs infinite handled). -
#remaining ⇒ Float
Seconds remaining;
Float::INFINITYwhen infinite. -
#remaining_ms ⇒ Float
Milliseconds remaining;
Float::INFINITYwhen infinite. -
#remaining_ns ⇒ Float, Integer
Nanoseconds remaining before expiry;
Float::INFINITYwhen infinite. -
#same_instant?(other) ⇒ Boolean
truewhenotheris a Deadline with the same monotonic expiry. -
#shield { ... } ⇒ Object
Temporarily disables #expired? for the current thread (all fibers).
-
#to_header(prefer: :remaining) ⇒ String
Serializes this deadline for the
X-TIMEx-Deadlineheader. -
#with_meta(origin: nil, depth: nil) ⇒ Deadline
Returns a copy with updated propagation metadata (+origin+,
depth).
Constructor Details
#initialize(monotonic_ns:, wall_ns: nil, origin: nil, depth: 0, infinite: false, initial_ns: nil) ⇒ void
246 247 248 249 250 251 252 253 254 |
# File 'lib/timex/deadline.rb', line 246 def initialize(monotonic_ns:, wall_ns: nil, origin: nil, depth: 0, infinite: false, initial_ns: nil) # rubocop:disable Metrics/ParameterLists @monotonic_ns = monotonic_ns @wall_ns = wall_ns @origin = origin @depth = depth || 0 @infinite = infinite @initial_ns = initial_ns freeze end |
Instance Attribute Details
#depth ⇒ Object (readonly)
placeholder; overwritten below
231 232 233 |
# File 'lib/timex/deadline.rb', line 231 def depth @depth end |
#initial_ns ⇒ Object (readonly)
placeholder; overwritten below
231 232 233 |
# File 'lib/timex/deadline.rb', line 231 def initial_ns @initial_ns end |
#monotonic_ns ⇒ Object (readonly)
placeholder; overwritten below
231 232 233 |
# File 'lib/timex/deadline.rb', line 231 def monotonic_ns @monotonic_ns end |
#origin ⇒ Object (readonly)
placeholder; overwritten below
231 232 233 |
# File 'lib/timex/deadline.rb', line 231 def origin @origin end |
#wall_ns ⇒ Object (readonly)
placeholder; overwritten below
231 232 233 |
# File 'lib/timex/deadline.rb', line 231 def wall_ns @wall_ns end |
Class Method Details
.at_wall(time) ⇒ Deadline
Builds a deadline that expires when wall clock reaches time (nanosecond precision).
61 62 63 64 65 66 67 68 69 70 |
# File 'lib/timex/deadline.rb', line 61 def at_wall(time) wall_now = Clock.wall_ns target_wall = (time.tv_sec * Clock::NS_PER_SECOND) + time.tv_nsec delta = target_wall - wall_now new( monotonic_ns: Clock.monotonic_ns + delta, wall_ns: target_wall, initial_ns: delta ) end |
.coerce(value) ⇒ Deadline
Normalizes user input into a TIMEx::Deadline.
82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/timex/deadline.rb', line 82 def coerce(value) case value when Deadline then value when Numeric then self.in(value) when Time then at_wall(value) when nil then infinite when Symbol raise ArgumentError, "cannot coerce #{value.inspect} into a Deadline " \ "(did you mean strategy: #{value.inspect}?)" else raise ArgumentError, "cannot coerce #{value.inspect} into a Deadline" end end |
.from_header(str, skew_tolerance_ms: nil) ⇒ Deadline?
Rejects combined ms and wall fields, duplicate keys, negative depth,
and oversized payloads. Skew detection emits telemetry but does not mutate
the parsed deadline.
Parses the wire-format deadline header into a TIMEx::Deadline, or nil when
malformed, oversized, ambiguous, or rejected for security policy.
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 |
# File 'lib/timex/deadline.rb', line 106 def from_header(str, skew_tolerance_ms: nil) skew_tolerance_ms ||= TIMEx.config.skew_tolerance_ms return nil if str.nil? || str.empty? || str.bytesize > MAX_HEADER_BYTESIZE parts = parse_header_pairs(str) return nil if parts.nil? || parts.empty? # Reject ambiguous payloads up front: an attacker who can append to a # trusted upstream's header could otherwise smuggle `ms=99999` next to # `wall=` to extend the budget, or supply both and rely on parser # precedence. Either is a single, well-defined field. return nil if parts.key?("ms") && parts.key?("wall") depth = parts["depth"] && Integer(parts["depth"], 10, exception: false) # Reject explicitly negative depth so client bugs don't silently coerce # to 0 and bypass `max_depth` ceilings on the receiver. return nil if depth&.negative? depth = depth.clamp(0, MAX_DEPTH) if depth origin = sanitize_origin(parts["origin"]) if parts.key?("ms") ms = parts["ms"] # Even for `ms=inf` we attach origin/depth so middleware can enforce # `max_depth` on infinite-budget propagations. Returning the shared # sentinel here would drop those, allowing depth-limit bypass. if ms == "inf" return infinite if origin.nil? && depth.nil? return new( monotonic_ns: Float::INFINITY, wall_ns: nil, origin:, depth: depth || 0, infinite: true ) end ms_value = Float(ms, exception: false) return nil if ms_value.nil? || !ms_value.finite? || ms_value.negative? || ms_value > MAX_MS_VALUE self.in(ms_value / 1000.0).(origin:, depth:) elsif parts.key?("wall") wall_raw = parts["wall"] return nil if wall_raw.bytesize > MAX_ISO8601_BYTESIZE wall_time = Time.iso8601(wall_raw) d = at_wall(wall_time) check_wall_skew(d, parts, skew_tolerance_ms) d.(origin:, depth:) end rescue ArgumentError, TypeError, RangeError nil end |
.in(seconds) ⇒ Deadline
Builds a relative deadline from now using the active Clock.
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
# File 'lib/timex/deadline.rb', line 38 def in(seconds) return infinite if seconds.nil? if seconds.is_a?(Numeric) return demoted_to_infinite(seconds, reason: :non_finite) if seconds.respond_to?(:finite?) && !seconds.finite? return demoted_to_infinite(seconds, reason: :over_max_budget) if seconds > MAX_BUDGET_SECONDS end product = seconds * Clock::NS_PER_SECOND return demoted_to_infinite(seconds, reason: :float_overflow) if product.is_a?(Float) && !product.finite? delta_ns = product.to_i new( monotonic_ns: Clock.monotonic_ns + delta_ns, wall_ns: Clock.wall_ns + delta_ns, initial_ns: delta_ns ) end |
Instance Method Details
#==(other) ⇒ Boolean Also known as: eql?
Requires wall_ns parity so #to_header+(+prefer: :wall+) cannot diverge for equal instances.
Returns true when monotonic instant, wall, and propagation metadata match.
415 416 417 418 419 420 421 |
# File 'lib/timex/deadline.rb', line 415 def ==(other) other.is_a?(Deadline) && other.monotonic_ns == monotonic_ns && other.wall_ns == wall_ns && other.origin == origin && other.depth == depth end |
#check!(strategy: nil) ⇒ void
325 326 327 328 329 |
# File 'lib/timex/deadline.rb', line 325 def check!(strategy: nil) return unless expired? raise expired_error(strategy:) end |
#expired? ⇒ Boolean
Returns false while #shield is active on the current thread.
Returns true when already past the monotonic expiry (never true when infinite).
313 314 315 316 317 318 |
# File 'lib/timex/deadline.rb', line 313 def expired? return false if infinite? return false if Thread.current.thread_variable_get(:timex_shielded) remaining_ns <= 0 end |
#expired_error(strategy: nil, message: "deadline expired") ⇒ Expired
Builds an Expired that consistently reports the original budget as
deadline_ms (positive) and the overshoot/elapsed-past as elapsed_ms.
Strategies should use this instead of constructing Expired ad-hoc to
keep deadline_ms semantics uniform across the codebase.
339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 |
# File 'lib/timex/deadline.rb', line 339 def expired_error(strategy: nil, message: "deadline expired") remaining = remaining_ms overshoot = if infinite_remaining_float?(remaining) nil else (-remaining).round end Expired.new( , strategy:, deadline_ms: initial_ms&.round, elapsed_ms: overshoot ) end |
#hash ⇒ Integer
Returns hash of monotonic instant and metadata.
425 426 427 |
# File 'lib/timex/deadline.rb', line 425 def hash [@monotonic_ns, @wall_ns, @origin, @depth].hash end |
#infinite? ⇒ Boolean
Returns true when this deadline never fires.
283 284 285 |
# File 'lib/timex/deadline.rb', line 283 def infinite? @infinite || @monotonic_ns == Float::INFINITY end |
#initial_ms ⇒ Float?
Original budget in milliseconds, or nil for infinite deadlines or when
no budget was captured at construction.
260 261 262 263 264 |
# File 'lib/timex/deadline.rb', line 260 def initial_ms return nil if infinite? || @initial_ns.nil? @initial_ns / 1_000_000.0 end |
#inspect ⇒ String
Returns short debug representation.
436 437 438 |
# File 'lib/timex/deadline.rb', line 436 def inspect "#<TIMEx::Deadline remaining=#{infinite? ? 'inf' : "#{remaining_ms.round}ms"} origin=#{@origin.inspect}>" end |
#min(other) ⇒ Deadline
Earliest-expiring of self and other (finite vs infinite handled).
359 360 361 362 363 364 365 366 367 |
# File 'lib/timex/deadline.rb', line 359 def min(other) return self if other.nil? other = self.class.coerce(other) return other if infinite? return self if other.infinite? @monotonic_ns <= other.monotonic_ns ? self : other end |
#remaining ⇒ Float
Returns seconds remaining; Float::INFINITY when infinite.
295 296 297 298 299 300 |
# File 'lib/timex/deadline.rb', line 295 def remaining r = remaining_ns return Float::INFINITY if infinite_remaining_float?(r) r / Clock::NS_PER_SECOND.to_f end |
#remaining_ms ⇒ Float
Returns milliseconds remaining; Float::INFINITY when infinite.
303 304 305 306 307 308 |
# File 'lib/timex/deadline.rb', line 303 def remaining_ms r = remaining_ns return Float::INFINITY if infinite_remaining_float?(r) r / 1_000_000.0 end |
#remaining_ns ⇒ Float, Integer
Returns nanoseconds remaining before expiry; Float::INFINITY when infinite.
288 289 290 291 292 |
# File 'lib/timex/deadline.rb', line 288 def remaining_ns return Float::INFINITY if infinite? @monotonic_ns - Clock.monotonic_ns end |
#same_instant?(other) ⇒ Boolean
Returns true when other is a TIMEx::Deadline with the same monotonic expiry.
431 432 433 |
# File 'lib/timex/deadline.rb', line 431 def same_instant?(other) other.is_a?(Deadline) && other.monotonic_ns == monotonic_ns end |
#shield { ... } ⇒ Object
376 377 378 379 380 381 382 |
# File 'lib/timex/deadline.rb', line 376 def shield previous = Thread.current.thread_variable_get(:timex_shielded) Thread.current.thread_variable_set(:timex_shielded, true) yield ensure Thread.current.thread_variable_set(:timex_shielded, previous) end |
#to_header(prefer: :remaining) ⇒ String
Serializes this deadline for the X-TIMEx-Deadline header.
388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 |
# File 'lib/timex/deadline.rb', line 388 def to_header(prefer: :remaining) buf = +"" if infinite? buf << "ms=inf" # Bare `ms=inf` round-trips to the shared {Deadline.infinite} sentinel # (see {.from_header}). Append metadata only when present. if @origin || !@depth.zero? buf << ";origin=" << @origin if @origin buf << ";depth=" << (@depth + 1).clamp(0, MAX_DEPTH).to_s end return buf end if prefer == :wall && @wall_ns buf << "wall=" << ns_to_iso8601(@wall_ns) buf << ";now=" << ns_to_iso8601(Clock.wall_ns) else buf << "ms=" << remaining_ms.round.to_s end buf << ";origin=" << @origin if @origin buf << ";depth=" << (@depth + 1).clamp(0, MAX_DEPTH).to_s buf end |
#with_meta(origin: nil, depth: nil) ⇒ Deadline
Returns a copy with updated propagation metadata (+origin+, depth).
271 272 273 274 275 276 277 278 279 280 |
# File 'lib/timex/deadline.rb', line 271 def (origin: nil, depth: nil) self.class.new( monotonic_ns: @monotonic_ns, wall_ns: @wall_ns, origin: (origin && self.class.send(:sanitize_origin, origin)) || @origin, depth: depth || @depth, infinite: @infinite, initial_ns: @initial_ns ) end |