taski 0.4.2 → 0.7.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 +50 -0
- data/README.md +51 -33
- data/Steepfile +1 -0
- data/docs/GUIDE.md +340 -0
- data/examples/README.md +68 -20
- data/examples/{context_demo.rb → args_demo.rb} +27 -27
- data/examples/clean_demo.rb +204 -0
- data/examples/data_pipeline_demo.rb +3 -3
- data/examples/group_demo.rb +113 -0
- data/examples/nested_section_demo.rb +161 -0
- data/examples/parallel_progress_demo.rb +1 -1
- data/examples/reexecution_demo.rb +93 -80
- data/examples/system_call_demo.rb +56 -0
- data/examples/tree_progress_demo.rb +164 -0
- data/lib/taski/{context.rb → args.rb} +3 -3
- data/lib/taski/execution/execution_context.rb +379 -0
- data/lib/taski/execution/executor.rb +538 -0
- data/lib/taski/execution/registry.rb +26 -2
- data/lib/taski/execution/scheduler.rb +308 -0
- data/lib/taski/execution/task_output_pipe.rb +42 -0
- data/lib/taski/execution/task_output_router.rb +216 -0
- data/lib/taski/execution/task_wrapper.rb +295 -146
- data/lib/taski/execution/tree_progress_display.rb +793 -0
- data/lib/taski/execution/worker_pool.rb +104 -0
- data/lib/taski/section.rb +23 -0
- data/lib/taski/static_analysis/analyzer.rb +4 -2
- data/lib/taski/static_analysis/visitor.rb +86 -5
- data/lib/taski/task.rb +223 -120
- data/lib/taski/version.rb +1 -1
- data/lib/taski.rb +147 -28
- data/sig/taski.rbs +310 -67
- metadata +17 -8
- data/docs/advanced-features.md +0 -625
- data/docs/api-guide.md +0 -509
- data/docs/error-handling.md +0 -684
- data/lib/taski/execution/coordinator.rb +0 -63
- data/lib/taski/execution/parallel_progress_display.rb +0 -201
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "etc"
|
|
4
|
+
|
|
5
|
+
module Taski
|
|
6
|
+
module Execution
|
|
7
|
+
# WorkerPool manages a pool of worker threads that execute tasks.
|
|
8
|
+
# It provides methods to start, stop, and enqueue tasks for execution.
|
|
9
|
+
#
|
|
10
|
+
# == Responsibilities
|
|
11
|
+
#
|
|
12
|
+
# - Manage worker thread lifecycle (start, shutdown)
|
|
13
|
+
# - Distribute tasks to worker threads via Queue
|
|
14
|
+
# - Execute tasks via callback provided by Executor
|
|
15
|
+
#
|
|
16
|
+
# == API
|
|
17
|
+
#
|
|
18
|
+
# - {#start} - Start all worker threads
|
|
19
|
+
# - {#enqueue} - Add a task to the execution queue
|
|
20
|
+
# - {#shutdown} - Gracefully shutdown all worker threads
|
|
21
|
+
# - {#execution_queue} - Access the underlying Queue (for testing)
|
|
22
|
+
#
|
|
23
|
+
# == Thread Safety
|
|
24
|
+
#
|
|
25
|
+
# WorkerPool uses Queue for thread-safe task distribution.
|
|
26
|
+
# The Queue handles synchronization between the main thread
|
|
27
|
+
# (which enqueues tasks) and worker threads (which pop tasks).
|
|
28
|
+
class WorkerPool
|
|
29
|
+
attr_reader :execution_queue
|
|
30
|
+
|
|
31
|
+
# @param registry [Registry] The task registry for thread tracking
|
|
32
|
+
# @param worker_count [Integer, nil] Number of worker threads (defaults to CPU count)
|
|
33
|
+
# @param on_execute [Proc] Callback to execute a task, receives (task_class, wrapper)
|
|
34
|
+
def initialize(registry:, worker_count: nil, &on_execute)
|
|
35
|
+
@worker_count = worker_count || default_worker_count
|
|
36
|
+
@registry = registry
|
|
37
|
+
@on_execute = on_execute
|
|
38
|
+
@execution_queue = Queue.new
|
|
39
|
+
@workers = []
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Start all worker threads.
|
|
43
|
+
def start
|
|
44
|
+
@worker_count.times do
|
|
45
|
+
worker = Thread.new { worker_loop }
|
|
46
|
+
@workers << worker
|
|
47
|
+
@registry.register_thread(worker)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Enqueue a task for execution.
|
|
52
|
+
#
|
|
53
|
+
# @param task_class [Class] The task class to execute
|
|
54
|
+
# @param wrapper [TaskWrapper] The task wrapper
|
|
55
|
+
def enqueue(task_class, wrapper)
|
|
56
|
+
@execution_queue.push({task_class: task_class, wrapper: wrapper})
|
|
57
|
+
debug_log("Enqueued: #{task_class}")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Shutdown all worker threads gracefully.
|
|
61
|
+
def shutdown
|
|
62
|
+
enqueue_shutdown_signals
|
|
63
|
+
@workers.each(&:join)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Enqueue shutdown signals for all workers.
|
|
67
|
+
def enqueue_shutdown_signals
|
|
68
|
+
@worker_count.times { @execution_queue.push(:shutdown) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def default_worker_count
|
|
74
|
+
Etc.nprocessors.clamp(2, 8)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def worker_loop
|
|
78
|
+
loop do
|
|
79
|
+
work_item = @execution_queue.pop
|
|
80
|
+
break if work_item == :shutdown
|
|
81
|
+
|
|
82
|
+
task_class = work_item[:task_class]
|
|
83
|
+
wrapper = work_item[:wrapper]
|
|
84
|
+
|
|
85
|
+
debug_log("Worker executing: #{task_class}")
|
|
86
|
+
|
|
87
|
+
begin
|
|
88
|
+
@on_execute.call(task_class, wrapper)
|
|
89
|
+
rescue => e
|
|
90
|
+
# Log error but don't crash the worker thread.
|
|
91
|
+
# Task-level errors are handled in the execute callback.
|
|
92
|
+
# This catches unexpected errors in the callback itself.
|
|
93
|
+
warn "[WorkerPool] Unexpected error executing #{task_class}: #{e.message}"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def debug_log(message)
|
|
99
|
+
return unless ENV["TASKI_DEBUG"]
|
|
100
|
+
puts "[WorkerPool] #{message}"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
data/lib/taski/section.rb
CHANGED
|
@@ -24,6 +24,12 @@ module Taski
|
|
|
24
24
|
raise "Section #{self.class} does not have an implementation. Override 'impl' method."
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# Register runtime dependency for clean phase (before register_impl_selection)
|
|
28
|
+
register_runtime_dependency(implementation_class)
|
|
29
|
+
|
|
30
|
+
# Register selected impl for progress display
|
|
31
|
+
register_impl_selection(implementation_class)
|
|
32
|
+
|
|
27
33
|
apply_interface_to_implementation(implementation_class)
|
|
28
34
|
|
|
29
35
|
self.class.exported_methods.each do |method|
|
|
@@ -40,6 +46,23 @@ module Taski
|
|
|
40
46
|
|
|
41
47
|
private
|
|
42
48
|
|
|
49
|
+
# Register the selected implementation as a runtime dependency.
|
|
50
|
+
# This allows the clean phase to include the dynamically selected impl.
|
|
51
|
+
# Handles nil ExecutionContext gracefully.
|
|
52
|
+
#
|
|
53
|
+
# @param impl_class [Class] The selected implementation class
|
|
54
|
+
def register_runtime_dependency(impl_class)
|
|
55
|
+
context = Execution::ExecutionContext.current
|
|
56
|
+
context&.register_runtime_dependency(self.class, impl_class)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def register_impl_selection(implementation_class)
|
|
60
|
+
context = Execution::ExecutionContext.current
|
|
61
|
+
return unless context
|
|
62
|
+
|
|
63
|
+
context.notify_section_impl_selected(self.class, implementation_class)
|
|
64
|
+
end
|
|
65
|
+
|
|
43
66
|
# @param implementation_class [Class] The implementation task class
|
|
44
67
|
def apply_interface_to_implementation(implementation_class)
|
|
45
68
|
interface_methods = self.class.exported_methods
|
|
@@ -7,8 +7,8 @@ module Taski
|
|
|
7
7
|
module StaticAnalysis
|
|
8
8
|
class Analyzer
|
|
9
9
|
# Analyzes a task class and returns its static dependencies.
|
|
10
|
-
# For Task: dependencies detected from run method (SomeTask.method calls)
|
|
11
|
-
# For Section: impl candidates detected from impl method (constants returned)
|
|
10
|
+
# For Task: dependencies detected from run method and called methods (SomeTask.method calls)
|
|
11
|
+
# For Section: impl candidates detected from impl method and called methods (constants returned)
|
|
12
12
|
#
|
|
13
13
|
# Static dependencies are used for:
|
|
14
14
|
# - Tree display visualization
|
|
@@ -27,6 +27,8 @@ module Taski
|
|
|
27
27
|
|
|
28
28
|
visitor = Visitor.new(task_class, target_method)
|
|
29
29
|
visitor.visit(parse_result.value)
|
|
30
|
+
# Follow method calls to analyze dependencies in called methods
|
|
31
|
+
visitor.follow_method_calls
|
|
30
32
|
visitor.dependencies
|
|
31
33
|
end
|
|
32
34
|
|
|
@@ -9,17 +9,34 @@ module Taski
|
|
|
9
9
|
|
|
10
10
|
# @param target_task_class [Class] The task class to analyze
|
|
11
11
|
# @param target_method [Symbol] The method name to analyze (:run or :impl)
|
|
12
|
-
|
|
12
|
+
# @param methods_to_analyze [Set<Symbol>] Set of method names to analyze (for following calls)
|
|
13
|
+
def initialize(target_task_class, target_method = :run, methods_to_analyze = nil)
|
|
13
14
|
super()
|
|
14
15
|
@target_task_class = target_task_class
|
|
15
16
|
@target_method = target_method
|
|
16
17
|
@dependencies = Set.new
|
|
17
18
|
@in_target_method = false
|
|
18
19
|
@current_namespace_path = []
|
|
20
|
+
# Methods to analyze: starts with just the target method, grows as we find calls
|
|
21
|
+
@methods_to_analyze = methods_to_analyze || Set.new([@target_method])
|
|
22
|
+
# Track which methods we've already analyzed to prevent infinite loops
|
|
23
|
+
@analyzed_methods = Set.new
|
|
24
|
+
# Collect method calls made within analyzed methods (for following)
|
|
25
|
+
@method_calls_to_follow = Set.new
|
|
26
|
+
# Store method definitions found in the class for later analysis
|
|
27
|
+
@class_method_defs = {}
|
|
28
|
+
# Track if we're in an impl call chain (for Section constant detection)
|
|
29
|
+
@in_impl_chain = false
|
|
19
30
|
end
|
|
20
31
|
|
|
21
32
|
def visit_class_node(node)
|
|
22
|
-
within_namespace(extract_constant_name(node.constant_path))
|
|
33
|
+
within_namespace(extract_constant_name(node.constant_path)) do
|
|
34
|
+
if in_target_class?
|
|
35
|
+
# First pass: collect all method definitions in the target class
|
|
36
|
+
collect_method_definitions(node)
|
|
37
|
+
end
|
|
38
|
+
super
|
|
39
|
+
end
|
|
23
40
|
end
|
|
24
41
|
|
|
25
42
|
def visit_module_node(node)
|
|
@@ -27,17 +44,25 @@ module Taski
|
|
|
27
44
|
end
|
|
28
45
|
|
|
29
46
|
def visit_def_node(node)
|
|
30
|
-
if
|
|
47
|
+
if in_target_class? && should_analyze_method?(node.name)
|
|
48
|
+
@analyzed_methods.add(node.name)
|
|
31
49
|
@in_target_method = true
|
|
50
|
+
@current_analyzing_method = node.name
|
|
51
|
+
# Start impl chain when entering impl method
|
|
52
|
+
@in_impl_chain = true if node.name == :impl && @target_method == :impl
|
|
32
53
|
super
|
|
33
54
|
@in_target_method = false
|
|
55
|
+
@current_analyzing_method = nil
|
|
34
56
|
else
|
|
35
57
|
super
|
|
36
58
|
end
|
|
37
59
|
end
|
|
38
60
|
|
|
39
61
|
def visit_call_node(node)
|
|
40
|
-
|
|
62
|
+
if @in_target_method
|
|
63
|
+
detect_task_dependency(node)
|
|
64
|
+
detect_method_call_to_follow(node)
|
|
65
|
+
end
|
|
41
66
|
super
|
|
42
67
|
end
|
|
43
68
|
|
|
@@ -53,8 +78,46 @@ module Taski
|
|
|
53
78
|
super
|
|
54
79
|
end
|
|
55
80
|
|
|
81
|
+
# After visiting, follow any method calls that need analysis
|
|
82
|
+
# @in_impl_chain is preserved because methods called from impl should
|
|
83
|
+
# also detect constants as impl candidates
|
|
84
|
+
def follow_method_calls
|
|
85
|
+
new_methods = @method_calls_to_follow - @analyzed_methods
|
|
86
|
+
return if new_methods.empty?
|
|
87
|
+
|
|
88
|
+
# Add new methods to analyze
|
|
89
|
+
@methods_to_analyze.merge(new_methods)
|
|
90
|
+
@method_calls_to_follow.clear
|
|
91
|
+
|
|
92
|
+
# Re-analyze the class methods
|
|
93
|
+
# Preserve impl chain context: methods called from impl should continue
|
|
94
|
+
# detecting constants as impl candidates
|
|
95
|
+
@class_method_defs.each do |method_name, method_node|
|
|
96
|
+
next unless new_methods.include?(method_name)
|
|
97
|
+
|
|
98
|
+
@analyzed_methods.add(method_name)
|
|
99
|
+
@in_target_method = true
|
|
100
|
+
@current_analyzing_method = method_name
|
|
101
|
+
visit(method_node)
|
|
102
|
+
@in_target_method = false
|
|
103
|
+
@current_analyzing_method = nil
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Recursively follow any new calls discovered
|
|
107
|
+
follow_method_calls
|
|
108
|
+
end
|
|
109
|
+
|
|
56
110
|
private
|
|
57
111
|
|
|
112
|
+
# Collect all method definitions in the target class for later analysis
|
|
113
|
+
def collect_method_definitions(class_node)
|
|
114
|
+
class_node.body&.body&.each do |node|
|
|
115
|
+
if node.is_a?(Prism::DefNode)
|
|
116
|
+
@class_method_defs[node.name] = node
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
58
121
|
def within_namespace(name)
|
|
59
122
|
@current_namespace_path.push(name)
|
|
60
123
|
yield
|
|
@@ -70,8 +133,26 @@ module Taski
|
|
|
70
133
|
node.slice
|
|
71
134
|
end
|
|
72
135
|
|
|
136
|
+
def should_analyze_method?(method_name)
|
|
137
|
+
@methods_to_analyze.include?(method_name) && !@analyzed_methods.include?(method_name)
|
|
138
|
+
end
|
|
139
|
+
|
|
73
140
|
def in_impl_method?
|
|
74
|
-
@in_target_method && @
|
|
141
|
+
@in_target_method && @in_impl_chain
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Detect method calls that should be followed (calls to methods in the same class)
|
|
145
|
+
def detect_method_call_to_follow(node)
|
|
146
|
+
# Only follow calls without explicit receiver (self.method or just method)
|
|
147
|
+
return if node.receiver && !self_receiver?(node.receiver)
|
|
148
|
+
|
|
149
|
+
method_name = node.name
|
|
150
|
+
# Mark this method for later analysis if it's defined in the class
|
|
151
|
+
@method_calls_to_follow.add(method_name) if @class_method_defs.key?(method_name)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def self_receiver?(receiver)
|
|
155
|
+
receiver.is_a?(Prism::SelfNode)
|
|
75
156
|
end
|
|
76
157
|
|
|
77
158
|
def detect_impl_candidate(node)
|