Skip to content
Open
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_test_driver",
srcs = ["control_client_test_driver.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_test_driver",
":filesystem_reader",
":setup_filesystem.sh",
":slow_setup.sh",
"//score/launch_manager",
],
config = ":rt_running_when_process_exits.json",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/********************************************************************************
* 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
********************************************************************************/
#include <gtest/gtest.h>

#include <filesystem>
#include <string_view>

#include "tests/utils/test_helper/test_helper.hpp"
#include <score/mw/lifecycle/control_client.h>
#include <score/mw/lifecycle/report_running.h>

namespace
{
/// @brief Marker file written by slow_setup.sh once it has finished (and is about to exit).
constexpr std::string_view kSlowSetupOutput = "slow_setup_output.txt";
} // namespace

// Given a configuration with two run targets, each pulling in a self-terminating component whose
// ready condition is "Terminated" but which differ in whether that component has a dependent:
//
// - run_target_reader: filesystem_reader (ready "Running") depends on setup_filesystem_sh
// (self-terminating, ready "Terminated"). The terminated-ready
// component HAS a dependent.
// - run_target_slow_setup: depends directly on slow_setup_sh (self-terminating, ready
// "Terminated") which has NO dependent component.
//
// In both cases the run target must only report success once the terminated-ready component's
// process has actually exited. Without the fix, graph accounting for such a node happens as soon as
// the process is *started*, so ActivateRunTarget(...).Get() returns while the script is still
// running and its marker file has not been written yet.
TEST(RtRunningWhenProcessExits, ControlClientTestDriver)
{
score::mw::lifecycle::ControlClient client;
score::cpp::stop_token stop_token;

ASSERT_TRUE(check_clean({test_end_location}));
// The marker file may be left over from a previous run when executing manually on the host.
// Remove it so that its presence is a reliable signal that slow_setup.sh terminated during
// *this* run.
ASSERT_TRUE(check_clean({kSlowSetupOutput}, /*strict=*/false));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for the CI strict should always be kept as true?


TEST_STEP("Report running")
{
score::mw::lifecycle::report_running();
}

// The with-dependents case: filesystem_reader asserts on the prepared file and on the setup
// script process being gone, so the ordering is checked there.
TEST_STEP("Activate run target with a terminated-ready component that HAS a dependent")
{
auto result = client.ActivateRunTarget("run_target_reader").Get(stop_token);
EXPECT_TRUE(result.has_value()) << "Activating run_target_reader failed: " << result.error().Message();
}

// The no-dependents case: activation must only complete once slow_setup.sh has terminated.
TEST_STEP("Activate run target with a terminated-ready component that has NO dependent")
{
auto result = client.ActivateRunTarget("run_target_slow_setup").Get(stop_token);
EXPECT_TRUE(result.has_value()) << "Activating run_target_slow_setup failed: " << result.error().Message();
}

TEST_STEP("Verify slow_setup.sh had terminated before activation completed")
{
EXPECT_TRUE(std::filesystem::exists(kSlowSetupOutput))
<< "run_target_slow_setup reported success while slow_setup.sh was still running: its "
"output file has not been written yet. A run target depending on a terminated-ready "
"component must only become ready once that component's process has actually exited.";
}

TEST_STEP("Activate run target Off")
{
client.ActivateRunTarget("Off");
}
}

int main()
{
return TestRunner(__FILE__, TerminationBehavior::kWait, TerminationNotification::kTestEnd).RunTests();
}
Loading
Loading