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,8 +183,9 @@ class BasicControlProvider
if (!result.has_value())
{
SCORE_LANGUAGE_FUTURECPP_ASSERT_MESSAGE(
static_cast<ExecErrc>(*result.error()) == ExecErrc::kActivationInProgress,
"Impossible to communicate errors other than ExecErrc::kActivationInProgress to the client");
static_cast<ExecErrc>(*result.error()) == ExecErrc::kActivationInProgress ||
static_cast<ExecErrc>(*result.error()) == ExecErrc::kShuttingDown,
"Impossible to communicate errors other than kActivationInProgress & kShuttingDown to the client");

response = GetActiveRunTargetResponse{QueryStatus::kNotAvailable, RunTargetName("")};
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -557,21 +557,21 @@ TEST_F(ControlProviderUT, GetActiveRunTarget_UnexpectedError_Aborts)
{
RecordProperty(
"Description",
"If getActiveRunTarget() returns any error other than kActivationInProgress "
"If getActiveRunTarget() returns any error other than kActivationInProgress or kShuttingDown "
"the handler hits an internal assertion and the process terminates, because "
"no other error code can be communicated to the client.");

auto provider = MakeProvider();
ASSERT_TRUE(on_get_active_) << "get_active_run_target handler was not registered";

EXPECT_CALL(graph_, getActiveRunTarget()).WillRepeatedly(Return(MakeUnexpected(ExecErrc::kFailed)));
EXPECT_CALL(graph_, getActiveRunTarget()).WillRepeatedly(Return(MakeUnexpected(ExecErrc::kMetaModelError)));

EXPECT_DEATH(
{
GetActiveRunTargetResponse response{};
on_get_active_(response);
},
"Impossible to communicate errors other than ExecErrc::kActivationInProgress to the client");
"Impossible to communicate errors other than kActivationInProgress & kShuttingDown to the client");
}

// ============================================================================
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,46 @@ void Graph::handleComponentEvent(const ComponentEvent& event)
{
nodeExecuted(data.node_identifier, IComponent::RequestResult::kSuccess);
}
// GetActiveRunTarget & SetRequestedRunTarget are only reached while shutting down,
// when the graph is no longer serving run target requests.
// The caller blocks on the slot, so it must always be completed.
else if constexpr (std::is_same_v<T, GetActiveRunTarget>)
{
if (data.completion != nullptr)
{
data.completion->complete(score::MakeUnexpected(ExecErrc::kFailed));
}
}
else if constexpr (std::is_same_v<T, SetRequestedRunTarget>)
{
if (data.completion != nullptr)
{
data.completion->complete(score::MakeUnexpected(ExecErrc::kFailed));
}
}
},
event);
}

void Graph::unblockComponentEvent(const ComponentEvent& event, const ExecErrc reason)
{
std::visit(
[this, reason](const auto& data) {
using T = std::decay_t<decltype(data)>;
if constexpr (std::is_same_v<T, GetActiveRunTarget>)
{
if (data.completion != nullptr)
{
data.completion->complete(score::MakeUnexpected(reason));
}
}
else if constexpr (std::is_same_v<T, SetRequestedRunTarget>)
{
if (data.completion != nullptr)
{
data.completion->complete(score::MakeUnexpected(reason));
}
}
},
event);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,12 @@ class Graph final
/// @param event The event to process.
void handleComponentEvent(const ComponentEvent& event);

/// @brief Completes the slot attached to certain events.
/// Does not follow through on the request, the event is ignored.
/// @param event The event to complete.
/// @param event The error code containing the reason for ignoring the request.
void unblockComponentEvent(const ComponentEvent& event, const ExecErrc reason);

/// @brief Cancel the current transition because a new state has been requested.
/// Sets the graph state to kAborting.
/// If no jobs are in progress, transitions immediately to kUndefinedState.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,50 @@ TEST_F(GraphOrdinaryTransitionTest, simpleDeactivationTransition)
EXPECT_EQ(graph_->getRequestedRunTarget(), target);
}

TEST_F(GraphOrdinaryTransitionTest, GivenGetActiveRunTargetEvent_WhenHandleComponentEventCalled_ThenSlotIsCompleted)
{
RecordProperty(
"Description",
"Given a GetActiveRunTarget event. "
"When the event is passed to handleComponentEvent(). "
"Then the slot is completed with an error. "
"And the wait() does not block.");

// Given
CompletionSlot<Result<IdentifierHash>> slot;
auto event = GetActiveRunTarget{&slot};

// When
graph_->handleComponentEvent(event);

// Then
auto completion = slot.wait();
ASSERT_FALSE(completion.has_value());
EXPECT_EQ(completion.error(), ExecErrc::kFailed);
}

TEST_F(GraphOrdinaryTransitionTest, GivenSetRequestedRunTarget_WhenHandleComponentEventCalled_ThenSlotIsCompleted)
{
RecordProperty(
"Description",
"Given a SetRequestedRunTarget event. "
"When the event is passed to handleComponentEvent(). "
"Then the slot is completed with an error. "
"And the wait() does not block.");

// Given
CompletionSlot<Result<void>> slot;
auto event = SetRequestedRunTarget{IdentifierHash{run_target_name(0)}, &slot};

// When
graph_->handleComponentEvent(event);

// Then
auto completion = slot.wait();
ASSERT_FALSE(completion.has_value());
EXPECT_EQ(completion.error(), ExecErrc::kFailed);
}

class GraphActivationSourceTest : public GraphTest
{
protected:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,26 +271,36 @@ void ProcessGroupManager::allProcessGroupsOff()
// SupervisionFailure is intentionally ignored here so recovery transitions do not
// fight the forced transition to Off.
auto waitForStateCompletion = [this](GraphState state_to_be_completed, int32_t max_wait_ms) -> bool {
constexpr int32_t kSleepIntervalMs = 10;
constexpr int32_t kMaxSleepIntervalMs = 10;

auto has_state = [this, state_to_be_completed]() {
return graph_->getState() == state_to_be_completed;
};

int32_t remaining_ms = max_wait_ms;
while (has_state() && (remaining_ms > 0))
// A flood of events makes event_queue_->waitForEvents() return immediately,
// as the queue will never be empty.
// We cannot rely on a kMaxSleepIntervalMs delay per iteration
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds{max_wait_ms};
while (has_state() && (std::chrono::steady_clock::now() < deadline))
{
static_cast<void>(event_queue_->waitForEvents(std::chrono::milliseconds(kSleepIntervalMs)));
static_cast<void>(event_queue_->waitForEvents(std::chrono::milliseconds(kMaxSleepIntervalMs)));
while (auto event = event_queue_->getNextEvent())
{
if (std::holds_alternative<SupervisionFailure>(*event))
{
continue;
}
graph_->handleComponentEvent(*event);
else if (
std::holds_alternative<GetActiveRunTarget>(*event) ||
std::holds_alternative<SetRequestedRunTarget>(*event))
{
graph_->unblockComponentEvent(*event, ExecErrc::kShuttingDown);
}
else
{
graph_->handleComponentEvent(*event);
}
}

remaining_ms -= kSleepIntervalMs;
}

return !has_state();
Expand Down
1 change: 1 addition & 0 deletions score/launch_manager/src/execution_error.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ enum class ExecErrc : score::result::ErrorCode
kRequestQueueIsFull = 13, ///< The activation request queue is full; the request was discarded
kRunTargetDoesntExist = 14, ///< The requested Run Target does not exist in the current configuration
kNotImplemented = 15, ///< The requested functionality is not yet implemented
kShuttingDown = 16, ///< The request could not be completed because launch manager is shutting down
};

class ExecErrorDomain final : public score::result::ErrorDomain
Expand Down
1 change: 1 addition & 0 deletions tests/integration/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ test_suite(
"//tests/integration/incorrect_config_non_reporting",
"//tests/integration/lm_shutdown_during_rt_switch",
"//tests/integration/lm_shutdown_during_switch_to_off",
"//tests/integration/lm_shutdown_with_control_requests",
"//tests/integration/parallel_launch",
"//tests/integration/process_complex_rep_failure",
"//tests/integration/process_crash_monitoring",
Expand Down
54 changes: 54 additions & 0 deletions tests/integration/lm_shutdown_with_control_requests/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# *******************************************************************************
# 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("@rules_cc//cc:cc_library.bzl", "cc_library")
load("//tests/utils/bazel:integration.bzl", "integration_test")

cc_library(
name = "common",
hdrs = ["common.hpp"],
)

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

cc_binary(
name = "lm_control_requester",
srcs = ["lm_control_requester.cpp"],
deps = [
":common",
"//score/launch_manager:control_cc",
],
)

integration_test(
name = "lm_shutdown_with_control_requests",
timeout = "short",
srcs = ["test.py"],
binaries = [
"//tests/utils/test_helper:process_hanging_on_sigterm",
":control_client_test_driver",
":lm_control_requester",
"//score/launch_manager",
],
config = ":config.json",
)
26 changes: 26 additions & 0 deletions tests/integration/lm_shutdown_with_control_requests/common.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/********************************************************************************
* 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
********************************************************************************/

#ifndef SCORE_TESTS_INTEGRATION_LM_SHUTDOWN_WITH_CONTROL_REQUESTS_COMMON_HPP
#define SCORE_TESTS_INTEGRATION_LM_SHUTDOWN_WITH_CONTROL_REQUESTS_COMMON_HPP

#include <string_view>

/// @brief Written by component_a when it has reported running (run_target_a is
/// active).
constexpr std::string_view a_started = "component_a_started";

/// @brief Written by component_a when it starts being terminated
constexpr std::string_view a_terminating = "component_a_terminating";

#endif // SCORE_TESTS_INTEGRATION_LM_SHUTDOWN_WITH_CONTROL_REQUESTS_COMMON_HPP
92 changes: 92 additions & 0 deletions tests/integration/lm_shutdown_with_control_requests/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
{
"schema_version": 1,
"defaults": {
"deployment_config": {
"bin_dir": "/tmp/tests/lm_shutdown_with_control_requests",
"ready_timeout_ms": 1000,
"shutdown_timeout_ms": 1000,
"ready_recovery_action": {
"restart": {
"number_of_attempts": 0
}
},
"recovery_action": {
"switch_run_target": {
"run_target": "fallback_run_target"
}
},
"environmental_variables": {
"LD_LIBRARY_PATH": "/opt/lib"
},
"sandbox": {
"uid": 0,
"gid": 0,
"scheduling_policy": "SCHED_OTHER",
"scheduling_priority": 0
}
},
"component_properties": {
"application_profile": {
"application_type": "Reporting",
"is_self_terminating": false
},
"ready_condition": {
"process_state": "Running"
}
}
},
"components": {
"component_initial": {
"component_properties": {
"binary_name": "control_client_test_driver",
"application_profile": {
"application_type": "State_Manager",
"alive_supervision": {
"min_indications": 0
}
}
},
"deployment_config": {
"ready_timeout_ms": 1000,
"shutdown_timeout_ms": 5000,
"environmental_variables": {
"PROCESSIDENTIFIER": "control_client_test_driver"
}
}
},
"component_a": {
"component_properties": {
"binary_name": "process_hanging_on_sigterm"
},
"deployment_config": {
"shutdown_timeout_ms": 5000,
"environmental_variables": {
"PROCESSIDENTIFIER": "component_a"
}
}
}
},
"run_targets": {
"Startup": {
"depends_on": [
"component_initial"
]
},
"run_target_a": {
"depends_on": [
"component_initial",
"component_a"
]
},
"Off": {
"depends_on": []
}
},
"initial_run_target": "Startup",
"alive_supervision": {
"evaluation_cycle_ms": 50
},
"fallback_run_target": {
"depends_on": []
}
}
Loading
Loading