Class: Everywhere::TaskPool

Inherits:
Object
  • Object
show all
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 StandardError here 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

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

Returns:

  • (Boolean)


56
# File 'lib/everywhere/task_pool.rb', line 56

def any_running? = @lock.synchronize { @threads.each_value.any?(&:alive?) }

#busy?(name) ⇒ Boolean

Returns:

  • (Boolean)


52
# File 'lib/everywhere/task_pool.rb', line 52

def busy?(name) = @lock.synchronize { !!@threads[name]&.alive? }

#failedObject



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_allObject

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