1
1
mirror of https://github.com/NixOS/mobile-nixos.git synced 2024-12-18 05:21:47 +03:00
mobile-nixos/boot/init/lib/task.rb
Samuel Dionne-Riel 00f81fa95c boot/init: Handle hung tasks
The way we're handling them is to have a global timer that is reset at
any point a task is ran.

This gives a maximum amount of chances to any task to have its
dependencies resolve.

A minimum of 60s is given, but in reality the chances are the conditions
for trying to resolve were already present before the timeout started
counting towards that particular dependency.

Note that a long running task, when successfully ran, does not cause the
timeout to be reached.

E.g. at 10s of timeout a task is started, the loop is not executed until
the task exits. When it exits the branch followed is for a task that
ran, which means that even if the task took 70s total (which gives us 80
seconds) a timeout of 60s wouldn't apply here.

Though, please, don't make your tasks take that much time to run!
2020-11-07 20:13:31 -05:00

210 lines
5.9 KiB
Ruby

# Namespace where tasks can be defined, and hosting methods harmonizing a run.
module Tasks
HUNG_BOOT_NOTIFICATION = 3 # seconds
HUNG_BOOT_TIMEOUT = 60 # seconds
# Register a singleton task to be instantiated and ran.
# @internal
def self.register_singleton(klass)
$logger.debug("Task #{klass.name} registered...")
@singletons_to_be_instantiated ||= []
@singletons_to_be_instantiated << klass
end
# Register one task to be ran.
def self.register(task)
$logger.debug("Task #{task.name} registered...")
@tasks ||= []
@tasks << task
end
# Tries to run *all* tasks.
def self.go()
# Registers tasks that still needs to be instantiated
@singletons_to_be_instantiated.each do |klass|
# Their mere existence registers them.
klass.instance
end
@singletons_to_be_instantiated = []
# Sort tasks to reduce the amount of loops it needs to fulfill them all.
# It's only a reduction due to files, mounts and devices being
# unpredictable!
@tasks.sort!
hung_tasks_timer = Time.now
until @tasks.all?(&:ran) do
$logger.debug("=== Tasks resolution loop start ===")
ran_one = false
todo = @tasks
.reject(&:ran)
.tap do |tasks|
$logger.debug(" Tasks order:")
tasks.each do |t|
$logger.debug(" - #{t}")
end
end
# Update the current progress
count = @tasks.length.to_f
Progress.update({progress: (100 * (1 - (todo.length / count))).ceil})
todo.each do |task|
if task._try_run_task then
ran_one = true
$logger.debug("#{task} ran.")
break
end
end
if ran_one
# Reset the timer
hung_tasks_timer = Time.now
# And reset the hung state in the progress UI
Progress.update({label: nil, hung: nil})
else
elapsed = Time.now - hung_tasks_timer
$logger.debug("Time elapsed since something ran: #{elapsed}")
# Any tasks, not currently depending on another task, that have yet
# to be ran.
# Serves nothing to point to tasks depending on other tasks.
failed_tasks = todo.reject(&:depends_on_any_unfulfilled_task?)
failed_dependencies = failed_tasks.map(&:dependencies).inject(:+).uniq
if elapsed > HUNG_BOOT_NOTIFICATION
label = "#{failed_tasks.length} tasks are waiting on #{failed_dependencies.length} unique dependencies.\n\n" +
"(#{(HUNG_BOOT_TIMEOUT - elapsed).ceil} seconds left until boot is aborted.)"
Progress.update({label: label, hung: elapsed})
end
if elapsed > HUNG_BOOT_TIMEOUT
# Building this message is not pretty!
msg =
"#{failed_tasks.length} #{if failed_tasks.length == 1 then "task" else "tasks" end} " +
"did not run within #{HUNG_BOOT_TIMEOUT} seconds.\n" +
"\n" +
"#{failed_dependencies.length} #{if failed_dependencies.length == 1 then "dependency" else "dependencies" end} " +
"could not resolve:\n" +
failed_dependencies.map(&:pretty_name).join("\n") +
"\n"
# Fail with a black backdrop, and force the message to stay up 60s
System.failure("hung_tasks", msg, color: "000000", delay: 60)
end
# Don't burn the CPU if we're waiting on something...
$logger.debug("Sleeping")
sleep(0.1)
end
end
end
end
# Basic task class.
class Task
attr_reader :ran
def self.new(*args)
$logger.debug("New instance of #{self.name}...")
$logger.debug(" -> #{args.inspect}")
instance = super(*args)
Tasks.register(instance)
instance
end
def name
self.class.name
end
def self.inherited(subclass)
$logger.debug("#{subclass.name} created...")
end
# Sort first by dependencies, then by name, then by object_id
# (for stable sort order)
def <=>(other)
return -1 if other.depends_on?(self)
return 1 if depends_on?(other)
by_ux_priority = ux_priority <=> other.ux_priority
return by_ux_priority unless by_ux_priority == 0
by_name = name <=> other.name
return by_name unless by_name == 0
object_id <=> other.object_id
end
def depends_on?(other)
dependencies.any? do |dependency|
dependency.depends_on?(other)
end
end
def add_dependency(kind, *args)
raise NameError.new("No dependency named #{kind}") unless Dependencies.constants.include?(kind.to_sym)
dependencies << Dependencies.const_get(kind.to_sym).new(*args)
end
def dependencies_fulfilled?()
dependencies.all?(&:fulfilled?)
end
def depends_on_any_unfulfilled_task?()
dependencies.reject(&:fulfilled?).any? do |dep|
dep.is_a?(Dependencies::Task) or
dep.is_a?(Dependencies::Target)
end
end
# Internal actual way to run the task
# This runs the `#run` method.
# Returns true when the task was ran.
def _try_run_task()
$logger.debug("Looking to run task #{name}...")
return unless dependencies_fulfilled?
unless @ran
$logger.info("Running #{name}...")
run()
$logger.debug("Finished #{name}...")
@ran = true
end
@ran
end
def dependencies()
@dependencies ||= []
@dependencies
end
# This allows a task to be ordered before other tasks, because it is used to
# enhance the UX of the boot process. Assume this will be compared with +<=>+.
# This should seldom be used, and mainly for tasks that show the progress of
# the boot process.
# (For internal use.)
# @internal
def ux_priority()
0
end
# Same as name, but with the object_id appended.
def to_s()
name + "<0x#{object_id.to_s(16)}>"
end
end
# A task that can only have one instance.
class SingletonTask < Task
include Singleton
def self.inherited(subclass)
super
# Delay initializing, as right now we have an fresh new empty class.
Tasks.register_singleton(subclass)
end
end