taski 0.9.0 → 0.9.2
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 +25 -0
- data/README.md +43 -0
- data/docs/GUIDE.md +40 -1
- data/lib/taski/execution/executor.rb +32 -23
- data/lib/taski/execution/fiber_protocol.rb +27 -0
- data/lib/taski/execution/task_wrapper.rb +2 -2
- data/lib/taski/execution/worker_pool.rb +95 -54
- data/lib/taski/progress/config.rb +90 -0
- data/lib/taski/progress/layout/base.rb +25 -3
- data/lib/taski/progress/layout/simple.rb +17 -31
- data/lib/taski/progress/layout/theme_drop.rb +1 -1
- data/lib/taski/progress/layout/tree/event.rb +49 -0
- data/lib/taski/progress/layout/tree/live.rb +85 -0
- data/lib/taski/progress/layout/tree/structure.rb +142 -0
- data/lib/taski/progress/layout/tree.rb +25 -283
- data/lib/taski/progress/theme/base.rb +1 -1
- data/lib/taski/progress/theme/compact.rb +1 -1
- data/lib/taski/static_analysis/start_dep_analyzer.rb +400 -0
- data/lib/taski/task.rb +11 -6
- data/lib/taski/task_proxy.rb +59 -0
- data/lib/taski/test_helper.rb +1 -1
- data/lib/taski/version.rb +1 -1
- data/lib/taski.rb +25 -7
- metadata +22 -1
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Taski
|
|
6
|
+
module StaticAnalysis
|
|
7
|
+
# Analyzes a task's run method AST to find dependencies that are safe
|
|
8
|
+
# to speculatively pre-start (start_dep). Uses a whitelist approach:
|
|
9
|
+
# only confirmed patterns are collected; unknown patterns cause the
|
|
10
|
+
# analyzer to stop (returning what was collected so far up to that point).
|
|
11
|
+
#
|
|
12
|
+
# Currently handles variable assignment patterns only:
|
|
13
|
+
# a = Dep.value (LocalVariableWriteNode)
|
|
14
|
+
# @a = Dep.value (InstanceVariableWriteNode)
|
|
15
|
+
#
|
|
16
|
+
# This is a performance optimization only — if analysis fails or returns
|
|
17
|
+
# empty, tasks still work correctly via lazy Fiber pull (need_dep).
|
|
18
|
+
class StartDepAnalyzer
|
|
19
|
+
DepInfo = Data.define(:klass, :method_name)
|
|
20
|
+
AnalysisResult = Data.define(:start_deps, :sync_deps)
|
|
21
|
+
|
|
22
|
+
# AST node types that are known safe (not dependencies, won't stop scanning)
|
|
23
|
+
SAFE_TYPES = Set[
|
|
24
|
+
Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode,
|
|
25
|
+
Prism::ConstantReadNode, Prism::ConstantPathNode,
|
|
26
|
+
Prism::IntegerNode, Prism::FloatNode, Prism::StringNode,
|
|
27
|
+
Prism::SymbolNode, Prism::NilNode, Prism::TrueNode, Prism::FalseNode,
|
|
28
|
+
Prism::SelfNode
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
@cache = {}
|
|
32
|
+
@cache_mutex = Mutex.new
|
|
33
|
+
|
|
34
|
+
class << self
|
|
35
|
+
# Analyze a task class and return deps safe to prestart.
|
|
36
|
+
# Results are cached per task class.
|
|
37
|
+
# @param task_class [Class] The task class to analyze
|
|
38
|
+
# @return [Array<DepInfo>] Deduplicated list of safe dependencies
|
|
39
|
+
def analyze(task_class)
|
|
40
|
+
@cache_mutex.synchronize do
|
|
41
|
+
return @cache[task_class] if @cache.key?(task_class)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
result = new.analyze(task_class)
|
|
45
|
+
|
|
46
|
+
@cache_mutex.synchronize do
|
|
47
|
+
@cache[task_class] ||= result
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Clear cache (for testing)
|
|
52
|
+
def clear_cache!
|
|
53
|
+
@cache_mutex.synchronize { @cache.clear }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
EMPTY_RESULT = AnalysisResult.new(start_deps: Set.new.freeze, sync_deps: Set.new.freeze).freeze
|
|
58
|
+
|
|
59
|
+
def initialize
|
|
60
|
+
@deps = []
|
|
61
|
+
@seen_classes = Set.new
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Analyze a task class's run method and return safe-to-prestart deps
|
|
65
|
+
# and sync_dep_classes (deps whose proxy variables are used unsafely).
|
|
66
|
+
# @param task_class [Class] The task class to analyze
|
|
67
|
+
# @return [AnalysisResult]
|
|
68
|
+
def analyze(task_class)
|
|
69
|
+
@task_class = task_class
|
|
70
|
+
@exported_ivars = Set.new(task_class.exported_methods.map { |m| :"@#{m}" })
|
|
71
|
+
source_location = task_class.instance_method(:run).source_location
|
|
72
|
+
return EMPTY_RESULT unless source_location
|
|
73
|
+
|
|
74
|
+
file_path, _line = source_location
|
|
75
|
+
parse_result = Prism.parse_file(file_path)
|
|
76
|
+
|
|
77
|
+
run_node = find_run_method(parse_result.value, task_class)
|
|
78
|
+
return EMPTY_RESULT unless run_node&.body
|
|
79
|
+
|
|
80
|
+
scan_statements(run_node.body)
|
|
81
|
+
unsafe_classes = detect_unsafe_proxy_usage(run_node.body)
|
|
82
|
+
all_dep_classes = Set.new(@deps.map(&:klass))
|
|
83
|
+
start_deps = all_dep_classes - unsafe_classes
|
|
84
|
+
sync_deps = unsafe_classes
|
|
85
|
+
AnalysisResult.new(start_deps: start_deps, sync_deps: sync_deps)
|
|
86
|
+
rescue NameError
|
|
87
|
+
EMPTY_RESULT
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
# Find the def run node inside the target class
|
|
93
|
+
def find_run_method(program_node, task_class)
|
|
94
|
+
target_name = task_class.name
|
|
95
|
+
find_run_in_tree(program_node, [], target_name)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def find_run_in_tree(node, namespace_path, target_name)
|
|
99
|
+
case node
|
|
100
|
+
when Prism::ProgramNode
|
|
101
|
+
node.statements.body.each do |child|
|
|
102
|
+
result = find_run_in_tree(child, namespace_path, target_name)
|
|
103
|
+
return result if result
|
|
104
|
+
end
|
|
105
|
+
when Prism::ModuleNode
|
|
106
|
+
name = node.constant_path.slice
|
|
107
|
+
new_path = namespace_path + [name]
|
|
108
|
+
node.body&.body&.each do |child|
|
|
109
|
+
result = find_run_in_tree(child, new_path, target_name)
|
|
110
|
+
return result if result
|
|
111
|
+
end
|
|
112
|
+
when Prism::ClassNode
|
|
113
|
+
name = node.constant_path.slice
|
|
114
|
+
new_path = namespace_path + [name]
|
|
115
|
+
full_name = new_path.join("::")
|
|
116
|
+
|
|
117
|
+
node.body&.body&.each do |child|
|
|
118
|
+
if full_name == target_name
|
|
119
|
+
return child if child.is_a?(Prism::DefNode) && child.name == :run
|
|
120
|
+
else
|
|
121
|
+
result = find_run_in_tree(child, new_path, target_name)
|
|
122
|
+
return result if result
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
when Prism::StatementsNode
|
|
126
|
+
node.body.each do |child|
|
|
127
|
+
result = find_run_in_tree(child, namespace_path, target_name)
|
|
128
|
+
return result if result
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
nil
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Scan statements, collecting deps. Stops at the first unknown pattern.
|
|
136
|
+
def scan_statements(node)
|
|
137
|
+
return unless node.is_a?(Prism::StatementsNode)
|
|
138
|
+
node.body.each { |stmt| break unless try_match(stmt) }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Match a statement against known patterns.
|
|
142
|
+
# Returns true to continue scanning, false to stop.
|
|
143
|
+
def try_match(stmt)
|
|
144
|
+
case stmt
|
|
145
|
+
when Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode
|
|
146
|
+
check_dep_call(stmt.value)
|
|
147
|
+
true
|
|
148
|
+
when *SAFE_TYPES
|
|
149
|
+
true
|
|
150
|
+
else
|
|
151
|
+
false
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Check if a node is a Task dependency call (Constant.method) and collect it.
|
|
156
|
+
def check_dep_call(node)
|
|
157
|
+
return unless node.is_a?(Prism::CallNode)
|
|
158
|
+
return unless node.receiver
|
|
159
|
+
|
|
160
|
+
case node.receiver
|
|
161
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
162
|
+
constant_name = node.receiver.slice
|
|
163
|
+
resolved = resolve_constant(constant_name)
|
|
164
|
+
if resolved.is_a?(Class) && defined?(Taski::Task) && resolved < Taski::Task
|
|
165
|
+
collect_dep(node)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Collect a dependency, deduplicating by class
|
|
171
|
+
def collect_dep(call_node)
|
|
172
|
+
constant_name = call_node.receiver.slice
|
|
173
|
+
method_name = call_node.name
|
|
174
|
+
klass = resolve_constant(constant_name)
|
|
175
|
+
return unless klass
|
|
176
|
+
|
|
177
|
+
@deps << DepInfo.new(klass: klass, method_name: method_name) if @seen_classes.add?(klass)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Phase 2: Detect proxy variables used in unsafe contexts.
|
|
181
|
+
# Returns a Set of dep classes whose proxy variables are used unsafely.
|
|
182
|
+
# A proxy variable is a local variable assigned from a Taski::Task dep call
|
|
183
|
+
# (e.g., `a = Dep.value`). If such a variable is later used in an unsafe
|
|
184
|
+
# context (as argument, condition, array element, etc.), the dep class is
|
|
185
|
+
# added to sync_dep_classes so it will be resolved synchronously.
|
|
186
|
+
def detect_unsafe_proxy_usage(body_node)
|
|
187
|
+
proxy_vars = build_proxy_var_map(body_node)
|
|
188
|
+
|
|
189
|
+
unsafe_classes = Set.new
|
|
190
|
+
scan_for_unsafe_usage(body_node, proxy_vars, unsafe_classes)
|
|
191
|
+
unsafe_classes
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Build mapping of { local_var_name => dep_class } from assignment statements
|
|
195
|
+
def build_proxy_var_map(body_node)
|
|
196
|
+
proxy_vars = {}
|
|
197
|
+
return proxy_vars unless body_node.is_a?(Prism::StatementsNode)
|
|
198
|
+
|
|
199
|
+
body_node.body.each do |stmt|
|
|
200
|
+
next unless stmt.is_a?(Prism::LocalVariableWriteNode)
|
|
201
|
+
|
|
202
|
+
dep_class = extract_dep_class(stmt.value)
|
|
203
|
+
proxy_vars[stmt.name] = dep_class if dep_class
|
|
204
|
+
end
|
|
205
|
+
proxy_vars
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Extract the dep class from a call node if it's a Taski::Task dep call
|
|
209
|
+
def extract_dep_class(node)
|
|
210
|
+
return nil unless node.is_a?(Prism::CallNode)
|
|
211
|
+
return nil unless node.receiver
|
|
212
|
+
|
|
213
|
+
case node.receiver
|
|
214
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
215
|
+
constant_name = node.receiver.slice
|
|
216
|
+
resolved = resolve_constant(constant_name)
|
|
217
|
+
if resolved.is_a?(Class) && defined?(Taski::Task) && resolved < Taski::Task
|
|
218
|
+
resolved
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Recursively scan AST for unsafe proxy variable usage.
|
|
224
|
+
# Safe contexts: receiver of CallNode, string interpolation,
|
|
225
|
+
# RHS of local/ivar assignment. Everything else is unsafe.
|
|
226
|
+
def scan_for_unsafe_usage(node, proxy_vars, unsafe_classes) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
227
|
+
case node
|
|
228
|
+
when Prism::StatementsNode
|
|
229
|
+
node.body.each { |child| scan_for_unsafe_usage(child, proxy_vars, unsafe_classes) }
|
|
230
|
+
|
|
231
|
+
when Prism::LocalVariableWriteNode
|
|
232
|
+
if (dep_class = proxy_dep_class(node.value, proxy_vars))
|
|
233
|
+
# Reassignment or direct dep call: track the new variable name
|
|
234
|
+
proxy_vars[node.name] = dep_class
|
|
235
|
+
else
|
|
236
|
+
scan_for_unsafe_usage(node.value, proxy_vars, unsafe_classes)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
when Prism::InstanceVariableWriteNode
|
|
240
|
+
if (dep_class = proxy_dep_class(node.value, proxy_vars))
|
|
241
|
+
if @exported_ivars.include?(node.name)
|
|
242
|
+
# @exported = proxy → safe (resolve_proxy_exports handles it)
|
|
243
|
+
else
|
|
244
|
+
# @non_exported = proxy → track for unsafe usage detection
|
|
245
|
+
proxy_vars[node.name] = dep_class
|
|
246
|
+
end
|
|
247
|
+
else
|
|
248
|
+
scan_for_unsafe_usage(node.value, proxy_vars, unsafe_classes)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
when Prism::CallNode
|
|
252
|
+
# Receiver: proxy.foo → safe (method_missing fires)
|
|
253
|
+
if proxy_var_read?(node.receiver, proxy_vars)
|
|
254
|
+
# safe — don't flag receiver
|
|
255
|
+
elsif node.receiver
|
|
256
|
+
scan_for_unsafe_usage(node.receiver, proxy_vars, unsafe_classes)
|
|
257
|
+
end
|
|
258
|
+
# Arguments: foo(proxy) → UNSAFE
|
|
259
|
+
node.arguments&.arguments&.each do |arg|
|
|
260
|
+
if proxy_var_read?(arg, proxy_vars)
|
|
261
|
+
unsafe_classes.add(proxy_vars[arg.name])
|
|
262
|
+
else
|
|
263
|
+
scan_for_unsafe_usage(arg, proxy_vars, unsafe_classes)
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
scan_for_unsafe_usage(node.block, proxy_vars, unsafe_classes) if node.block
|
|
267
|
+
|
|
268
|
+
when Prism::IfNode
|
|
269
|
+
check_predicate_unsafe(node.predicate, proxy_vars, unsafe_classes)
|
|
270
|
+
scan_for_unsafe_usage(node.statements, proxy_vars, unsafe_classes) if node.statements
|
|
271
|
+
scan_for_unsafe_usage(node.subsequent, proxy_vars, unsafe_classes) if node.subsequent
|
|
272
|
+
|
|
273
|
+
when Prism::UnlessNode
|
|
274
|
+
check_predicate_unsafe(node.predicate, proxy_vars, unsafe_classes)
|
|
275
|
+
scan_for_unsafe_usage(node.statements, proxy_vars, unsafe_classes) if node.statements
|
|
276
|
+
scan_for_unsafe_usage(node.else_clause, proxy_vars, unsafe_classes) if node.else_clause
|
|
277
|
+
|
|
278
|
+
when Prism::WhileNode, Prism::UntilNode
|
|
279
|
+
check_predicate_unsafe(node.predicate, proxy_vars, unsafe_classes)
|
|
280
|
+
scan_for_unsafe_usage(node.statements, proxy_vars, unsafe_classes) if node.statements
|
|
281
|
+
|
|
282
|
+
when Prism::InterpolatedStringNode
|
|
283
|
+
node.parts.each do |part|
|
|
284
|
+
next unless part.is_a?(Prism::EmbeddedStatementsNode)
|
|
285
|
+
|
|
286
|
+
if part.statements&.body&.size == 1 &&
|
|
287
|
+
proxy_var_read?(part.statements.body[0], proxy_vars)
|
|
288
|
+
# safe — string interpolation calls to_s
|
|
289
|
+
else
|
|
290
|
+
scan_for_unsafe_usage(part, proxy_vars, unsafe_classes)
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
when Prism::EmbeddedStatementsNode
|
|
295
|
+
scan_for_unsafe_usage(node.statements, proxy_vars, unsafe_classes) if node.statements
|
|
296
|
+
|
|
297
|
+
when Prism::ArrayNode
|
|
298
|
+
node.elements.each do |elem|
|
|
299
|
+
if proxy_var_read?(elem, proxy_vars)
|
|
300
|
+
unsafe_classes.add(proxy_vars[elem.name])
|
|
301
|
+
else
|
|
302
|
+
scan_for_unsafe_usage(elem, proxy_vars, unsafe_classes)
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
when Prism::ElseNode
|
|
307
|
+
scan_for_unsafe_usage(node.statements, proxy_vars, unsafe_classes) if node.statements
|
|
308
|
+
|
|
309
|
+
when Prism::BeginNode
|
|
310
|
+
scan_for_unsafe_usage(node.statements, proxy_vars, unsafe_classes) if node.statements
|
|
311
|
+
scan_for_unsafe_usage(node.rescue_clause, proxy_vars, unsafe_classes) if node.rescue_clause
|
|
312
|
+
scan_for_unsafe_usage(node.ensure_clause, proxy_vars, unsafe_classes) if node.ensure_clause
|
|
313
|
+
|
|
314
|
+
when Prism::RescueNode
|
|
315
|
+
scan_for_unsafe_usage(node.statements, proxy_vars, unsafe_classes) if node.statements
|
|
316
|
+
scan_for_unsafe_usage(node.subsequent, proxy_vars, unsafe_classes) if node.subsequent
|
|
317
|
+
|
|
318
|
+
when Prism::EnsureNode
|
|
319
|
+
scan_for_unsafe_usage(node.statements, proxy_vars, unsafe_classes) if node.statements
|
|
320
|
+
|
|
321
|
+
when Prism::ParenthesesNode
|
|
322
|
+
scan_for_unsafe_usage(node.body, proxy_vars, unsafe_classes) if node.body
|
|
323
|
+
|
|
324
|
+
when Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode
|
|
325
|
+
# Bare proxy variable read in unknown context → UNSAFE
|
|
326
|
+
unsafe_classes.add(proxy_vars[node.name]) if proxy_vars.key?(node.name)
|
|
327
|
+
|
|
328
|
+
else
|
|
329
|
+
# For any unhandled node type, recurse into children (safety-first)
|
|
330
|
+
if node.respond_to?(:compact_child_nodes)
|
|
331
|
+
node.compact_child_nodes.each do |child|
|
|
332
|
+
scan_for_unsafe_usage(child, proxy_vars, unsafe_classes)
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Check if a predicate node is an unsafe proxy variable read
|
|
339
|
+
def check_predicate_unsafe(predicate, proxy_vars, unsafe_classes)
|
|
340
|
+
if proxy_var_read?(predicate, proxy_vars)
|
|
341
|
+
unsafe_classes.add(proxy_vars[predicate_key(predicate)])
|
|
342
|
+
else
|
|
343
|
+
scan_for_unsafe_usage(predicate, proxy_vars, unsafe_classes)
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Return the dep class if the node reads a proxy variable (local or ivar)
|
|
348
|
+
# or is a direct dep call. Returns nil otherwise.
|
|
349
|
+
def proxy_dep_class(node, proxy_vars)
|
|
350
|
+
if proxy_var_read?(node, proxy_vars)
|
|
351
|
+
proxy_vars[predicate_key(node)]
|
|
352
|
+
else
|
|
353
|
+
extract_dep_class(node)
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# Check if node is a proxy variable read (local var or ivar)
|
|
358
|
+
def proxy_var_read?(node, proxy_vars)
|
|
359
|
+
case node
|
|
360
|
+
when Prism::LocalVariableReadNode
|
|
361
|
+
proxy_vars.key?(node.name)
|
|
362
|
+
when Prism::InstanceVariableReadNode
|
|
363
|
+
proxy_vars.key?(node.name)
|
|
364
|
+
else
|
|
365
|
+
false
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Extract the proxy_vars key from a variable read node
|
|
370
|
+
def predicate_key(node)
|
|
371
|
+
node.name
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Resolve a constant name to the class, with namespace fallback.
|
|
375
|
+
def resolve_constant(constant_name)
|
|
376
|
+
Object.const_get(constant_name)
|
|
377
|
+
rescue NameError
|
|
378
|
+
resolve_with_namespace(constant_name)
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def resolve_with_namespace(constant_name)
|
|
382
|
+
return nil unless @task_class
|
|
383
|
+
|
|
384
|
+
namespace_parts = @task_class.name.split("::")
|
|
385
|
+
namespace_parts.length.downto(0) do |i|
|
|
386
|
+
prefix = namespace_parts.take(i).join("::")
|
|
387
|
+
full_name = prefix.empty? ? constant_name : "#{prefix}::#{constant_name}"
|
|
388
|
+
|
|
389
|
+
begin
|
|
390
|
+
return Object.const_get(full_name)
|
|
391
|
+
rescue NameError
|
|
392
|
+
next
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
nil
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
end
|
data/lib/taski/task.rb
CHANGED
|
@@ -6,6 +6,7 @@ require_relative "execution/registry"
|
|
|
6
6
|
require_relative "execution/task_wrapper"
|
|
7
7
|
require_relative "progress/layout/tree"
|
|
8
8
|
require_relative "progress/theme/plain"
|
|
9
|
+
require_relative "task_proxy"
|
|
9
10
|
|
|
10
11
|
module Taski
|
|
11
12
|
# Base class for all tasks in the Taski framework.
|
|
@@ -114,7 +115,7 @@ module Taski
|
|
|
114
115
|
def tree
|
|
115
116
|
output = StringIO.new
|
|
116
117
|
theme = Progress::Theme::Plain.new
|
|
117
|
-
layout = Progress::Layout::Tree.
|
|
118
|
+
layout = Progress::Layout::Tree.for(output: output, theme: theme)
|
|
118
119
|
context = Execution::ExecutionFacade.new(root_task_class: self)
|
|
119
120
|
layout.context = context
|
|
120
121
|
layout.on_ready
|
|
@@ -181,12 +182,16 @@ module Taski
|
|
|
181
182
|
registry = Taski.current_registry
|
|
182
183
|
if registry
|
|
183
184
|
if Thread.current[:taski_fiber_context]
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
185
|
+
start_deps = Thread.current[:taski_start_deps]
|
|
186
|
+
if start_deps&.include?(self)
|
|
187
|
+
# Lazy resolution via proxy - safe dep confirmed by static analysis
|
|
188
|
+
TaskProxy.new(self, method)
|
|
189
|
+
else
|
|
190
|
+
# Synchronous resolution: dep not in allowlist (unknown or unsafe usage)
|
|
191
|
+
result = Fiber.yield(Taski::Execution::FiberProtocol::NeedDep.new(self, method))
|
|
192
|
+
raise result.error if result in Taski::Execution::FiberProtocol::DepError
|
|
193
|
+
result
|
|
188
194
|
end
|
|
189
|
-
result
|
|
190
195
|
else
|
|
191
196
|
# Synchronous resolution (clean phase, outside Fiber)
|
|
192
197
|
wrapper = registry.get_or_create(self) do
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Taski
|
|
4
|
+
# Lazy proxy that defers dependency resolution until the value is actually used.
|
|
5
|
+
# Inherits from BasicObject to minimize available methods, maximizing method_missing delegation.
|
|
6
|
+
class TaskProxy < BasicObject
|
|
7
|
+
def initialize(task_class, method)
|
|
8
|
+
@task_class = task_class
|
|
9
|
+
@method = method
|
|
10
|
+
@resolved = false
|
|
11
|
+
@value = nil
|
|
12
|
+
@error = nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def __resolve__
|
|
16
|
+
::Kernel.raise @error if @error
|
|
17
|
+
return @value if @resolved
|
|
18
|
+
@value = ::Fiber.yield(::Taski::Execution::FiberProtocol::NeedDep.new(@task_class, @method))
|
|
19
|
+
if @value in ::Taski::Execution::FiberProtocol::DepError
|
|
20
|
+
@error = @value.error
|
|
21
|
+
::Kernel.raise @error
|
|
22
|
+
end
|
|
23
|
+
@resolved = true
|
|
24
|
+
@value
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def __taski_proxy_resolve__
|
|
28
|
+
__resolve__
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def method_missing(name, *args, **kwargs, &block)
|
|
32
|
+
__resolve__.__send__(name, *args, **kwargs, &block)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def respond_to_missing?(name, include_private = false)
|
|
36
|
+
name == :__taski_proxy_resolve__ || __resolve__.respond_to?(name, include_private)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def !
|
|
40
|
+
!__resolve__
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def ==(other)
|
|
44
|
+
__resolve__ == other
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def !=(other)
|
|
48
|
+
__resolve__ != other
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def equal?(other)
|
|
52
|
+
__resolve__.equal?(other)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def respond_to?(name, include_private = false)
|
|
56
|
+
name == :__taski_proxy_resolve__ || __resolve__.respond_to?(name, include_private)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
data/lib/taski/test_helper.rb
CHANGED
|
@@ -69,7 +69,7 @@ module Taski
|
|
|
69
69
|
|
|
70
70
|
if MockRegistry.mock_for(task_class)
|
|
71
71
|
wrapper.mark_completed(nil) unless wrapper.completed?
|
|
72
|
-
@completion_queue.push(
|
|
72
|
+
@completion_queue.push(Taski::Execution::FiberProtocol::TaskCompleted.new(task_class, wrapper))
|
|
73
73
|
return
|
|
74
74
|
end
|
|
75
75
|
|
data/lib/taski/version.rb
CHANGED
data/lib/taski.rb
CHANGED
|
@@ -4,6 +4,8 @@ require_relative "taski/version"
|
|
|
4
4
|
require_relative "taski/static_analysis/analyzer"
|
|
5
5
|
require_relative "taski/static_analysis/visitor"
|
|
6
6
|
require_relative "taski/static_analysis/dependency_graph"
|
|
7
|
+
require_relative "taski/static_analysis/start_dep_analyzer"
|
|
8
|
+
require_relative "taski/execution/fiber_protocol"
|
|
7
9
|
require_relative "taski/execution/registry"
|
|
8
10
|
require_relative "taski/execution/task_observer"
|
|
9
11
|
require_relative "taski/execution/execution_facade"
|
|
@@ -14,6 +16,7 @@ require_relative "taski/execution/executor"
|
|
|
14
16
|
require_relative "taski/progress/layout/log"
|
|
15
17
|
require_relative "taski/progress/layout/simple"
|
|
16
18
|
require_relative "taski/progress/layout/tree"
|
|
19
|
+
require_relative "taski/progress/config"
|
|
17
20
|
require_relative "taski/args"
|
|
18
21
|
require_relative "taski/env"
|
|
19
22
|
require_relative "taski/logging"
|
|
@@ -265,14 +268,28 @@ module Taski
|
|
|
265
268
|
reset_args! if created_args
|
|
266
269
|
end
|
|
267
270
|
|
|
268
|
-
NOT_CONFIGURED = Object.new.freeze
|
|
269
271
|
PROGRESS_MONITOR = Monitor.new
|
|
270
|
-
|
|
272
|
+
PROGRESS_NOT_SET = Object.new.freeze
|
|
273
|
+
@progress_display = PROGRESS_NOT_SET
|
|
274
|
+
@progress_config = Progress::Config.new {
|
|
275
|
+
PROGRESS_MONITOR.synchronize do
|
|
276
|
+
unless @progress_display.equal?(PROGRESS_NOT_SET)
|
|
277
|
+
@progress_display.stop if @progress_display.respond_to?(:stop)
|
|
278
|
+
end
|
|
279
|
+
@progress_display = PROGRESS_NOT_SET
|
|
280
|
+
end
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
# Get the progress configuration singleton.
|
|
284
|
+
# @return [Progress::Config]
|
|
285
|
+
def self.progress
|
|
286
|
+
PROGRESS_MONITOR.synchronize { @progress_config }
|
|
287
|
+
end
|
|
271
288
|
|
|
272
289
|
def self.progress_display
|
|
273
290
|
PROGRESS_MONITOR.synchronize do
|
|
274
|
-
if @progress_display.equal?(
|
|
275
|
-
@progress_display =
|
|
291
|
+
if @progress_display.equal?(PROGRESS_NOT_SET)
|
|
292
|
+
@progress_display = @progress_config.build
|
|
276
293
|
end
|
|
277
294
|
@progress_display
|
|
278
295
|
end
|
|
@@ -280,7 +297,7 @@ module Taski
|
|
|
280
297
|
|
|
281
298
|
def self.progress_display=(display)
|
|
282
299
|
PROGRESS_MONITOR.synchronize do
|
|
283
|
-
unless @progress_display.equal?(
|
|
300
|
+
unless @progress_display.equal?(PROGRESS_NOT_SET)
|
|
284
301
|
@progress_display.stop if @progress_display.respond_to?(:stop)
|
|
285
302
|
end
|
|
286
303
|
@progress_display = display
|
|
@@ -289,10 +306,11 @@ module Taski
|
|
|
289
306
|
|
|
290
307
|
def self.reset_progress_display!
|
|
291
308
|
PROGRESS_MONITOR.synchronize do
|
|
292
|
-
unless @progress_display.equal?(
|
|
309
|
+
unless @progress_display.equal?(PROGRESS_NOT_SET)
|
|
293
310
|
@progress_display.stop if @progress_display.respond_to?(:stop)
|
|
294
311
|
end
|
|
295
|
-
@progress_display =
|
|
312
|
+
@progress_display = PROGRESS_NOT_SET
|
|
313
|
+
@progress_config.reset
|
|
296
314
|
end
|
|
297
315
|
end
|
|
298
316
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: taski
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.9.
|
|
4
|
+
version: 0.9.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ahogappa
|
|
@@ -9,6 +9,20 @@ bindir: exe
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: base64
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
12
26
|
- !ruby/object:Gem::Dependency
|
|
13
27
|
name: liquid
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -84,6 +98,7 @@ files:
|
|
|
84
98
|
- lib/taski/env.rb
|
|
85
99
|
- lib/taski/execution/execution_facade.rb
|
|
86
100
|
- lib/taski/execution/executor.rb
|
|
101
|
+
- lib/taski/execution/fiber_protocol.rb
|
|
87
102
|
- lib/taski/execution/registry.rb
|
|
88
103
|
- lib/taski/execution/scheduler.rb
|
|
89
104
|
- lib/taski/execution/task_observer.rb
|
|
@@ -92,6 +107,7 @@ files:
|
|
|
92
107
|
- lib/taski/execution/task_wrapper.rb
|
|
93
108
|
- lib/taski/execution/worker_pool.rb
|
|
94
109
|
- lib/taski/logging.rb
|
|
110
|
+
- lib/taski/progress/config.rb
|
|
95
111
|
- lib/taski/progress/layout/base.rb
|
|
96
112
|
- lib/taski/progress/layout/filters.rb
|
|
97
113
|
- lib/taski/progress/layout/log.rb
|
|
@@ -99,6 +115,9 @@ files:
|
|
|
99
115
|
- lib/taski/progress/layout/tags.rb
|
|
100
116
|
- lib/taski/progress/layout/theme_drop.rb
|
|
101
117
|
- lib/taski/progress/layout/tree.rb
|
|
118
|
+
- lib/taski/progress/layout/tree/event.rb
|
|
119
|
+
- lib/taski/progress/layout/tree/live.rb
|
|
120
|
+
- lib/taski/progress/layout/tree/structure.rb
|
|
102
121
|
- lib/taski/progress/theme/base.rb
|
|
103
122
|
- lib/taski/progress/theme/compact.rb
|
|
104
123
|
- lib/taski/progress/theme/default.rb
|
|
@@ -106,8 +125,10 @@ files:
|
|
|
106
125
|
- lib/taski/progress/theme/plain.rb
|
|
107
126
|
- lib/taski/static_analysis/analyzer.rb
|
|
108
127
|
- lib/taski/static_analysis/dependency_graph.rb
|
|
128
|
+
- lib/taski/static_analysis/start_dep_analyzer.rb
|
|
109
129
|
- lib/taski/static_analysis/visitor.rb
|
|
110
130
|
- lib/taski/task.rb
|
|
131
|
+
- lib/taski/task_proxy.rb
|
|
111
132
|
- lib/taski/test_helper.rb
|
|
112
133
|
- lib/taski/test_helper/errors.rb
|
|
113
134
|
- lib/taski/test_helper/minitest.rb
|