Class: Everywhere::TaskPool
- Inherits:
-
Object
- Object
- Everywhere::TaskPool
- Defined in:
- lib/everywhere/task_pool.rb
Overview
Named background tasks for the every dev key loop.
Three guarantees, each of which fixes a specific way the old synchronous command misbehaved:
* At most one live task per name. Pressing `i` twice must not run two
iOS builds over the same staged work dir, which Builders::Ios rm_rf's
and re-copies on every run.
* No task failure can reach the main thread. A Swift compile error used to
unwind through the key loop into Dev#call's `ensure teardown` and take
the Rails server down with it.
* Every task is cancellable. Its children lead their own process groups
and are TERMed (then KILLed) at shutdown.
Constant Summary collapse
- GRACE =
A task's failure is data, not control flow — hence
rescue Exception, one of the few places it is the correct choice. MRI enqueues a SystemExit that escapes a non-main thread onto the MAIN thread and re-raises it there, wherever that thread happens to be. Since UI.die! raises Everywhere::Fatal (a SystemExit),rescue StandardErrorhere would leave the original bug fully intact, just relocated. Verified:ruby -e 'Thread.new { raise SystemExit.new(2) }; sleep 1; puts "alive"' # => exits 2, never prints 5
Instance Method Summary collapse
- #any_running? ⇒ Boolean
- #busy?(name) ⇒ Boolean
- #failed ⇒ Object
-
#initialize(on_error: nil) ⇒ TaskPool
constructor
A new instance of TaskPool.
-
#shutdown(grace: GRACE) ⇒ Object
Kill the tasks' children first, then the threads.
-
#start(name, &block) ⇒ Object
:started, :busy or :stopping.
-
#wait_all ⇒ Object
Join every live task.
Constructor Details
#initialize(on_error: nil) ⇒ TaskPool
Returns a new instance of TaskPool.
32 33 34 35 36 37 38 |
# File 'lib/everywhere/task_pool.rb', line 32 def initialize(on_error: nil) @lock = Mutex.new @threads = {} @failed = [] @stopping = false @on_error = on_error end |
Instance Method Details
#any_running? ⇒ Boolean
56 |
# File 'lib/everywhere/task_pool.rb', line 56 def any_running? = @lock.synchronize { @threads.each_value.any?(&:alive?) } |
#busy?(name) ⇒ Boolean
52 |
# File 'lib/everywhere/task_pool.rb', line 52 def busy?(name) = @lock.synchronize { !!@threads[name]&.alive? } |
#failed ⇒ Object
54 |
# File 'lib/everywhere/task_pool.rb', line 54 def failed = @lock.synchronize { @failed.dup } |
#shutdown(grace: GRACE) ⇒ Object
Kill the tasks' children first, then the threads. Ordering matters: a worker blocked reading a build's output only unwinds once that build is gone, so signalling the children is what actually makes the joins return.
73 74 75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/everywhere/task_pool.rb', line 73 def shutdown(grace: GRACE) live = @lock.synchronize do @stopping = true @threads.each_value.select(&:alive?) end return if live.empty? live.each { |thread| ChildProcesses.terminate(thread, grace: grace) } deadline = now + grace live.each { |thread| thread.join([deadline - now, 0].max) } live.select(&:alive?).each(&:kill) Console.multiplexed = false end |
#start(name, &block) ⇒ Object
:started, :busy or :stopping.
41 42 43 44 45 46 47 48 49 50 |
# File 'lib/everywhere/task_pool.rb', line 41 def start(name, &block) @lock.synchronize do return :stopping if @stopping return :busy if @threads[name]&.alive? @threads[name] = spawn_worker(name, &block) retag :started end end |
#wait_all ⇒ Object
Join every live task. Used by the headless path, where there is no key loop to keep the process alive and a failed build still has to be news.
60 61 62 63 64 65 66 67 68 |
# File 'lib/everywhere/task_pool.rb', line 60 def wait_all loop do live = @lock.synchronize { @threads.each_value.select(&:alive?) } break if live.empty? live.each(&:join) end Console.multiplexed = false end |