timex 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.DS_Store +0 -0
  3. data/CHANGELOG.md +11 -0
  4. data/CODE_OF_CONDUCT.md +132 -0
  5. data/LICENSE.txt +4 -0
  6. data/README.md +112 -0
  7. data/Rakefile +28 -0
  8. data/lib/generators/timex/install_generator.rb +21 -0
  9. data/lib/generators/timex/templates/install.rb +54 -0
  10. data/lib/timex/auto_check.rb +59 -0
  11. data/lib/timex/cancellation_token.rb +84 -0
  12. data/lib/timex/clock.rb +113 -0
  13. data/lib/timex/composers/adaptive.rb +222 -0
  14. data/lib/timex/composers/base.rb +20 -0
  15. data/lib/timex/composers/hedged.rb +146 -0
  16. data/lib/timex/composers/two_phase.rb +97 -0
  17. data/lib/timex/configuration.rb +163 -0
  18. data/lib/timex/deadline.rb +458 -0
  19. data/lib/timex/expired.rb +77 -0
  20. data/lib/timex/named_component.rb +33 -0
  21. data/lib/timex/on_timeout.rb +15 -0
  22. data/lib/timex/propagation/http_header.rb +49 -0
  23. data/lib/timex/propagation/rack_middleware.rb +180 -0
  24. data/lib/timex/registry.rb +132 -0
  25. data/lib/timex/result.rb +137 -0
  26. data/lib/timex/strategies/base.rb +88 -0
  27. data/lib/timex/strategies/closeable.rb +81 -0
  28. data/lib/timex/strategies/cooperative.rb +27 -0
  29. data/lib/timex/strategies/io.rb +247 -0
  30. data/lib/timex/strategies/ractor.rb +84 -0
  31. data/lib/timex/strategies/subprocess.rb +267 -0
  32. data/lib/timex/strategies/unsafe.rb +54 -0
  33. data/lib/timex/strategies/wakeup.rb +154 -0
  34. data/lib/timex/telemetry/adapters.rb +173 -0
  35. data/lib/timex/telemetry.rb +119 -0
  36. data/lib/timex/test/virtual_clock.rb +51 -0
  37. data/lib/timex/timeout_handling.rb +39 -0
  38. data/lib/timex/version.rb +8 -0
  39. data/lib/timex.rb +79 -0
  40. data/mkdocs.yml +193 -0
  41. metadata +239 -0
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TIMEx
4
+ module Strategies
5
+ # Pipe-based wakeup primitive: blocked I/O on {#read_io} unblocks when the
6
+ # deadline fires (or {#cancel!} is called), and the {CancellationToken}
7
+ # transitions to {#fired?}. Use to wake threads blocked in +IO.select+ on
8
+ # resources without native deadlines.
9
+ #
10
+ # Each instance is **single-use**: the pipe is created lazily and closed in
11
+ # +ensure+ after {#run}. Construct a fresh instance per operation.
12
+ #
13
+ # @note Accessing {#read_io} / {#write_io} before {#arm} without a constructor
14
+ # deadline creates the pipe but does **not** install a timer; the read end
15
+ # blocks until {#cancel!} unless you pass a deadline to {#initialize} or call {#arm}.
16
+ #
17
+ # @see CancellationToken
18
+ # @see Base
19
+ class Wakeup < Base
20
+
21
+ attr_reader :token
22
+
23
+ # @param deadline [Deadline, Numeric, Time, nil] when given, calls {#arm} immediately
24
+ def initialize(deadline = nil)
25
+ super()
26
+ @token = CancellationToken.new
27
+ @read_io = nil
28
+ @write_io = nil
29
+ @timer = nil
30
+ @closed = false
31
+ @io_mutex = Mutex.new
32
+ arm(deadline) if deadline
33
+ end
34
+
35
+ # @return [::IO] readable end of the wakeup pipe (creates the pipe lazily)
36
+ def read_io
37
+ ensure_pipe
38
+ @read_io
39
+ end
40
+
41
+ # @return [::IO] writable end used internally to signal readiness
42
+ def write_io
43
+ ensure_pipe
44
+ @write_io
45
+ end
46
+
47
+ # @return [Boolean] +true+ after {#close}
48
+ def closed?
49
+ @io_mutex.synchronize { @closed }
50
+ end
51
+
52
+ # Arms a background timer that invokes {#cancel!} with +:timeout+ when the
53
+ # deadline elapses.
54
+ #
55
+ # @param deadline [Deadline, Numeric, Time, nil]
56
+ # @return [void]
57
+ # @raise [TIMEx::Error] when the instance was already closed
58
+ def arm(deadline)
59
+ raise TIMEx::Error, "Wakeup is single-use; construct a fresh instance" if closed?
60
+
61
+ deadline = Deadline.coerce(deadline)
62
+ return if deadline.infinite?
63
+
64
+ ensure_pipe
65
+ @timer = Thread.new do
66
+ remaining = deadline.remaining
67
+ ::Kernel.sleep(remaining) if remaining.positive?
68
+ fire(reason: :timeout)
69
+ end
70
+ end
71
+
72
+ # Cancels observers and wakes blocked readers.
73
+ #
74
+ # @param reason [Symbol] opaque reason forwarded to {CancellationToken#cancel}
75
+ # @return [Boolean, nil] result of the internal fire sequence
76
+ def cancel!(reason: :user)
77
+ fire(reason:)
78
+ end
79
+
80
+ # @return [Boolean] whether {#cancel!} / timeout has fired
81
+ def fired?
82
+ @token.cancelled?
83
+ end
84
+
85
+ # Idempotently closes pipe ends and stops the timer thread.
86
+ #
87
+ # @return [void]
88
+ def close
89
+ @timer&.kill
90
+ @io_mutex.synchronize do
91
+ return if @closed
92
+
93
+ @read_io.close if @read_io && !@read_io.closed?
94
+ @write_io.close if @write_io && !@write_io.closed?
95
+ @closed = true
96
+ end
97
+ end
98
+
99
+ protected
100
+
101
+ # @param deadline [Deadline]
102
+ # @yieldparam deadline [Deadline]
103
+ # @return [Object]
104
+ # @raise [TIMEx::Error] when reused after {#close}
105
+ def run(deadline)
106
+ raise TIMEx::Error, "Wakeup is single-use; construct a fresh instance" if closed?
107
+
108
+ arm(deadline) unless @timer
109
+ yield(deadline)
110
+ ensure
111
+ close
112
+ end
113
+
114
+ private
115
+
116
+ # @return [void]
117
+ def ensure_pipe
118
+ @io_mutex.synchronize do
119
+ raise TIMEx::Error, "Wakeup is single-use; construct a fresh instance" if @closed
120
+ return if @read_io
121
+
122
+ @read_io, @write_io = ::IO.pipe
123
+ end
124
+ end
125
+
126
+ # @param reason [Symbol]
127
+ # @return [void]
128
+ def fire(reason:)
129
+ return unless @token.cancel(reason:)
130
+
131
+ # Kill the sleeping timer if we were the first to win the race so the
132
+ # thread doesn't dangle in `Kernel.sleep` and write to a closed pipe.
133
+ if @timer && reason != :timeout && @timer.alive?
134
+ begin
135
+ @timer.kill
136
+ rescue StandardError
137
+ nil
138
+ end
139
+ end
140
+
141
+ return unless @write_io
142
+
143
+ begin
144
+ @write_io.write_nonblock("!")
145
+ rescue StandardError
146
+ nil
147
+ end
148
+ end
149
+
150
+ end
151
+ end
152
+ end
153
+
154
+ TIMEx::Registry.register(:wakeup, TIMEx::Strategies::Wakeup)
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TIMEx
4
+ module Telemetry
5
+ # Concrete {Telemetry} backends. All adapters receive symbol/string event names
6
+ # and a mutable +payload+ hash for a single logical operation.
7
+ #
8
+ # @see Telemetry.instrument
9
+ # @see Telemetry.emit
10
+ module Adapters
11
+
12
+ # No-op base type documenting the adapter protocol.
13
+ class Base
14
+
15
+ # @param event [Symbol, String]
16
+ # @param payload [Hash{Symbol => Object}]
17
+ # @return [void]
18
+ def start(event:, payload:); end
19
+
20
+ # @param event [Symbol, String]
21
+ # @param payload [Hash{Symbol => Object}]
22
+ # @return [void]
23
+ def finish(event:, payload:); end
24
+
25
+ # Default one-shot implementation pairing {#start} and {#finish}.
26
+ #
27
+ # @param event [Symbol, String]
28
+ # @param payload [Hash{Symbol => Object}]
29
+ # @return [void]
30
+ def emit(event:, payload:)
31
+ start(event:, payload:)
32
+ finish(event:, payload:)
33
+ end
34
+
35
+ end
36
+
37
+ # Sentinel adapter used when nothing is configured.
38
+ class Null < Base; end
39
+
40
+ # Emits a single structured log line per {#finish} with a conservative key allowlist.
41
+ class Logger < Base
42
+
43
+ # Keys considered safe to log by default. Application-supplied data
44
+ # like `headers` or block arguments are excluded to avoid leaking
45
+ # secrets/PII through structured logs. `origin` is whitelisted
46
+ # because {Deadline.from_header} enforces `ORIGIN_PATTERN` so the
47
+ # value can only be `[A-Za-z0-9_.-]+`. Pass `extra_keys:` to include
48
+ # additional whitelisted keys.
49
+ DEFAULT_SAFE_KEYS = %i[event strategy outcome deadline_ms elapsed_ms error_class
50
+ soft_ms grace_ms estimate_ms budget_ms
51
+ soft_timeout hard_timeout depth skew_ms origin
52
+ reason].freeze
53
+
54
+ # @param logger [#info] logger receiving +#info+ calls
55
+ # @param extra_keys [Array<Symbol, String>] additional payload keys to include
56
+ def initialize(logger, extra_keys: [])
57
+ super()
58
+ @logger = logger
59
+ @safe_keys = (DEFAULT_SAFE_KEYS + Array(extra_keys).map(&:to_sym)).uniq.freeze
60
+ end
61
+
62
+ # @param event [Symbol, String]
63
+ # @param payload [Hash{Symbol => Object}]
64
+ # @return [void]
65
+ def finish(event:, payload:)
66
+ @logger.info("[timex] #{event} #{filtered(payload).inspect}")
67
+ end
68
+
69
+ private
70
+
71
+ # @param payload [Hash{Symbol => Object}]
72
+ # @return [Hash{Symbol => Object}]
73
+ def filtered(payload)
74
+ payload.each_with_object({}) do |(k, v), acc|
75
+ acc[k] = v if @safe_keys.include?(k)
76
+ end
77
+ end
78
+
79
+ end
80
+
81
+ # Bridges TIMEx events to +ActiveSupport::Notifications+.
82
+ class ActiveSupportNotifications < Base
83
+
84
+ EVENT_PREFIX = "timex."
85
+
86
+ def initialize
87
+ super
88
+ require "active_support/notifications"
89
+ end
90
+
91
+ # @param event [Symbol, String]
92
+ # @param payload [Hash{Symbol => Object}]
93
+ # @return [void]
94
+ def start(event:, payload:)
95
+ instrumenter = ::ActiveSupport::Notifications.instrumenter
96
+ payload[:__asn_token] = instrumenter.start("#{EVENT_PREFIX}#{event}", payload)
97
+ end
98
+
99
+ # @param event [Symbol, String]
100
+ # @param payload [Hash{Symbol => Object}]
101
+ # @return [void]
102
+ def finish(event:, payload:)
103
+ token = payload.delete(:__asn_token)
104
+ return unless token
105
+
106
+ ::ActiveSupport::Notifications.instrumenter.finish_with_state(token, "#{EVENT_PREFIX}#{event}", payload)
107
+ end
108
+
109
+ end
110
+
111
+ # Bridges TIMEx spans to OpenTelemetry when the gem is available.
112
+ class OpenTelemetry < Base
113
+
114
+ ATTRIBUTE_TYPES = [String, Symbol, Numeric, TrueClass, FalseClass, NilClass].freeze
115
+
116
+ def initialize
117
+ super
118
+ require "opentelemetry/api"
119
+ end
120
+
121
+ # @param event [Symbol, String]
122
+ # @param payload [Hash{Symbol => Object}]
123
+ # @return [void]
124
+ def start(event:, payload:)
125
+ tracer = ::OpenTelemetry.tracer_provider.tracer("timex")
126
+ payload[:__otel_span] = tracer.start_span(
127
+ event,
128
+ attributes: coerce_attributes(payload)
129
+ )
130
+ end
131
+
132
+ # @param event [Symbol, String]
133
+ # @param payload [Hash{Symbol => Object}]
134
+ # @return [void]
135
+ def finish(event:, payload:)
136
+ span = payload.delete(:__otel_span)
137
+ return unless span
138
+
139
+ span.set_attribute("timex.elapsed_ms", payload[:elapsed_ms]) if payload[:elapsed_ms] && span.respond_to?(:set_attribute)
140
+
141
+ if span.respond_to?(:status=)
142
+ case payload[:outcome]
143
+ when :timeout
144
+ span.status = ::OpenTelemetry::Trace::Status.error("timeout")
145
+ when :error
146
+ span.status = ::OpenTelemetry::Trace::Status.error(payload[:error_class].to_s)
147
+ end
148
+ end
149
+
150
+ span.finish
151
+ end
152
+
153
+ private
154
+
155
+ # Drops keys with nested or non-OTel-supported values rather than
156
+ # lossy `to_s`-ifying everything.
157
+ #
158
+ # @param payload [Hash{Symbol => Object}]
159
+ # @return [Hash{String => Object}]
160
+ def coerce_attributes(payload)
161
+ payload.each_with_object({}) do |(k, v), acc|
162
+ next if k.to_s.start_with?("__")
163
+ next unless ATTRIBUTE_TYPES.any? { |t| v.is_a?(t) }
164
+
165
+ acc[k.to_s] = v.is_a?(Symbol) ? v.to_s : v
166
+ end
167
+ end
168
+
169
+ end
170
+
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TIMEx
4
+ # Lightweight instrumentation facade for strategies, composers, and internal
5
+ # events (deadline skew, cancellation observer errors, etc.).
6
+ #
7
+ # Resolution order for {#adapter}: explicit {Telemetry.adapter=} assignment,
8
+ # then {Configuration#telemetry_adapter}, then {Adapters::Null}.
9
+ #
10
+ # @see Telemetry::Adapters
11
+ # @see Configuration#telemetry_adapter=
12
+ module Telemetry
13
+
14
+ @adapter = nil
15
+ @null_adapter = nil
16
+ @strict = false
17
+
18
+ extend self
19
+
20
+ # Resolves the active adapter on each access so runtime config changes apply.
21
+ #
22
+ # @return [#emit, #start, #finish] concrete adapter instance
23
+ def adapter
24
+ return @adapter if @adapter
25
+
26
+ TIMEx.config.telemetry_adapter || (@null_adapter ||= Adapters::Null.new)
27
+ end
28
+
29
+ # Forces a specific adapter (overrides {Configuration#telemetry_adapter} until cleared).
30
+ #
31
+ # @param value [#emit, #start, #finish, nil]
32
+ # @return [#emit, #start, #finish, nil]
33
+ def adapter=(value)
34
+ @adapter = value
35
+ end
36
+
37
+ # When +true+, adapter errors propagate instead of being swallowed by {#instrument} / {#emit}.
38
+ #
39
+ # @return [Boolean]
40
+ attr_accessor :strict
41
+
42
+ # Clears memoized adapter state and resets {#strict} to +false+.
43
+ #
44
+ # @return [void]
45
+ def reset!
46
+ @adapter = nil
47
+ @null_adapter = nil
48
+ @strict = false
49
+ end
50
+
51
+ # @return [Boolean] +true+ when the resolved adapter is {Adapters::Null}
52
+ #
53
+ # @note Lets hot paths skip kwarg-heavy instrumentation when telemetry is disabled.
54
+ def null_adapter?
55
+ adapter.is_a?(Adapters::Null)
56
+ end
57
+
58
+ # Emits a one-shot event (no span pairing).
59
+ #
60
+ # @param event [Symbol, String] logical event name
61
+ # @param payload [Hash{Symbol => Object}] structured attributes
62
+ # @return [void]
63
+ def emit(event:, **payload)
64
+ a = adapter
65
+ return if a.is_a?(Adapters::Null)
66
+
67
+ safely { a.emit(event:, payload:) }
68
+ end
69
+
70
+ # Wraps a block with +start+ / +finish+ callbacks and elapsed timing in +payload+.
71
+ #
72
+ # @param event [Symbol, String] logical event name
73
+ # @param base_payload [Hash{Symbol => Object}] initial payload (mutated; see @note)
74
+ # @yieldparam payload [Hash{Symbol => Object}] same object passed to the adapter
75
+ # @return [Object] the block's return value
76
+ #
77
+ # @note Mutates +base_payload+ in place to avoid per-call +dup+ on the hot path.
78
+ def instrument(event:, **base_payload)
79
+ a = adapter
80
+ return yield(base_payload) if a.is_a?(Adapters::Null)
81
+
82
+ # `base_payload` is a fresh hash from the kwarg splat at the call site;
83
+ # the caller can't observe our mutations, so we skip the defensive dup
84
+ # to save an allocation per instrumented call.
85
+ payload = base_payload
86
+ started_ns = Clock.monotonic_ns
87
+ safely { a.start(event:, payload:) }
88
+ begin
89
+ result = yield(payload)
90
+ payload[:outcome] ||= :ok
91
+ result
92
+ rescue StandardError, Expired => e
93
+ payload[:outcome] ||= e.is_a?(Expired) ? :timeout : :error
94
+ payload[:error_class] = e.class.name unless e.is_a?(Expired)
95
+ raise
96
+ end
97
+ ensure
98
+ if started_ns
99
+ payload[:elapsed_ms] = (Clock.monotonic_ns - started_ns) / 1_000_000.0
100
+ safely { a.finish(event:, payload:) }
101
+ end
102
+ end
103
+
104
+ private
105
+
106
+ # @yield adapter callback
107
+ # @return [Object, nil]
108
+ def safely
109
+ yield
110
+ rescue StandardError
111
+ raise if @strict
112
+
113
+ nil
114
+ end
115
+
116
+ end
117
+ end
118
+
119
+ require_relative "telemetry/adapters"
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TIMEx
4
+ # Test helpers for controlling TIMEx time without real sleeps.
5
+ #
6
+ # Installs a {Clock::VirtualClock} on the current thread and exposes helpers to
7
+ # advance it from nested code.
8
+ #
9
+ # @see Clock::VirtualClock
10
+ module Test
11
+
12
+ extend self
13
+
14
+ # Installs a {Clock::VirtualClock} for the block and yields it for manual control.
15
+ #
16
+ # @param start_ns [Integer] initial monotonic nanoseconds for the virtual clock
17
+ # @yieldparam clock [Clock::VirtualClock] the installed virtual clock
18
+ # @return [Object] the block's return value
19
+ #
20
+ # @note Also sets +:timex_test_clock+ so {.advance} can find the active clock
21
+ # without threading the object through call sites.
22
+ def with_virtual_clock(start_ns: 0)
23
+ previous_clock = Thread.current.thread_variable_get(:timex_clock)
24
+ previous_test_clock = Thread.current.thread_variable_get(:timex_test_clock)
25
+ clock = Clock::VirtualClock.new(monotonic_ns: start_ns)
26
+ Thread.current.thread_variable_set(:timex_clock, clock)
27
+ Thread.current.thread_variable_set(:timex_test_clock, clock)
28
+ yield clock
29
+ ensure
30
+ Thread.current.thread_variable_set(:timex_clock, previous_clock)
31
+ Thread.current.thread_variable_set(:timex_test_clock, previous_test_clock)
32
+ end
33
+
34
+ # Advances the active {Clock::VirtualClock} by +seconds+.
35
+ #
36
+ # @param seconds [Numeric] delta in seconds
37
+ # @return [Clock::VirtualClock] the advanced clock
38
+ # @raise [RuntimeError] when called outside {.with_virtual_clock}
39
+ def advance(seconds)
40
+ clock = Thread.current.thread_variable_get(:timex_test_clock) ||
41
+ raise("call inside TIMEx::Test.with_virtual_clock { ... }")
42
+ clock.advance(seconds)
43
+ end
44
+
45
+ # @see .with_virtual_clock
46
+ def freeze_time(&)
47
+ with_virtual_clock(&)
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TIMEx
4
+ # Shared +on_timeout:+ dispatcher included by strategies and composers.
5
+ #
6
+ # Centralizes semantics for +:raise+, +:raise_standard+, +:return_nil+,
7
+ # +:result+, and custom +Proc+ handlers so layers cannot drift.
8
+ #
9
+ # @see ON_TIMEOUT_SYMBOLS
10
+ # @see Expired
11
+ # @see Result.timeout
12
+ module TimeoutHandling
13
+
14
+ private
15
+
16
+ # Dispatches an {Expired} according to +on_timeout+.
17
+ #
18
+ # @param on_timeout [Symbol, Proc] mode or custom handler
19
+ # @param expired [Expired] the deadline exception
20
+ # @return [Object, nil] handler return value; may raise
21
+ # @raise [Expired] when +on_timeout == :raise+
22
+ # @raise [TimeoutError] when +on_timeout == :raise_standard+
23
+ # @raise [ArgumentError] when +on_timeout+ is unknown
24
+ def handle_timeout(on_timeout, expired)
25
+ case on_timeout
26
+ when :raise then raise expired
27
+ when :raise_standard then raise TimeoutError.from(expired)
28
+ when :return_nil then nil
29
+ when :result then Result.timeout(strategy: self.class.name_symbol, expired:)
30
+ when Proc then on_timeout.call(expired)
31
+ else
32
+ raise ArgumentError,
33
+ "unknown on_timeout: #{on_timeout.inspect} " \
34
+ "(expected one of #{ON_TIMEOUT_SYMBOLS.inspect}, or a Proc)"
35
+ end
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TIMEx
4
+
5
+ # Gem version. Bumped on release; mirrored in the gemspec.
6
+ VERSION = "0.1.0"
7
+
8
+ end
data/lib/timex.rb ADDED
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TIMEx
4
+
5
+ Error = Class.new(StandardError)
6
+ ConfigurationError = Class.new(Error)
7
+ StrategyNotFoundError = Class.new(Error)
8
+
9
+ extend self
10
+
11
+ # Primary entrypoint: runs +block+ under +deadline_or_seconds+ using the resolved strategy.
12
+ #
13
+ # @param deadline_or_seconds [Deadline, Numeric, Time, nil] budget or deadline
14
+ # @param strategy [Symbol, #call, nil] registered key, callable, or +nil+ for default
15
+ # @param auto_check [Boolean, nil] +nil+ uses {Configuration#auto_check_default}
16
+ # @param on_timeout [Symbol, Proc, nil] +nil+ uses {Configuration#default_on_timeout}
17
+ # @param opts [Hash{Symbol => Object}] forwarded to the strategy +#call+
18
+ # @yieldparam deadline [Deadline] coerced deadline passed to the inner block
19
+ # @return [Object] strategy/composer result (including timeout handler results)
20
+ # @raise [ArgumentError] when no block is given
21
+ def call(deadline_or_seconds, strategy: nil, auto_check: nil, on_timeout: nil, **opts, &block)
22
+ raise ArgumentError, "block required" unless block
23
+
24
+ deadline = Deadline.coerce(deadline_or_seconds)
25
+ strategy = Registry.resolve_for_call(strategy)
26
+ cfg = config
27
+ on_timeout ||= cfg.default_on_timeout
28
+ auto_check = cfg.auto_check_default if auto_check.nil?
29
+ runner =
30
+ if auto_check
31
+ ->(d) { TIMEx::AutoCheck.run(d) { yield(d) } }
32
+ else
33
+ block
34
+ end
35
+
36
+ strategy.call(
37
+ deadline:,
38
+ on_timeout:,
39
+ **opts,
40
+ &runner
41
+ )
42
+ end
43
+ alias deadline call
44
+
45
+ end
46
+
47
+ require_relative "timex/version"
48
+ require_relative "timex/on_timeout"
49
+ require_relative "timex/named_component"
50
+ require_relative "timex/expired"
51
+ require_relative "timex/configuration"
52
+ require_relative "timex/clock"
53
+ require_relative "timex/telemetry"
54
+ require_relative "timex/deadline"
55
+ require_relative "timex/result"
56
+ require_relative "timex/cancellation_token"
57
+ require_relative "timex/registry"
58
+ require_relative "timex/timeout_handling"
59
+ require_relative "timex/strategies/base"
60
+ require_relative "timex/strategies/cooperative"
61
+ require_relative "timex/strategies/io"
62
+ require_relative "timex/strategies/unsafe"
63
+ require_relative "timex/strategies/wakeup"
64
+ require_relative "timex/strategies/closeable"
65
+ require_relative "timex/strategies/subprocess"
66
+ require_relative "timex/strategies/ractor"
67
+ require_relative "timex/composers/base"
68
+ require_relative "timex/composers/two_phase"
69
+ require_relative "timex/composers/hedged"
70
+ require_relative "timex/composers/adaptive"
71
+ require_relative "timex/auto_check"
72
+ require_relative "timex/propagation/http_header"
73
+ require_relative "timex/propagation/rack_middleware"
74
+ require_relative "timex/test/virtual_clock"
75
+
76
+ require_relative "generators/timex/install_generator" if defined?(Rails::Generators)
77
+
78
+ # Rails integration is opt-in via the install generator initializer; a Railtie
79
+ # hook is not loaded from this file to keep the core gem free of Rails deps.