Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: capture errors that shouldnt cause the whole plan to be discarded during network resolution #444

Open
wants to merge 9 commits into
base: transition-to-runkit
Choose a base branch
from
15 changes: 12 additions & 3 deletions lib/syskit/cli/doc/gen.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,20 @@ def self.save_profile_model(target_path, profile_m)
# @param [Actions::Profile::Definition] profile_def
def self.save_profile_definition(target_path, profile_def)
begin
task = compute_system_network(
task, resolution_errors = compute_system_network(
profile_def,
validate_abstract_network: false,
validate_generated_network: false
)
unless resolution_errors.empty?
raise NetworkGeneration::PartialNetworkResolution,
resolution_errors
end

hierarchy, dataflow =
save_profile_definition_graphs(target_path, task, profile_def)
.map(&:to_s)
rescue StandardError => e
rescue StandardError, NetworkGeneration::PartialNetworkResolution => e
Roby.warn "could not generate profile definition graph for " \
"#{profile_def.name}"
Roby.log_exception_with_backtrace(e, Roby, :warn)
Expand Down Expand Up @@ -266,7 +270,12 @@ def self.compute_system_network(model, main_plan = Roby::Plan.new, **options)
main_plan.add(original_task = model.as_plan)
engine = Syskit::NetworkGeneration::Engine.new(main_plan)
planning_task = original_task.planning_task
mapping = engine.compute_system_network([planning_task], **options)
mapping, resolution_errors =
engine.compute_system_network([planning_task], **options)
unless resolution_errors.empty?
raise Syskit::NetworkGeneration::PartialNetworkResolution,
resolution_errors
end

if engine.work_plan.respond_to?(:commit_transaction)
engine.work_plan.commit_transaction
Expand Down
146 changes: 74 additions & 72 deletions lib/syskit/exceptions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -483,113 +483,115 @@ def pretty_print(pp)
end
end

# Exception raised at the end of #resolve if some tasks do not have a
# Exception raised at the end of #resolve if a task does not have a
# deployed equivalent
class MissingDeployments < SpecError
# The tasks that are not deployed, as a hash from the actual task to
class MissingDeployment < SpecError
# The tasks that is not deployed, as a hash from the actual task to
# a set of [role_set, parent_task] pairs
#
# This is computed in #initialize as the dependency structure will
# probably change afterwards
attr_reader :tasks

# The task that is not deployed
attr_reader :task

# Parent tasks of the not deployed task
attr_reader :parents
# All the available deployment candidates
attr_reader :candidates
# The deployment hints of the not deployed task
attr_reader :deployment_hints

# Initializes this exception by providing a mapping from tasks that
# have no deployments to the deployment candidates
# have no deployments to the deployment candidates, and the specific task the
# error refers to.
#
# @param [Hash{TaskContext=>[Array<Model<Deployment>>]}] tasks_with_candidates
def initialize(tasks_with_candidates)
@tasks = {}
tasks_with_candidates.each do |task_to_deploy, candidates|
parents = task_to_deploy.dependency_context
candidates = candidates.map do |deployed_task, existing_tasks|
existing_tasks = existing_tasks.map do |task|
[task, task.dependency_context]
end
[deployed_task, existing_tasks]
def initialize(task, candidates)
@task = task
@parents = task.dependency_context
@candidates = candidates.map do |deployed_task, existing_tasks|
existing_tasks = existing_tasks.map do |task|
[task, task.dependency_context]
end

@tasks[task_to_deploy] = [
parents, candidates, task_to_deploy.deployment_hints
]
[deployed_task, existing_tasks]
end
@deployment_hints = task.deployment_hints
end

def pretty_print(pp)
pp.text "cannot deploy the following tasks"
tasks.each do |task, (parents, possible_deployments)|
pp.text "cannot deploy the following task"
pp.breakable
pp.text "#{task} (#{task.orogen_model.name})"
pp.nest(2) do
pp.breakable
pp.text "#{task} (#{task.orogen_model.name})"
pp.nest(2) do
pp.breakable
pp.seplist(parents) do |parent_task|
role, parent_task = parent_task
pp.text "child #{role} of #{parent_task}"
end
pp.seplist(parents) do |parent_task|
role, parent_task = parent_task
pp.text "child #{role} of #{parent_task}"
end
end

tasks.each do |task, (_parents, possible_deployments, deployment_hints)|
has_free_deployment = possible_deployments.any? { |_, existing| existing.empty? }
pp.breakable
if has_free_deployment
pp.text "#{task}: multiple possible deployments, choose one with #prefer_deployed_tasks(deployed_task_name)"
unless deployment_hints.empty?
deployment_hints.each do |hint|
pp.text " current hints: #{deployment_hints.map(&:to_s).join(', ')}"
end
has_free_deployment = candidates.any? { |_, existing| existing.empty? }
pp.breakable
if has_free_deployment
pp.text "#{task}: multiple possible deployments, choose one with #prefer_deployed_tasks(deployed_task_name)"
unless deployment_hints.empty?
deployment_hints.each do |hint|
pp.text " current hints: #{deployment_hints.map(&:to_s).join(', ')}"
end
elsif possible_deployments.empty?
pp.text "#{task}: no deployments available"
else
pp.text "#{task}: some deployments exist, but they are already used in this network"
end
elsif candidates.empty?
pp.text "#{task}: no deployments available"
else
pp.text "#{task}: some deployments exist, but they are already used in this network"
end

pp.nest(2) do
possible_deployments.each do |deployed_task, existing|
pp.breakable
process_server_name = deployed_task.configured_deployment
.process_server_name
orogen_model = deployed_task.configured_deployment
.orogen_model
pp.text(
"task #{deployed_task.mapped_task_name} from deployment " \
"#{orogen_model.name} defined in " \
"#{orogen_model.project.name} on #{process_server_name}"
)
pp.nest(2) do
existing.each do |task, parents|
pp.breakable
msg = parents.map do |parent_task|
role, parent_task = *parent_task
"child #{role} of #{parent_task}"
end
pp.text "already used by #{task}: #{msg.join(', ')}"
pp.nest(2) do
candidates.each do |deployed_task, existing|
pp.breakable
process_server_name = deployed_task.configured_deployment
.process_server_name
orogen_model = deployed_task.configured_deployment
.orogen_model
pp.text(
"task #{deployed_task.mapped_task_name} from deployment " \
"#{orogen_model.name} defined in " \
"#{orogen_model.project.name} on #{process_server_name}"
)
pp.nest(2) do
existing.each do |task, parents|
pp.breakable
msg = parents.map do |parent_task|
role, parent_task = *parent_task
"child #{role} of #{parent_task}"
end
pp.text "already used by #{task}: #{msg.join(', ')}"
end
end
end
end
end
end

# Exception raised at the end of #resolve if some tasks are referring to non-existing
# configurations
# Exception raised at the end of #resolve if a task is referring to non-existing
# configuration
class MissingConfigurationSection < SpecError
# Association of tasks and the sections that are missing
attr_reader :missing_sections_by_task
# The sections that are missing
attr_reader :missing_sections
# The task with a missing configuration section
attr_reader :task

def initialize(missing_sections_by_task)
@missing_sections_by_task = missing_sections_by_task
def initialize(task, missing_sections)
@task = task
@missing_sections = missing_sections
end

def pretty_print(pp)
pp.text "the following configuration sections are used but do not exist"
@missing_sections_by_task.each do |task, sections|
pp.breakable
pp.text "'#{sections.join('\', \'')}', in use by:"
pp.breakable
pp.text " #{task} (#{task.orogen_model.name})"
end
pp.breakable
pp.text "'#{missing_sections.join('\', \'')}', in use by:"
pp.breakable
pp.text " #{task} (#{task.orogen_model.name})"
end
end

Expand Down
10 changes: 8 additions & 2 deletions lib/syskit/gui/component_network_view.rb
Original file line number Diff line number Diff line change
Expand Up @@ -150,11 +150,17 @@ def render(model,
begin
if method == :compute_system_network
tic = Time.now
@task = compute_system_network(model, plan)
@task, resolution_errors = compute_system_network(model, plan)
exception = NetworkGeneration::PartialNetworkResolution.new(
resolution_errors
)
timing = Time.now - tic
elsif method == :compute_deployed_network
tic = Time.now
@task = compute_deployed_network(model, plan)
@task, resolution_errors = compute_deployed_network(model, plan)
exception = NetworkGeneration::PartialNetworkResolution.new(
resolution_errors
)
timing = Time.now - tic
else
@task = instanciate_model(model, plan, instanciate_options)
Expand Down
1 change: 1 addition & 0 deletions lib/syskit/network_generation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ module NetworkGeneration
require "syskit/network_generation/engine"
require "syskit/network_generation/async"
require "syskit/network_generation/logger"
require "syskit/network_generation/exceptions"
12 changes: 9 additions & 3 deletions lib/syskit/network_generation/async.rb
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ def cancelled?

class InvalidState < RuntimeError; end

SystemNetworktoPlanApplyResult =
Struct.new :success, :instances, :errors, keyword_init: true

# Apply the result of the generation
#
# @return [Boolean] true if the result has been applied, and false
Expand All @@ -133,14 +136,17 @@ def apply
engine = future.engine
if @cancelled
engine.discard_work_plan
false
SystemNetworktoPlanApplyResult.new(success: false)
elsif future.fulfilled?
required_instances = future.value
required_instances, resolution_errors = future.value
begin
engine.apply_system_network_to_plan(
required_instances, **@apply_system_network_options
)
true
SystemNetworktoPlanApplyResult.new(
success: true, instances: required_instances,
errors: resolution_errors
)
rescue ::Exception => e
engine.handle_resolution_exception(e, on_error: Engine.on_error)
raise e
Expand Down
Loading