Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ PgManagerConfig ConfigurationAdapter::buildPgManagerConfig(const ComponentConfig
const auto& props = comp.component_properties;

pgm.is_self_terminating_ = props.application_profile.is_self_terminating;
pgm.ready_on_termination_ =
props.ready_condition.has_value() && (props.ready_condition->process_state == ProcessState::Terminated);
pgm.startup_timeout_ms_ = std::chrono::milliseconds(deploy.ready_timeout_ms);
pgm.termination_timeout_ms_ = std::chrono::milliseconds(deploy.shutdown_timeout_ms);
pgm.execution_error_code_ = kDefaultProcessExecutionError;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ namespace score::mw::lifecycle::internal::configuration
struct PgManagerConfig final
{
bool is_self_terminating_{};
bool ready_on_termination_{};
std::chrono::milliseconds startup_timeout_ms_{};
std::chrono::milliseconds termination_timeout_ms_{};
uint32_t number_of_restart_attempts{};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ TEST_F(ConfigurationAdapterTest, GetOsProcessConfigurationMapsComponentFields)
EXPECT_THAT(os_proc->startup_config_.uid_, Eq(1000U));
EXPECT_THAT(os_proc->startup_config_.gid_, Eq(1000U));
EXPECT_THAT(os_proc->pgm_config_.is_self_terminating_, Eq(false));
EXPECT_THAT(os_proc->pgm_config_.ready_on_termination_, Eq(false));
EXPECT_THAT(os_proc->pgm_config_.startup_timeout_ms_, Eq(std::chrono::milliseconds{500}));
EXPECT_THAT(os_proc->pgm_config_.termination_timeout_ms_, Eq(std::chrono::milliseconds{500}));
}
Expand Down Expand Up @@ -493,6 +494,86 @@ TEST(ConfigurationAdapterReadyConditionTest, DependencyDefaultsToRunningWhenTarg
adapter.deinitialize();
}

TEST(ConfigurationAdapterReadyConditionTest, ReadyOnTerminationUsesOwnReadyConditionNotDependencies)
{
RecordProperty(
"Description",
"pgm_config_.ready_on_termination_ is derived from the component's own ready_condition, independently of the "
"ready conditions reached through its dependencies.");
RecordProperty("TestType", "interface-test");
RecordProperty("DerivationTechnique", "explorative-testing");

ComponentConfig comp_a;
comp_a.name = "comp_a";
comp_a.component_properties.application_profile.application_type = ApplicationType::Native;
comp_a.component_properties.application_profile.is_self_terminating = true;
comp_a.component_properties.ready_condition = ReadyCondition{ProcessState::Terminated};
comp_a.deployment_config.bin_dir = "/opt";
comp_a.component_properties.binary_name = "comp_a";
comp_a.deployment_config.working_dir = "/tmp";
comp_a.deployment_config.sandbox.uid = 0;
comp_a.deployment_config.sandbox.gid = 0;
comp_a.deployment_config.sandbox.scheduling_policy = SCHED_OTHER;
comp_a.deployment_config.sandbox.scheduling_priority = 0;

ComponentConfig comp_b;
comp_b.name = "comp_b";
comp_b.component_properties.application_profile.application_type = ApplicationType::Native;
comp_b.component_properties.application_profile.is_self_terminating = false;
comp_b.component_properties.ready_condition = ReadyCondition{ProcessState::Running};
comp_b.component_properties.depends_on = {"comp_a"};
comp_b.deployment_config.bin_dir = "/opt";
comp_b.component_properties.binary_name = "comp_b";
comp_b.deployment_config.working_dir = "/tmp";
comp_b.deployment_config.sandbox.uid = 0;
comp_b.deployment_config.sandbox.gid = 0;
comp_b.deployment_config.sandbox.scheduling_policy = SCHED_OTHER;
comp_b.deployment_config.sandbox.scheduling_priority = 0;

std::vector<ComponentConfig> components;
components.push_back(std::move(comp_a));
components.push_back(std::move(comp_b));

RunTargetConfig startup;
startup.name = "Startup";
startup.depends_on = {"comp_b"};
startup.transition_timeout_ms = 5000;
startup.recovery_action.run_target = "fallback_run_target";

std::vector<RunTargetConfig> run_targets;
run_targets.push_back(std::move(startup));

FallbackRunTargetConfig fallback;
fallback.transition_timeout_ms = 1500;
AliveSupervisionConfig alive;
alive.evaluation_cycle_ms = 500;

auto config = ConfigBuilder{}
.setComponents(std::move(components))
.setRunTargets(std::move(run_targets))
.setInitialRunTarget("Startup")
.setFallbackRunTarget(std::move(fallback))
.setAliveSupervision(alive)
.build();

ConfigurationAdapter adapter;
adapter.initialize(config);

IdentifierHash pg_name{"MainPG"};

auto comp_a_result = adapter.getOsProcessConfiguration(pg_name, 0U);
ASSERT_TRUE(comp_a_result.has_value());
EXPECT_THAT((*comp_a_result)->pgm_config_.ready_on_termination_, Eq(true))
<< "comp_a declares ready_condition Terminated, even though it has no dependencies";

auto comp_b_result = adapter.getOsProcessConfiguration(pg_name, 1U);
ASSERT_TRUE(comp_b_result.has_value());
EXPECT_THAT((*comp_b_result)->pgm_config_.ready_on_termination_, Eq(false))
<< "comp_b declares ready_condition Running, even though it depends on a Terminated component";

adapter.deinitialize();
}

TEST(ConfigurationAdapterFallbackTest, FallbackRunTargetResolvesDependenciesRecursively)
{
RecordProperty("Description", "Fallback run target resolves transitive component dependencies.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,6 @@ void Graph::createProcessInfoNodes(uint32_t num_processes)
for (uint32_t process_id = 0U; process_id < num_processes; ++process_id)
{
LM_LOG_DEBUG() << "Creating process node with id:" << process_id;
auto ready_condition = nodeHasTerminatedDeps(getProcessGroupName(), process_id)
? ProcessInfoNode::ReadyCondition::kTerminated
: ProcessInfoNode::ReadyCondition::kRunning;

const auto* config =
configuration_->getOsProcessConfiguration(getProcessGroupName(), process_id).value_or(nullptr);
Expand All @@ -101,6 +98,10 @@ void Graph::createProcessInfoNodes(uint32_t num_processes)
<< getProcessGroupName();
}

const auto ready_condition = (config && config->pgm_config_.ready_on_termination_)
? ProcessInfoNode::ReadyCondition::kTerminated
: ProcessInfoNode::ReadyCondition::kRunning;

const auto index = nodes_.emplace(
std::in_place_type<ProcessInfoNode>,
config,
Expand Down Expand Up @@ -155,18 +156,6 @@ int32_t Graph::getRunTargetIndex(IdentifierHash pg_state) const
return -1;
}

bool Graph::nodeHasTerminatedDeps(IdentifierHash pg_name, uint32_t node_index)
{
const DependencyList* dep_list = configuration_->getOsProcessDependencies(pg_name, node_index).value_or(nullptr);

if (dep_list && dep_list->size() > 0)
{
return (*dep_list)[0].process_state_ == ProcessState::kTerminated;
}

return false;
}

void Graph::createSuccessorLists(IdentifierHash pg_name)
{
LM_LOG_DEBUG() << "Creating successor lists for process group" << pg_name;
Expand Down Expand Up @@ -398,7 +387,7 @@ void Graph::handleComponentEvent(const ComponentEvent& event)
using T = std::decay_t<decltype(data)>;
if constexpr (std::is_same_v<T, ActivationSuccessful> || std::is_same_v<T, DeactivationComplete>)
{
LM_LOG_DEBUG() << "Component " << data.node_index << " finished "
LM_LOG_DEBUG() << "Component" << data.node_index << "finished"
<< (std::is_same_v<T, ActivationSuccessful> ? std::string_view("activation")
: std::string_view("deactivation"))
<< " successfully";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -289,9 +289,6 @@ class Graph final
void forceKillProcesses();

private:
/// @brief Helper function to identify a node with ready state "Terminated" from the legacy configuration
bool nodeHasTerminatedDeps(IdentifierHash pg_name, uint32_t node_index);

/// @brief Reports that a node has finished executing, enqueuing successors or updating the graph state if a
/// transition has finished.
void nodeExecuted(uint32_t node, score::cpp::expected_blank<IComponent::ComponentError> error);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ ProcessInfoNode::ProcessInfoNode(

IComponent::RequestResult ProcessInfoNode::tryReportCompletion(score::mw::lifecycle::ProcessState new_state)
{
if (new_state == ProcessState::kFailed)
{
// Didn't reach running or startup
return tryReportError(ComponentError::kErrorBeforeReady);
}

ProcessState desired_state{};
switch (ready_condition_)
{
Expand All @@ -54,12 +60,8 @@ IComponent::RequestResult ProcessInfoNode::tryReportCompletion(score::mw::lifecy
desired_state = ProcessState::kTerminated;
break;
}
if (new_state == ProcessState::kFailed)
{
// Didn't reach running or startup
return tryReportError(ComponentError::kErrorBeforeReady);
}
if (new_state == desired_state)
// NOTE: Make assumptions over the enumeration values of ProcessState
if (new_state >= desired_state)
{
return tryReportSuccess();
}
Expand Down Expand Up @@ -256,7 +258,12 @@ IComponent::RequestResult ProcessInfoNode::startProcess(score::cpp::stop_token s
}

setState(ProcessState::kRunning); // Can fail if we've terminated already
return tryReportCompletion(ProcessState::kRunning);

// A self-terminating process may already have exited before startup completed. tryHandleTermination()
// leaves such a node waiting for the startup thread, so report against the state actually reached.
const ProcessState reached_state =
(getState() == ProcessState::kTerminated) ? ProcessState::kTerminated : ProcessState::kRunning;
return tryReportCompletion(reached_state);
}

void ProcessInfoNode::setupControlClientChannel()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,31 @@ TEST_F(ProcessInfoNodeStartupTest, SelfTerminating_ExitsBeforeMapInsert_ReturnsS
ASSERT_THAT(node->getState(), Eq(score::mw::lifecycle::ProcessState::kTerminated));
}

TEST_F(ProcessInfoNodeStartupTest, SelfTerminating_TerminatedReadyCondition_ExitsBeforeMapInsert_ReturnsSuccess)
{
RecordProperty(
"Description",
"A self-terminating process whose ready condition is kTerminated and that exits with status 0 before the map "
"insertion completes reports success from activate() instead of waiting forever.");

auto node = createProcessInfoNode(osal::CommsType::kNoComms, 0, true, ProcessInfoNode::ReadyCondition::kTerminated);
// Simulate the process exiting before the map insertion happens.
EXPECT_CALL(mock_processIf_, startProcess(_, _, _))
.WillOnce(DoAll(
InvokeWithoutArgs([node = node.get()] {
node->tryHandleTermination(0);
}),
Return(osal::OsalReturnType::kSuccess)));
EXPECT_CALL(*process_map_, insertIfNotTerminated(_, _))
.WillOnce(Return(score::mw::lifecycle::internal::SafeProcessMapReturnType::kYield));

auto result = node->activate(score::cpp::stop_token{});

ASSERT_THAT(result.has_value(), IsTrue());
ASSERT_THAT(result.value(), Eq(IComponent::RequestState::kSuccess));
ASSERT_THAT(node->getState(), Eq(score::mw::lifecycle::ProcessState::kTerminated));
}

TEST_F(ProcessInfoNodeStartupTest, ActivateAlreadyActiveNode_ReturnsSuccess)
{
RecordProperty(
Expand Down
49 changes: 49 additions & 0 deletions tests/integration/rt_running_when_process_exits/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# *******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************
load("@rules_cc//cc:cc_binary.bzl", "cc_binary")
load("//tests/utils/bazel:integration.bzl", "integration_test")

cc_binary(
name = "filesystem_reader",
srcs = ["filesystem_reader.cpp"],
deps = [
"//score/launch_manager:lifecycle_cc",
"//tests/utils/test_helper",
"@googletest//:gtest_main",
],
)

cc_binary(
name = "control_client_mock",
srcs = ["mock_control_client.cpp"],
deps = [
"//score/launch_manager:control_cc",
"//score/launch_manager:lifecycle_cc",
"//tests/utils/test_helper",
"@googletest//:gtest_main",
],
)

integration_test(
name = "rt_running_when_process_exits",
timeout = "short",
srcs = ["rt_running_when_process_exits.py"],
binaries = [
":control_client_mock",
":filesystem_reader",
":setup_filesystem.sh",
":slow_setup.sh",
"//score/launch_manager",
],
config = ":rt_running_when_process_exits.json",
)
Loading
Loading