phronomy 0.7.0 → 0.8.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 (134) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +8 -7
  3. data/CHANGELOG.md +151 -1
  4. data/README.md +170 -47
  5. data/Rakefile +33 -0
  6. data/benchmark/baseline.json +1 -1
  7. data/benchmark/bench_context_assembler.rb +2 -2
  8. data/benchmark/bench_regression.rb +6 -5
  9. data/benchmark/bench_token_estimator.rb +5 -5
  10. data/benchmark/bench_tool_schema.rb +1 -1
  11. data/benchmark/bench_vector_store.rb +1 -1
  12. data/docs/decisions/004-invoke-timeout-is-not-cancellation.md +24 -0
  13. data/docs/decisions/006-no-built-in-guardrails.md +20 -2
  14. data/docs/decisions/010-cooperative-first-concurrency.md +248 -0
  15. data/lib/phronomy/agent/base.rb +285 -137
  16. data/lib/phronomy/agent/checkpoint.rb +118 -0
  17. data/lib/phronomy/agent/concerns/suspendable.rb +15 -0
  18. data/lib/phronomy/agent/context/conversation/compaction_context.rb +117 -0
  19. data/lib/phronomy/agent/context/conversation/trigger_context.rb +43 -0
  20. data/lib/phronomy/agent/context/conversation/trim_context.rb +82 -0
  21. data/lib/phronomy/agent/context/instruction/prompt_template.rb +102 -0
  22. data/lib/phronomy/agent/context/knowledge/embeddings/base.rb +45 -0
  23. data/lib/phronomy/agent/context/knowledge/embeddings/ruby_llm_embeddings.rb +51 -0
  24. data/lib/phronomy/agent/context/knowledge/loader/base.rb +31 -0
  25. data/lib/phronomy/agent/context/knowledge/loader/csv_loader.rb +62 -0
  26. data/lib/phronomy/agent/context/knowledge/loader/markdown_loader.rb +82 -0
  27. data/lib/phronomy/agent/context/knowledge/loader/plain_text_loader.rb +28 -0
  28. data/lib/phronomy/agent/context/knowledge/source/base.rb +60 -0
  29. data/lib/phronomy/agent/context/knowledge/source/entity_knowledge.rb +102 -0
  30. data/lib/phronomy/agent/context/knowledge/source/rag_knowledge.rb +63 -0
  31. data/lib/phronomy/agent/context/knowledge/source/static_knowledge.rb +58 -0
  32. data/lib/phronomy/agent/context/knowledge/splitter/base.rb +53 -0
  33. data/lib/phronomy/agent/context/knowledge/splitter/fixed_size_splitter.rb +57 -0
  34. data/lib/phronomy/agent/context/knowledge/splitter/recursive_splitter.rb +111 -0
  35. data/lib/phronomy/agent/context/knowledge/vector_store/async_backend.rb +116 -0
  36. data/lib/phronomy/agent/context/knowledge/vector_store/base.rb +95 -0
  37. data/lib/phronomy/agent/context/knowledge/vector_store/in_memory.rb +109 -0
  38. data/lib/phronomy/agent/context/knowledge/vector_store/pgvector.rb +133 -0
  39. data/lib/phronomy/agent/context/knowledge/vector_store/redis_search.rb +198 -0
  40. data/lib/phronomy/agent/fsm.rb +42 -65
  41. data/lib/phronomy/agent/invocation_pipeline.rb +99 -0
  42. data/lib/phronomy/agent/lifecycle/fsm_session.rb +251 -0
  43. data/lib/phronomy/agent/lifecycle/phase_machine_builder.rb +249 -0
  44. data/lib/phronomy/agent/react_agent.rb +27 -14
  45. data/lib/phronomy/agent/runner.rb +2 -2
  46. data/lib/phronomy/agent/tool_executor.rb +108 -0
  47. data/lib/phronomy/concurrency/async_queue.rb +157 -0
  48. data/lib/phronomy/concurrency/blocking_adapter_pool.rb +443 -0
  49. data/lib/phronomy/concurrency/cancellation_scope.rb +125 -0
  50. data/lib/phronomy/concurrency/cancellation_token.rb +140 -0
  51. data/lib/phronomy/concurrency/concurrency_gate.rb +157 -0
  52. data/lib/phronomy/concurrency/deadline.rb +65 -0
  53. data/lib/phronomy/concurrency/gate_registry.rb +52 -0
  54. data/lib/phronomy/concurrency/pool_registry.rb +57 -0
  55. data/lib/phronomy/configuration.rb +142 -0
  56. data/lib/phronomy/context.rb +2 -8
  57. data/lib/phronomy/diagnostics.rb +62 -0
  58. data/lib/phronomy/embeddings.rb +2 -2
  59. data/lib/phronomy/eval/runner.rb +13 -9
  60. data/lib/phronomy/eval/scorer/llm_judge.rb +12 -1
  61. data/lib/phronomy/event_loop.rb +184 -46
  62. data/lib/phronomy/guardrail/prompt_injection_guardrail.rb +58 -0
  63. data/lib/phronomy/invocation_context.rb +152 -0
  64. data/lib/phronomy/knowledge_source.rb +0 -5
  65. data/lib/phronomy/llm_adapter/base.rb +104 -0
  66. data/lib/phronomy/llm_adapter/ruby_llm.rb +47 -0
  67. data/lib/phronomy/llm_adapter.rb +20 -0
  68. data/lib/phronomy/{context → llm_context_window}/assembler.rb +18 -3
  69. data/lib/phronomy/{context → llm_context_window}/context_version_cache.rb +1 -1
  70. data/lib/phronomy/{context → llm_context_window}/token_budget.rb +7 -4
  71. data/lib/phronomy/{context → llm_context_window}/token_estimator.rb +3 -3
  72. data/lib/phronomy/loader.rb +4 -4
  73. data/lib/phronomy/metrics.rb +38 -0
  74. data/lib/phronomy/{agent → multi_agent}/handoff.rb +2 -2
  75. data/lib/phronomy/{agent → multi_agent}/orchestrator.rb +151 -126
  76. data/lib/phronomy/multi_agent/parallel_tool_chat.rb +149 -0
  77. data/lib/phronomy/{agent → multi_agent}/team_coordinator.rb +2 -2
  78. data/lib/phronomy/runtime/deterministic_scheduler.rb +412 -0
  79. data/lib/phronomy/runtime/fake_scheduler.rb +165 -0
  80. data/lib/phronomy/runtime/runtime_metrics.rb +117 -0
  81. data/lib/phronomy/runtime/scheduler.rb +98 -0
  82. data/lib/phronomy/runtime/scheduler_timer_adapter.rb +79 -0
  83. data/lib/phronomy/runtime/task_registry.rb +48 -0
  84. data/lib/phronomy/runtime/thread_scheduler.rb +30 -0
  85. data/lib/phronomy/runtime/timer_queue.rb +106 -0
  86. data/lib/phronomy/runtime/timer_service.rb +42 -0
  87. data/lib/phronomy/runtime.rb +389 -0
  88. data/lib/phronomy/splitter.rb +3 -3
  89. data/lib/phronomy/task/backend.rb +80 -0
  90. data/lib/phronomy/task/fiber_backend.rb +157 -0
  91. data/lib/phronomy/task/immediate_backend.rb +89 -0
  92. data/lib/phronomy/task/thread_backend.rb +84 -0
  93. data/lib/phronomy/task.rb +275 -0
  94. data/lib/phronomy/task_group.rb +265 -0
  95. data/lib/phronomy/testing/fake_clock.rb +109 -0
  96. data/lib/phronomy/testing/fake_scheduler.rb +104 -0
  97. data/lib/phronomy/testing/scheduler_helpers.rb +59 -0
  98. data/lib/phronomy/testing.rb +12 -0
  99. data/lib/phronomy/tool/base.rb +156 -7
  100. data/lib/phronomy/tool/mcp_tool.rb +47 -16
  101. data/lib/phronomy/tool/scope_policy.rb +50 -0
  102. data/lib/phronomy/tracing/null_tracer.rb +3 -1
  103. data/lib/phronomy/tracing/open_telemetry_tracer.rb +34 -0
  104. data/lib/phronomy/vector_store.rb +2 -2
  105. data/lib/phronomy/version.rb +1 -1
  106. data/lib/phronomy/workflow.rb +52 -5
  107. data/lib/phronomy/workflow_context.rb +37 -2
  108. data/lib/phronomy/workflow_runner.rb +28 -77
  109. data/lib/phronomy.rb +43 -0
  110. metadata +73 -33
  111. data/lib/phronomy/agent/parallel_tool_chat.rb +0 -92
  112. data/lib/phronomy/cancellation_token.rb +0 -92
  113. data/lib/phronomy/context/compaction_context.rb +0 -111
  114. data/lib/phronomy/context/trigger_context.rb +0 -39
  115. data/lib/phronomy/context/trim_context.rb +0 -75
  116. data/lib/phronomy/embeddings/base.rb +0 -22
  117. data/lib/phronomy/embeddings/ruby_llm_embeddings.rb +0 -45
  118. data/lib/phronomy/fsm_session.rb +0 -201
  119. data/lib/phronomy/knowledge_source/base.rb +0 -36
  120. data/lib/phronomy/knowledge_source/entity_knowledge.rb +0 -96
  121. data/lib/phronomy/knowledge_source/rag_knowledge.rb +0 -57
  122. data/lib/phronomy/knowledge_source/static_knowledge.rb +0 -52
  123. data/lib/phronomy/loader/base.rb +0 -25
  124. data/lib/phronomy/loader/csv_loader.rb +0 -56
  125. data/lib/phronomy/loader/markdown_loader.rb +0 -76
  126. data/lib/phronomy/loader/plain_text_loader.rb +0 -22
  127. data/lib/phronomy/prompt_template.rb +0 -96
  128. data/lib/phronomy/splitter/base.rb +0 -47
  129. data/lib/phronomy/splitter/fixed_size_splitter.rb +0 -51
  130. data/lib/phronomy/splitter/recursive_splitter.rb +0 -105
  131. data/lib/phronomy/vector_store/base.rb +0 -82
  132. data/lib/phronomy/vector_store/in_memory.rb +0 -93
  133. data/lib/phronomy/vector_store/pgvector.rb +0 -127
  134. data/lib/phronomy/vector_store/redis_search.rb +0 -192
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ class Runtime
5
+ # Abstract base class for Runtime scheduler backends.
6
+ #
7
+ # A scheduler is responsible for turning +Runtime#spawn+ calls into
8
+ # runnable {Task} objects. Concrete subclasses decide whether tasks
9
+ # execute on threads, Fibers, or some other execution primitive.
10
+ #
11
+ # The interface is intentionally minimal: callers only see {Task}
12
+ # objects and never interact with the scheduler directly.
13
+ class Scheduler
14
+ # Thread-local key under which the active scheduler is stored.
15
+ # Shared with {Task::FiberBackend} (same symbol).
16
+ # @api private
17
+ SCHEDULER_KEY = :phronomy_deterministic_scheduler
18
+
19
+ # Returns the scheduler currently dispatching on this OS thread, or +nil+
20
+ # when running outside a cooperative (Fiber-based) scheduler context.
21
+ #
22
+ # Uses +Thread#thread_variable_get+ (not +Thread#[]+) so that the value is
23
+ # visible across all Fibers running on the same OS thread.
24
+ #
25
+ # @return [Scheduler, nil]
26
+ # @api private
27
+ def self.current
28
+ Thread.current.thread_variable_get(SCHEDULER_KEY)
29
+ end
30
+
31
+ # Creates a new scheduler-aware signal object for this scheduler.
32
+ # Signalling primitives use this instead of +ConditionVariable+ when a
33
+ # cooperative scheduler is active.
34
+ #
35
+ # Default implementation raises +NotImplementedError+. Subclasses that
36
+ # support cooperative suspension (e.g. {DeterministicScheduler}) must
37
+ # override this.
38
+ #
39
+ # @return [Object] an opaque signal handle understood by {#wait_for_signal}
40
+ # and {#raise_signal}
41
+ # @api private
42
+ def new_signal
43
+ raise NotImplementedError, "#{self.class}#new_signal is not implemented"
44
+ end
45
+
46
+ # Suspends the current execution unit (Fiber or Thread) until +signal+ is
47
+ # raised via {#raise_signal}.
48
+ #
49
+ # @param signal [Object] signal handle returned by {#new_signal}
50
+ # @return [void]
51
+ # @api private
52
+ def wait_for_signal(signal)
53
+ raise NotImplementedError, "#{self.class}#wait_for_signal is not implemented"
54
+ end
55
+
56
+ # Wakes up one waiter suspended on +signal+.
57
+ #
58
+ # @param signal [Object] signal handle returned by {#new_signal}
59
+ # @return [void]
60
+ # @api private
61
+ def raise_signal(signal)
62
+ raise NotImplementedError, "#{self.class}#raise_signal is not implemented"
63
+ end
64
+
65
+ # Wakes up all waiters suspended on +signal+.
66
+ #
67
+ # @param signal [Object] signal handle returned by {#new_signal}
68
+ # @return [void]
69
+ # @api private
70
+ def raise_signal_all(signal)
71
+ raise NotImplementedError, "#{self.class}#raise_signal_all is not implemented"
72
+ end
73
+
74
+ # Spawns a new task.
75
+ #
76
+ # @param name [String, nil] optional human-readable label
77
+ # @param parent [Task, nil] parent task for cancellation propagation
78
+ # @yield block to execute concurrently (or synchronously, depending on
79
+ # the concrete scheduler)
80
+ # @return [Task]
81
+ # @api private
82
+ def spawn(name:, parent:, &block)
83
+ raise NotImplementedError, "#{self.class}#spawn is not implemented"
84
+ end
85
+
86
+ # Cooperative yield point.
87
+ #
88
+ # Default implementation is a no-op. Thread-based subclasses should
89
+ # override with +Thread.pass+; fiber-based subclasses should switch to the
90
+ # next runnable fiber.
91
+ # @return [void]
92
+ # @api private
93
+ def yield
94
+ # no-op by default; subclasses override
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ class Runtime
5
+ # A drop-in replacement for {TimerQueue} that delegates timer scheduling to
6
+ # a {DeterministicScheduler} instead of spawning a dedicated background OS thread.
7
+ #
8
+ # When a {Runtime} is backed by {DeterministicScheduler} (e.g. the +:fiber+
9
+ # runtime backend), {Runtime#timer_queue} returns an instance of this adapter
10
+ # rather than a {TimerQueue}. This eliminates the +phronomy-timer-queue+
11
+ # background thread for Fiber-based runtimes.
12
+ #
13
+ # Timer callbacks are fired during {DeterministicScheduler#run_until_idle}
14
+ # when {DeterministicScheduler#autorun?} is +true+ (i.e. the +:fiber+ backend).
15
+ # They can also be fired explicitly by calling
16
+ # {DeterministicScheduler#fire_real_timers}.
17
+ #
18
+ # == Known Limitation (Issue #331)
19
+ #
20
+ # Timers that require an actual wall-clock sleep (e.g. a deadline of 10 s
21
+ # from now that will not be reached until real time elapses) will not fire
22
+ # automatically: +run_until_idle+ does not block waiting for future deadlines.
23
+ # This is an accepted limitation of the current stepping-stone implementation.
24
+ # Full resolution requires integrating the cooperative scheduler with the
25
+ # {EventLoop} tick cycle so that a single event-loop iteration checks both
26
+ # ready Fibers and expired wall-clock timers.
27
+ #
28
+ # Use the +:thread+ runtime backend (default) for production workloads that
29
+ # depend on real-time deadline enforcement.
30
+ #
31
+ # @see DeterministicScheduler#schedule_real_after
32
+ # @see DeterministicScheduler#fire_real_timers
33
+ # Bridges wall-clock timers to the cooperative {DeterministicScheduler}.
34
+ #
35
+ # Registers a recurring timer callback with the scheduler's {TimerQueue}
36
+ # so that Fiber-based tasks can await real time without blocking OS threads.
37
+ # @api private
38
+ class SchedulerTimerAdapter
39
+ # @param scheduler [DeterministicScheduler]
40
+ # @api private
41
+ def initialize(scheduler)
42
+ @scheduler = scheduler
43
+ end
44
+
45
+ # Schedules a one-shot callback to fire after +seconds+ from now.
46
+ # Delegates to {DeterministicScheduler#schedule_real_after}.
47
+ #
48
+ # Raises {PoolShutdownError} after {#shutdown} has been called, matching
49
+ # the behaviour of {TimerQueue#schedule}.
50
+ #
51
+ # @param seconds [Numeric] delay before the callback fires
52
+ # @yield called when the deadline is reached
53
+ # @return [self]
54
+ # @api private
55
+ def schedule(seconds:, &callback)
56
+ raise Phronomy::PoolShutdownError, "SchedulerTimerAdapter has been shut down" if @stopped
57
+
58
+ @scheduler.schedule_real_after(seconds, &callback)
59
+ self
60
+ end
61
+
62
+ # No-op: there is no background thread to stop.
63
+ # Present for API compatibility with {TimerQueue}.
64
+ # @return [self]
65
+ # @api private
66
+ def shutdown
67
+ @stopped = true
68
+ self
69
+ end
70
+
71
+ # Returns the number of pending (not yet fired) callbacks.
72
+ # @return [Integer]
73
+ # @api private
74
+ def pending_count
75
+ @scheduler.pending_real_timer_count
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ class Runtime
5
+ # Internal registry of active {Task} instances for a {Runtime}.
6
+ #
7
+ # Tracks every task that has been spawned but not yet completed so that
8
+ # {Runtime#shutdown} can drain them. Tasks that complete synchronously
9
+ # (e.g. under {FakeScheduler} / {ImmediateBackend}) deregister themselves
10
+ # before the caller returns from {Runtime#spawn}, so they are never added
11
+ # to the registry in the first place.
12
+ # @api private
13
+ class TaskRegistry
14
+ def initialize
15
+ @mutex = Mutex.new
16
+ @tasks = []
17
+ end
18
+
19
+ # Adds +task+ to the registry unless it already completed synchronously.
20
+ # @param task [Task]
21
+ # @return [void]
22
+ # @api private
23
+ def register(task)
24
+ @mutex.synchronize { @tasks << task unless task.done? }
25
+ end
26
+
27
+ # Removes +task+ from the registry (called from the task's ensure block).
28
+ # @param task [Task]
29
+ # @return [void]
30
+ # @api private
31
+ def deregister(task)
32
+ @mutex.synchronize { @tasks.delete(task) }
33
+ end
34
+
35
+ # Waits for all registered tasks to finish (used by {Runtime#shutdown}).
36
+ # @return [void]
37
+ # @api private
38
+ def drain
39
+ tasks = @mutex.synchronize { @tasks.dup }
40
+ tasks.each do |t|
41
+ t.join
42
+ rescue
43
+ nil
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ class Runtime
5
+ # Thread-based scheduler: spawns each task in a new OS thread.
6
+ #
7
+ # This is the default scheduler used by {Runtime} in production.
8
+ # It delegates directly to {Task.spawn} with the default
9
+ # {Task::ThreadBackend}, preserving the pre-282 behaviour exactly.
10
+ # @api private
11
+ class ThreadScheduler < Scheduler
12
+ # Spawns +block+ as a new {Task} backed by a Thread.
13
+ #
14
+ # @param name [String, nil]
15
+ # @param parent [Task, nil]
16
+ # @return [Task]
17
+ # @api private
18
+ def spawn(name:, parent:, &block)
19
+ Task.spawn(name: name, parent: parent, &block)
20
+ end
21
+
22
+ # Yields the current thread's time slice to other runnable threads.
23
+ # @return [void]
24
+ # @api private
25
+ def yield
26
+ Thread.pass
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ class Runtime
5
+ # A thread-safe timer queue backed by a single background thread.
6
+ #
7
+ # Replaces the pattern of spawning one +Thread.new { sleep(t); callback }+
8
+ # per deadline. Any number of timers share a single background thread that
9
+ # sleeps until the earliest pending deadline.
10
+ #
11
+ # Use {#schedule} to register a one-shot callback; call {#shutdown} when the
12
+ # queue is no longer needed (e.g. on process exit) to stop the background
13
+ # thread cleanly.
14
+ class TimerQueue
15
+ # @param clock [#call] zero-argument callable that returns the current
16
+ # monotonic time in seconds (defaults to +Process::CLOCK_MONOTONIC+).
17
+ # Override in tests to inject a fake clock.
18
+ # @api private
19
+ def initialize(clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) })
20
+ @clock = clock
21
+ @heap = [] # [[fire_at, callback], ...]
22
+ @mutex = Mutex.new
23
+ @cond = ConditionVariable.new
24
+ @stopped = false
25
+ @thread = Thread.new { run_loop }
26
+ @thread.name = "phronomy-timer-queue"
27
+ end
28
+
29
+ # Schedule a one-shot callback to fire after +seconds+ from now.
30
+ #
31
+ # @param seconds [Numeric] delay before the callback fires
32
+ # @yield called (in the timer thread) when the deadline is reached
33
+ # @return [self]
34
+ # @api private
35
+ def schedule(seconds:, &callback)
36
+ fire_at = @clock.call + seconds.to_f
37
+ @mutex.synchronize do
38
+ raise Phronomy::PoolShutdownError, "TimerQueue has been shut down" if @stopped
39
+ insert_sorted(fire_at, callback)
40
+ @cond.signal
41
+ end
42
+ self
43
+ end
44
+
45
+ # Stop the background thread. Pending (un-fired) callbacks are discarded.
46
+ #
47
+ # @return [self]
48
+ # @api private
49
+ def shutdown
50
+ @mutex.synchronize do
51
+ @stopped = true
52
+ @cond.signal
53
+ end
54
+ @thread.join
55
+ self
56
+ end
57
+
58
+ # Number of pending (not yet fired) callbacks. Primarily for testing.
59
+ # @return [Integer]
60
+ # @api private
61
+ def pending_count
62
+ @mutex.synchronize { @heap.size }
63
+ end
64
+
65
+ private
66
+
67
+ def insert_sorted(fire_at, callback)
68
+ @heap << [fire_at, callback]
69
+ @heap.sort_by! { |(t, _)| t }
70
+ end
71
+
72
+ def run_loop
73
+ loop do
74
+ callback = next_callback
75
+ break if callback == :stopped
76
+ begin
77
+ callback&.call
78
+ rescue => e
79
+ Phronomy.configuration.logger&.error { "[TimerQueue] callback raised #{e.class}: #{e.message}" }
80
+ end
81
+ end
82
+ end
83
+
84
+ def next_callback
85
+ @mutex.synchronize do
86
+ loop do
87
+ return :stopped if @stopped
88
+
89
+ if @heap.empty?
90
+ @cond.wait(@mutex)
91
+ else
92
+ now = @clock.call
93
+ fire_at, = @heap.first
94
+ if fire_at <= now
95
+ return @heap.shift[1]
96
+ else
97
+ remaining = fire_at - now
98
+ @cond.wait(@mutex, remaining)
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ class Runtime
5
+ # Lazy-initialised timer service for a {Runtime} instance.
6
+ #
7
+ # Returns a {SchedulerTimerAdapter} when the backing scheduler is a
8
+ # {DeterministicScheduler} (enabling virtual-time integration for the
9
+ # `:fiber` backend), or a standard {TimerQueue} (OS-thread backed) for all
10
+ # other schedulers.
11
+ # @api private
12
+ class TimerService
13
+ # @param scheduler [Scheduler]
14
+ # @api private
15
+ def initialize(scheduler)
16
+ @scheduler = scheduler
17
+ @mutex = Mutex.new
18
+ @timer = nil
19
+ end
20
+
21
+ # Returns (or lazily creates) the timer queue for this runtime.
22
+ # @return [TimerQueue, SchedulerTimerAdapter]
23
+ # @api private
24
+ def timer_queue
25
+ @mutex.synchronize do
26
+ @timer ||= if @scheduler.is_a?(DeterministicScheduler)
27
+ SchedulerTimerAdapter.new(@scheduler)
28
+ else
29
+ TimerQueue.new
30
+ end
31
+ end
32
+ end
33
+
34
+ # Shuts down the timer queue if it was started.
35
+ # @return [void]
36
+ # @api private
37
+ def shutdown
38
+ @mutex.synchronize { @timer&.shutdown }
39
+ end
40
+ end
41
+ end
42
+ end