phronomy 0.17.0 → 0.19.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +122 -1134
- data/README.md +70 -1280
- data/benchmark/bench_regression.rb +25 -5
- data/docs/changelog/0.14-and-earlier.md +1137 -0
- data/docs/decisions/008-orchestrator-uses-os-threads.md +46 -48
- data/docs/decisions/009-state-store-abstraction.md +1 -1
- data/docs/decisions/010-cooperative-first-concurrency.md +155 -235
- data/docs/decisions/014-unified-persistence-durable-state.md +268 -0
- data/docs/features.md +93 -0
- data/docs/getting-started.md +387 -0
- data/docs/migrations/0.15.md +35 -0
- data/docs/migrations/0.16.md +43 -0
- data/docs/migrations/0.19.md +148 -0
- data/docs/runtime-and-concurrency.md +349 -0
- data/examples/workflows/generic_task_event_mapping.rb +14 -6
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +2 -2
- data/lib/phronomy/agent/async_event_api.rb +3 -3
- data/lib/phronomy/agent/base.rb +116 -55
- data/lib/phronomy/agent/context/capability/base.rb +13 -3
- data/lib/phronomy/agent/context_assembler.rb +13 -3
- data/lib/phronomy/agent/execution_coordinator.rb +423 -252
- data/lib/phronomy/agent/journal_projection.rb +5 -1
- data/lib/phronomy/agent/shared_state.rb +2 -0
- data/lib/phronomy/agent/tool_executor.rb +29 -71
- data/lib/phronomy/agent/tool_invocation.rb +97 -47
- data/lib/phronomy/agent/tool_invocation_session_builder.rb +55 -161
- data/lib/phronomy/configuration.rb +7 -30
- data/lib/phronomy/diagnostics.rb +12 -41
- data/lib/phronomy/engine/concurrency/async_queue.rb +5 -188
- data/lib/phronomy/engine/concurrency/cancellation_scope.rb +6 -7
- data/lib/phronomy/engine/concurrency/cancellation_token.rb +48 -3
- data/lib/phronomy/engine/concurrency/deadline.rb +2 -3
- data/lib/phronomy/engine/concurrency/offload_pool.rb +696 -0
- data/lib/phronomy/engine/concurrency/pool_registry.rb +5 -5
- data/lib/phronomy/engine/event_loop.rb +170 -193
- data/lib/phronomy/engine/fsm_session.rb +6 -4
- data/lib/phronomy/engine/runtime/timer_queue.rb +48 -71
- data/lib/phronomy/engine/runtime/timer_service.rb +13 -21
- data/lib/phronomy/engine/runtime.rb +52 -158
- data/lib/phronomy/engine/task.rb +136 -277
- data/lib/phronomy/llm_adapter/base.rb +14 -14
- data/lib/phronomy/llm_adapter/ruby_llm.rb +3 -4
- data/lib/phronomy/llm_adapter.rb +2 -2
- data/lib/phronomy/metrics.rb +15 -30
- data/lib/phronomy/multi_agent/fan_out_invocation.rb +146 -0
- data/lib/phronomy/multi_agent/fan_out_session_builder.rb +125 -0
- data/lib/phronomy/multi_agent/handoff.rb +1 -0
- data/lib/phronomy/multi_agent/orchestrator.rb +147 -99
- data/lib/phronomy/multi_agent/team_coordinator.rb +2 -0
- data/lib/phronomy/persistence/in_memory.rb +113 -8
- data/lib/phronomy/persistence.rb +12 -3
- data/lib/phronomy/testing/eval/comparison.rb +23 -0
- data/lib/phronomy/testing/eval/dataset.rb +27 -0
- data/lib/phronomy/testing/eval/eval_case.rb +13 -0
- data/lib/phronomy/testing/eval/eval_result.rb +16 -0
- data/lib/phronomy/testing/eval/metrics.rb +43 -0
- data/lib/phronomy/testing/eval/runner.rb +52 -0
- data/lib/phronomy/testing/eval/scorer/base.rb +15 -0
- data/lib/phronomy/testing/eval/scorer/exact_match.rb +25 -0
- data/lib/phronomy/testing/eval/scorer/includes_scorer.rb +25 -0
- data/lib/phronomy/testing/eval/scorer/llm_judge.rb +46 -0
- data/lib/phronomy/testing/eval/scorer.rb +10 -0
- data/lib/phronomy/testing/eval.rb +9 -0
- data/lib/phronomy/testing/fake_clock.rb +6 -53
- data/lib/phronomy/testing.rb +2 -6
- data/lib/phronomy/tools/agent.rb +141 -6
- data/lib/phronomy/vector_store/async_backend.rb +21 -17
- data/lib/phronomy/vector_store/base.rb +2 -2
- data/lib/phronomy/vector_store/embeddings/base.rb +6 -5
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow.rb +10 -9
- data/lib/phronomy/workflow_runner.rb +363 -99
- data/lib/phronomy.rb +10 -4
- data/scripts/api_snapshot.rb +4 -4
- metadata +25 -33
- data/lib/phronomy/engine/concurrency/blocking_adapter_pool.rb +0 -561
- data/lib/phronomy/engine/runtime/deterministic_scheduler.rb +0 -439
- data/lib/phronomy/engine/runtime/fake_scheduler.rb +0 -165
- data/lib/phronomy/engine/runtime/runtime_metrics.rb +0 -116
- data/lib/phronomy/engine/runtime/scheduler.rb +0 -98
- data/lib/phronomy/engine/runtime/scheduler_timer_adapter.rb +0 -79
- data/lib/phronomy/engine/runtime/task_registry.rb +0 -95
- data/lib/phronomy/engine/runtime/thread_scheduler.rb +0 -30
- data/lib/phronomy/engine/task/backend.rb +0 -80
- data/lib/phronomy/engine/task/deferred_backend.rb +0 -73
- data/lib/phronomy/engine/task/fiber_backend.rb +0 -157
- data/lib/phronomy/engine/task/immediate_backend.rb +0 -89
- data/lib/phronomy/engine/task/mapped_backend.rb +0 -90
- data/lib/phronomy/engine/task/thread_backend.rb +0 -84
- data/lib/phronomy/engine/task_group.rb +0 -193
- data/lib/phronomy/eval/comparison.rb +0 -47
- data/lib/phronomy/eval/dataset.rb +0 -45
- data/lib/phronomy/eval/eval_case.rb +0 -17
- data/lib/phronomy/eval/eval_result.rb +0 -29
- data/lib/phronomy/eval/metrics.rb +0 -66
- data/lib/phronomy/eval/runner.rb +0 -94
- data/lib/phronomy/eval/scorer/base.rb +0 -22
- data/lib/phronomy/eval/scorer/exact_match.rb +0 -31
- data/lib/phronomy/eval/scorer/includes_scorer.rb +0 -32
- data/lib/phronomy/eval/scorer/llm_judge.rb +0 -72
- data/lib/phronomy/eval/scorer.rb +0 -9
- data/lib/phronomy/eval.rb +0 -7
- data/lib/phronomy/state_store/base.rb +0 -48
- data/lib/phronomy/state_store/in_memory.rb +0 -62
- data/lib/phronomy/testing/fake_scheduler.rb +0 -104
- data/lib/phronomy/testing/scheduler_helpers.rb +0 -68
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
class Runtime
|
|
5
|
-
# Internal store for task-centric counters and latency samples.
|
|
6
|
-
#
|
|
7
|
-
# All access is mutex-protected. The ring buffers for wait/run times are
|
|
8
|
-
# bounded to {WINDOW} samples so that long-lived runtimes do not grow
|
|
9
|
-
# unbounded.
|
|
10
|
-
# @api private
|
|
11
|
-
class RuntimeMetrics
|
|
12
|
-
WINDOW = 1000
|
|
13
|
-
private_constant :WINDOW
|
|
14
|
-
|
|
15
|
-
def initialize
|
|
16
|
-
@mutex = Mutex.new
|
|
17
|
-
@active_by_type = Hash.new(0)
|
|
18
|
-
@wait_ms = []
|
|
19
|
-
@run_ms = []
|
|
20
|
-
@cancelled = Hash.new(0)
|
|
21
|
-
@failed = Hash.new(0)
|
|
22
|
-
@starvation_count = 0
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# Records that a new task of +type+ has been spawned.
|
|
26
|
-
# @param type [Symbol]
|
|
27
|
-
# @return [void]
|
|
28
|
-
# @api private
|
|
29
|
-
def record_start(type)
|
|
30
|
-
@mutex.synchronize { @active_by_type[type] += 1 }
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# Appends a wait-time sample (milliseconds from spawn to start).
|
|
34
|
-
# @param wait_ms [Float]
|
|
35
|
-
# @return [void]
|
|
36
|
-
# @api private
|
|
37
|
-
def record_wait(wait_ms)
|
|
38
|
-
@mutex.synchronize do
|
|
39
|
-
@wait_ms << wait_ms
|
|
40
|
-
@wait_ms.shift if @wait_ms.size > WINDOW
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
# Records completion of a task (decrements active count, appends run time).
|
|
45
|
-
# @param type [Symbol]
|
|
46
|
-
# @param outcome [:completed, :cancelled, :failed]
|
|
47
|
-
# @param run_start_ms [Integer] monotonic millisecond timestamp from task start
|
|
48
|
-
# @return [void]
|
|
49
|
-
# @api private
|
|
50
|
-
def record_end(type, outcome, run_start_ms)
|
|
51
|
-
run_ms = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - run_start_ms
|
|
52
|
-
@mutex.synchronize do
|
|
53
|
-
@active_by_type[type] = [@active_by_type[type] - 1, 0].max
|
|
54
|
-
@run_ms << run_ms
|
|
55
|
-
@run_ms.shift if @run_ms.size > WINDOW
|
|
56
|
-
case outcome
|
|
57
|
-
when :cancelled then @cancelled[type] += 1
|
|
58
|
-
when :failed then @failed[type] += 1
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# Increments the CPU-starvation counter (task ran without yielding over
|
|
64
|
-
# the configured +blocking_detect_threshold_ms+ threshold).
|
|
65
|
-
# @return [void]
|
|
66
|
-
# @api private
|
|
67
|
-
def increment_starvation
|
|
68
|
-
@mutex.synchronize { @starvation_count += 1 }
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Returns the current starvation counter value.
|
|
72
|
-
# @return [Integer]
|
|
73
|
-
# @api private
|
|
74
|
-
def starvation_count
|
|
75
|
-
@mutex.synchronize { @starvation_count }
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# Returns the full task-centric metrics hash (see {Runtime#task_snapshot}).
|
|
79
|
-
# @return [Hash{Symbol => Numeric}]
|
|
80
|
-
# @api private
|
|
81
|
-
def snapshot
|
|
82
|
-
@mutex.synchronize do
|
|
83
|
-
active = @active_by_type.dup
|
|
84
|
-
wait = @wait_ms.dup
|
|
85
|
-
run = @run_ms.dup
|
|
86
|
-
cancelled = @cancelled.values.sum
|
|
87
|
-
failed = @failed.values.sum
|
|
88
|
-
starvation = @starvation_count
|
|
89
|
-
{
|
|
90
|
-
active_agent_tasks: active[:agent].to_i,
|
|
91
|
-
active_tool_tasks: active[:tool].to_i,
|
|
92
|
-
active_workflow_tasks: active[:workflow].to_i,
|
|
93
|
-
active_llm_tasks: active[:llm].to_i,
|
|
94
|
-
task_wait_time_p50_ms: _percentile(wait, 50),
|
|
95
|
-
task_wait_time_p95_ms: _percentile(wait, 95),
|
|
96
|
-
task_run_time_p50_ms: _percentile(run, 50),
|
|
97
|
-
task_run_time_p95_ms: _percentile(run, 95),
|
|
98
|
-
cancelled_tasks: cancelled,
|
|
99
|
-
failed_tasks: failed,
|
|
100
|
-
non_yield_threshold_violation_count: starvation
|
|
101
|
-
}
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
private
|
|
106
|
-
|
|
107
|
-
def _percentile(samples, pct)
|
|
108
|
-
return 0.0 if samples.empty?
|
|
109
|
-
|
|
110
|
-
sorted = samples.sort
|
|
111
|
-
idx = ((pct / 100.0) * (sorted.size - 1)).round
|
|
112
|
-
sorted[idx].round(3)
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
|
@@ -1,98 +0,0 @@
|
|
|
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
|
|
@@ -1,79 +0,0 @@
|
|
|
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
|
|
@@ -1,95 +0,0 @@
|
|
|
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
|
-
# deregister themselves before the caller returns from {Runtime#spawn}.
|
|
10
|
-
# @api private
|
|
11
|
-
class TaskRegistry
|
|
12
|
-
def initialize
|
|
13
|
-
@mutex = Mutex.new
|
|
14
|
-
@tasks = []
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
# Adds +task+ unless it already completed synchronously.
|
|
18
|
-
# @api private
|
|
19
|
-
def register(task)
|
|
20
|
-
@mutex.synchronize { @tasks << task unless task.done? }
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
# Removes +task+ from the registry.
|
|
24
|
-
# @api private
|
|
25
|
-
def deregister(task)
|
|
26
|
-
@mutex.synchronize { @tasks.delete(task) }
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Waits for registered tasks until the absolute monotonic +deadline+.
|
|
30
|
-
# The registry is re-snapshotted because tasks may create follow-up tasks
|
|
31
|
-
# while Runtime shutdown is draining accepted work.
|
|
32
|
-
#
|
|
33
|
-
# @param deadline [Numeric] absolute Process::CLOCK_MONOTONIC value
|
|
34
|
-
# @return [Symbol] +:empty+ or +:timeout+
|
|
35
|
-
# @api private
|
|
36
|
-
def drain_until(deadline)
|
|
37
|
-
loop do
|
|
38
|
-
tasks = snapshot
|
|
39
|
-
return :empty if tasks.empty?
|
|
40
|
-
|
|
41
|
-
tasks.each do |task|
|
|
42
|
-
remaining = deadline - monotonic_now
|
|
43
|
-
return :timeout if remaining <= 0
|
|
44
|
-
|
|
45
|
-
begin
|
|
46
|
-
task.join(remaining)
|
|
47
|
-
rescue
|
|
48
|
-
# Some backends re-raise the task error from join. The task's
|
|
49
|
-
# ensure block still deregisters it, so continue the drain.
|
|
50
|
-
nil
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
return :timeout if task.alive? && monotonic_now >= deadline
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
return :empty if empty?
|
|
57
|
-
return :timeout if monotonic_now >= deadline
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# Compatibility helper for callers that explicitly require an unbounded
|
|
62
|
-
# drain. Runtime#shutdown uses {#drain_until}.
|
|
63
|
-
# @api private
|
|
64
|
-
def drain
|
|
65
|
-
snapshot.each do |task|
|
|
66
|
-
task.join
|
|
67
|
-
rescue
|
|
68
|
-
nil
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
# @return [Boolean]
|
|
73
|
-
# @api private
|
|
74
|
-
def empty?
|
|
75
|
-
@mutex.synchronize { @tasks.empty? }
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# @return [Integer]
|
|
79
|
-
# @api private
|
|
80
|
-
def size
|
|
81
|
-
@mutex.synchronize { @tasks.size }
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
private
|
|
85
|
-
|
|
86
|
-
def snapshot
|
|
87
|
-
@mutex.synchronize { @tasks.dup }
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def monotonic_now
|
|
91
|
-
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
end
|
|
@@ -1,30 +0,0 @@
|
|
|
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
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
class Task
|
|
5
|
-
# Abstract base class for Task execution backends.
|
|
6
|
-
#
|
|
7
|
-
# A backend encapsulates the execution primitive (Thread, Fiber, etc.) and
|
|
8
|
-
# the lifecycle transitions it drives. Concrete backends must implement all
|
|
9
|
-
# abstract methods. The default concrete implementation is {ThreadBackend}.
|
|
10
|
-
#
|
|
11
|
-
# Backends receive a reference to the owning {Task} so they can call
|
|
12
|
-
# {Task#transition!} at the appropriate lifecycle points.
|
|
13
|
-
class Backend
|
|
14
|
-
# @param task [Task] the owning Task (used for status callbacks)
|
|
15
|
-
# @param block [Proc] the work to execute
|
|
16
|
-
# @api private
|
|
17
|
-
def initialize(task:, &block)
|
|
18
|
-
@task = task
|
|
19
|
-
@block = block
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
# Blocks until the task completes and returns its value.
|
|
23
|
-
# Re-raises errors from the block.
|
|
24
|
-
# @return [Object]
|
|
25
|
-
# @raise [Exception]
|
|
26
|
-
# @api private
|
|
27
|
-
def wait_result
|
|
28
|
-
raise NotImplementedError, "#{self.class}#wait_result not implemented"
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# Returns +true+ while execution is still ongoing.
|
|
32
|
-
# @return [Boolean]
|
|
33
|
-
# @api private
|
|
34
|
-
def alive?
|
|
35
|
-
raise NotImplementedError, "#{self.class}#alive? not implemented"
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# Requests cancellation.
|
|
39
|
-
# Thread-based backends may use +Thread#raise+; cooperative backends
|
|
40
|
-
# should mark the task cancelled and rely on {Task.checkpoint!}.
|
|
41
|
-
# @return [self]
|
|
42
|
-
# @api private
|
|
43
|
-
def cancel!
|
|
44
|
-
raise NotImplementedError, "#{self.class}#cancel! not implemented"
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Joins the execution context with an optional timeout.
|
|
48
|
-
# Returns +nil+ when a non-nil +limit+ expires before completion,
|
|
49
|
-
# matching +Thread#join+ semantics.
|
|
50
|
-
# @param limit [Numeric, nil]
|
|
51
|
-
# @return [Object, nil]
|
|
52
|
-
# @api private
|
|
53
|
-
def join(limit = nil)
|
|
54
|
-
raise NotImplementedError, "#{self.class}#join not implemented"
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Returns the task's result value once it has reached a terminal state.
|
|
58
|
-
# Only valid to call after the task is done.
|
|
59
|
-
# Subclasses should override if they store the result.
|
|
60
|
-
# @return [Object, nil]
|
|
61
|
-
# @api private
|
|
62
|
-
def completed_value
|
|
63
|
-
nil
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# Returns the exception raised by the task, or +nil+ on success/cancellation.
|
|
67
|
-
# Only valid to call after the task is done.
|
|
68
|
-
# Subclasses should override if they store errors.
|
|
69
|
-
# @return [Exception, nil]
|
|
70
|
-
# @api private
|
|
71
|
-
def completed_error
|
|
72
|
-
nil
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
private
|
|
76
|
-
|
|
77
|
-
attr_reader :task, :block
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
end
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "timeout"
|
|
4
|
-
|
|
5
|
-
module Phronomy
|
|
6
|
-
class Task
|
|
7
|
-
# Backend for externally-completed Tasks.
|
|
8
|
-
#
|
|
9
|
-
# DeferredBackend never starts a thread. The owner transitions it to a
|
|
10
|
-
# terminal state by calling Task#transition! and then #unblock.
|
|
11
|
-
# @api private
|
|
12
|
-
class DeferredBackend < Backend
|
|
13
|
-
def initialize(task:, &)
|
|
14
|
-
super
|
|
15
|
-
@done_queue = Queue.new
|
|
16
|
-
task.transition!(:running)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
# Unblocks await/join after the task reaches a terminal state.
|
|
20
|
-
# @api private
|
|
21
|
-
def unblock(value, error)
|
|
22
|
-
@done_queue.push([value, error])
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# Blocks until externally completed.
|
|
26
|
-
# @api private
|
|
27
|
-
def wait_result
|
|
28
|
-
scheduler = Thread.current.thread_variable_get(Task::SCHEDULER_KEY)
|
|
29
|
-
in_managed_fiber = !Fiber.respond_to?(:main) || Fiber.current != Fiber.main
|
|
30
|
-
if scheduler && in_managed_fiber && !@task.done?
|
|
31
|
-
scheduler.track_blocking_await
|
|
32
|
-
waiting_fiber = Fiber.current
|
|
33
|
-
@task.on_complete do |_value, _error|
|
|
34
|
-
scheduler.complete_blocking_await
|
|
35
|
-
scheduler.enqueue_fiber(-> { waiting_fiber.resume })
|
|
36
|
-
end
|
|
37
|
-
Fiber.yield(:cooperative_suspend)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
value, error = @done_queue.pop
|
|
41
|
-
raise error if error
|
|
42
|
-
|
|
43
|
-
value
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Deferred tasks have no execution thread of their own.
|
|
47
|
-
# @api private
|
|
48
|
-
def alive?
|
|
49
|
-
!@task.done?
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# Marks the task cancelled without interrupting external work.
|
|
53
|
-
# @api private
|
|
54
|
-
def cancel!
|
|
55
|
-
self
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Blocks until externally completed, with optional timeout.
|
|
59
|
-
# @api private
|
|
60
|
-
def join(limit = nil)
|
|
61
|
-
if limit.nil?
|
|
62
|
-
wait_result
|
|
63
|
-
else
|
|
64
|
-
begin
|
|
65
|
-
Timeout.timeout(limit) { wait_result }
|
|
66
|
-
rescue Timeout::Error
|
|
67
|
-
nil
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|