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
11 changes: 11 additions & 0 deletions score/launch_manager/docs/user_guide/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,17 @@ component_properties (object)
* **Allowed Values:**
* ``"Running"``: The process has started and reached its running state.
* ``"Terminated"``: The process has started, reached its running state, and then terminated successfully.
* **file_state** (object, optional)

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.

Is it valid to configure both process_state and file_state at the same time?

* **Description:** Specifies a ready condition based on the existence state of a file at a given path.
* **Properties:**

@NicolasFussberger NicolasFussberger Aug 4, 2026

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 assume we do not need a dedicated timeout here and we will use the existing ready_timeout parameter.
We need to refactor the usage of ready_timeout so that it actually tracks the time until ready condition is fulfilled and not until Running state is reached

* **file_path** (string, required)
* **Description:** Specifies the absolute path to the file being watched.
* **state** (string, optional)
* **Description:** Specifies the required existence state of the file.
* **Allowed Values:**
* ``"Exists"``: The component is ready when the file at ``file_path`` exists.
* ``"Deleted"``: The component is ready when the file at ``file_path`` is deleted.

@NicolasFussberger NicolasFussberger Aug 3, 2026

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.

Are there any files which cannot be watched via inotify and thus we need to poll for their existence with some to-be-configured interval?

* **Default:** ``"Exists"``

.. _lm_conf_deployment_config_object_:

Expand Down
15 changes: 14 additions & 1 deletion score/launch_manager/src/daemon/src/configuration/config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ enum class ProcessState : uint8_t
Terminated = 1
};

enum class FileExistenceState : uint8_t
{
Exists = 0,
Deleted,
};

struct ComponentAliveSupervision
{
uint32_t reporting_cycle_ms{};
Expand All @@ -52,9 +58,16 @@ struct ApplicationProfile
std::optional<ComponentAliveSupervision> alive_supervision;
};

struct FileState
{
std::string file_path;
FileExistenceState state{FileExistenceState::Exists};
};

struct ReadyCondition
{
ProcessState process_state{ProcessState::Running};
std::optional<ProcessState> process_state{ProcessState::Running};
std::optional<FileState> file_state;

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 wonder if here or even in the user config we shall introduce some 'type' attribute that defines which type of ready condition is configured/used, or is it intended to support any combinations of these?

"readyCondition" : {
   "type": "PROCESS_STATE",
   ....
}

For now with just 2 types its probably still manageable as is.
If we introduce more ready conditions, it might get out of hand.

};

struct ComponentProperties
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,28 @@
"Terminated"
],
"description": "Specifies the required state of the component's POSIX process. 'Running': the process has started and reached its running state. 'Terminated': the process has started, reached its running state, and then terminated successfully."
},
"file_state": {
"type": "object",
"description": "Specifies a ready condition based on the existence state of a file at a given path.",
"properties": {
"file_path": {
"type": "string",

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.

Is it possible to specify a regex here to validate the basic format of the path?
For example that it is an absolute path that starts with "/".

"description": "Specifies the absolute path to the file being watched."
},
"state": {
"type": "string",
"enum": [
"Exists",
"Deleted"
],
"description": "Specifies the required existence state of the file. 'Exists': the file must be present at 'file_path'. 'Deleted': the file must be absent from 'file_path'. Defaults to 'Exists' if not specified."
}
},
"required": [
"file_path"
],
"additionalProperties": false
}
},
"required": [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Config makeMinimalConfig()
comp_a.component_properties.application_profile.application_type = ApplicationType::ReportingAndSupervised;
comp_a.component_properties.application_profile.is_self_terminating = false;
comp_a.component_properties.application_profile.alive_supervision = ComponentAliveSupervision{500, 2, 1, 3};
comp_a.component_properties.ready_condition = ReadyCondition{ProcessState::Running};
comp_a.component_properties.ready_condition = ReadyCondition{ProcessState::Running, std::nullopt};
comp_a.deployment_config.ready_timeout_ms = 500;
comp_a.deployment_config.shutdown_timeout_ms = 500;
comp_a.deployment_config.bin_dir = "/opt/apps";
Expand All @@ -54,7 +54,7 @@ Config makeMinimalConfig()
comp_b.component_properties.application_profile.application_type = ApplicationType::Native;
comp_b.component_properties.application_profile.is_self_terminating = true;
comp_b.component_properties.depends_on = {"comp_a"};
comp_b.component_properties.ready_condition = ReadyCondition{ProcessState::Running};
comp_b.component_properties.ready_condition = ReadyCondition{ProcessState::Running, std::nullopt};
comp_b.deployment_config.ready_timeout_ms = 1000;
comp_b.deployment_config.shutdown_timeout_ms = 1000;
comp_b.deployment_config.bin_dir = "/opt/apps";
Expand Down Expand Up @@ -347,7 +347,7 @@ TEST(ConfigurationAdapterReadyConditionTest, DependencyUsesTargetComponentReadyC
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.component_properties.ready_condition = ReadyCondition{ProcessState::Terminated, std::nullopt};
comp_a.deployment_config.bin_dir = "/opt";
comp_a.component_properties.binary_name = "comp_a";
comp_a.deployment_config.working_dir = "/tmp";
Expand All @@ -360,7 +360,7 @@ TEST(ConfigurationAdapterReadyConditionTest, DependencyUsesTargetComponentReadyC
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.ready_condition = ReadyCondition{ProcessState::Running, std::nullopt};
comp_b.component_properties.depends_on = {"comp_a"};
comp_b.deployment_config.bin_dir = "/opt";
comp_b.component_properties.binary_name = "comp_b";
Expand Down Expand Up @@ -434,7 +434,7 @@ TEST(ConfigurationAdapterReadyConditionTest, DependencyDefaultsToRunningWhenTarg
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::Terminated};
comp_b.component_properties.ready_condition = ReadyCondition{ProcessState::Terminated, std::nullopt};
comp_b.component_properties.depends_on = {"comp_a"};
comp_b.deployment_config.bin_dir = "/opt";
comp_b.component_properties.binary_name = "comp_b";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,45 @@ TEST_F(FlatbufferConfigLoaderTest, LoadSingleComponent)
EXPECT_THAT(comp.deployment_config.working_dir, Eq("/tmp"));
}

TEST_F(FlatbufferConfigLoaderTest, LoadSingleComponentWithFileState)
{
RecordProperty("Description", "Loads a component whose ready_condition includes a file_state.");

::flatbuffers::FlatBufferBuilder fbb;

auto app_profile =
fb::CreateApplicationProfile(fbb, fb::ApplicationType::Native, false /*is_self_terminating*/);
auto bin_name = fbb.CreateString("my_binary");
auto file_state = fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::Exists);
auto ready_cond = fb::CreateReadyCondition(fbb, fb::ProcessState::Running, file_state);
auto comp_props =
fb::CreateComponentProperties(fbb, bin_name, app_profile, 0 /*depends_on*/, 0 /*process_arguments*/, ready_cond);

auto bin_dir = fbb.CreateString("/opt/bin");
auto work_dir = fbb.CreateString("/tmp");
auto sandbox = buildDefaultSandbox(fbb);
auto deploy = fb::CreateDeploymentConfig(
fbb, 1.5 /*ready_timeout*/, 2.5 /*shutdown_timeout*/, 0 /*environmental_variables*/,
bin_dir, work_dir, 0 /*ready_recovery_action*/, 0 /*recovery_action*/, sandbox);

auto comp_name = fbb.CreateString("TestComponent");
auto comp_desc = fbb.CreateString("A test component");
auto component = fb::CreateComponent(fbb, comp_name, comp_desc, comp_props, deploy);
auto comps = fbb.CreateVector(std::vector<::flatbuffers::Offset<fb::Component>>{component});

auto result = loadBuffer(buildConfigWithComponents(fbb, comps));

ASSERT_THAT(result.has_value(), IsTrue());
ASSERT_THAT(result->components().size(), Eq(1U));

const auto& comp = result->components()[0];
ASSERT_THAT(comp.component_properties.ready_condition.has_value(), IsTrue());
EXPECT_THAT(comp.component_properties.ready_condition->process_state, Eq(ProcessState::Running));
ASSERT_THAT(comp.component_properties.ready_condition->file_state.has_value(), IsTrue());
EXPECT_THAT(comp.component_properties.ready_condition->file_state->file_path, Eq("/tmp/ready"));
EXPECT_THAT(comp.component_properties.ready_condition->file_state->state, Eq(FileExistenceState::Exists));
}

TEST_F(FlatbufferConfigLoaderTest, LoadRunTargets)
{
RecordProperty("Description", "Loads run targets with dependencies and transition timeout.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ namespace
{

template <typename T>
score::cpp::expected<T, IConfigLoader::Error> requireScalarValue(const ::flatbuffers::Optional<T>& field,
const std::string_view field_name)
score::cpp::expected<T, IConfigLoader::Error> requireScalarValue(
const ::flatbuffers::Optional<T>& field,
const std::string_view field_name)
{
if (!field.has_value())
{
Expand Down Expand Up @@ -107,6 +108,18 @@ ProcessState convertProcessState(fb::ProcessState fb_state)
}
}

FileExistenceState convertFileExistenceState(fb::FileExistenceState fb_state)
{
switch (fb_state)
{
case fb::FileExistenceState::Deleted:
return FileExistenceState::Deleted;
case fb::FileExistenceState::Exists:
default:
return FileExistenceState::Exists;
}
}

score::cpp::expected<int32_t, IConfigLoader::Error> convertSchedulingPolicy(fb::SchedulingPolicy policy)
{
switch (policy)
Expand Down Expand Up @@ -300,6 +313,17 @@ score::cpp::expected<ApplicationProfile, IConfigLoader::Error> convertApplicatio
return result;
}

std::optional<FileState> convertFileState(const fb::FileState* fb_fs)
{
if (fb_fs == nullptr)
{
return std::nullopt;
}
SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE(
fb_fs->file_path(), "FileState::file_path must never be nullptr as it is required in the schema");
return FileState{fb_fs->file_path()->str(), convertFileExistenceState(fb_fs->state())};
}

score::cpp::expected<ReadyCondition, IConfigLoader::Error> convertReadyCondition(const fb::ReadyCondition* fb_rc)
{
ReadyCondition result{};
Expand All @@ -311,7 +335,13 @@ score::cpp::expected<ReadyCondition, IConfigLoader::Error> convertReadyCondition
return score::cpp::make_unexpected(process_state.error());
}
result.process_state = convertProcessState(*process_state);
result.file_state = convertFileState(fb_rc->file_state());
}

SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE(
!(result.process_state == std::nullopt && result.file_state == std::nullopt),
"At least one ready condition is required, exiting as configuration is invalid");

return result;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ score::cpp::expected<TargetT, IConfigLoader::Error> validateRange(int64_t value,
[[nodiscard]] ApplicationType convertApplicationType(fb::ApplicationType fb_type);
/// @brief Converts a FlatBuffer ProcessState enum to the config ProcessState.
[[nodiscard]] ProcessState convertProcessState(fb::ProcessState fb_state);
/// @brief Converts a FlatBuffer FileState struct to the config equivalent.
std::optional<FileState> convertFileState(const fb::FileState* fb_fs);
/// @brief Converts a FlatBuffer FileExistenceState enum to the config equivalent.
[[nodiscard]] FileExistenceState convertFileExistenceState(fb::FileExistenceState fb_state);
/// @brief Converts a FlatBuffer SchedulingPolicy enum to a POSIX scheduling policy constant.
[[nodiscard]] score::cpp::expected<int32_t, IConfigLoader::Error> convertSchedulingPolicy(
fb::SchedulingPolicy policy);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,77 @@ TEST_F(ConverterTest, ConvertReadyConditionMissingProcessStateReturnsError)
EXPECT_THAT(result.error(), Eq(IConfigLoader::Error::InvalidFormat));
}

TEST_F(ConverterTest, ConvertFileExistenceStateMapsBothValues)
{
RecordProperty("Description", "convertFileExistenceState maps both enum values correctly.");
EXPECT_THAT(details::convertFileExistenceState(fb::FileExistenceState::Exists), Eq(FileExistenceState::Exists));
EXPECT_THAT(details::convertFileExistenceState(fb::FileExistenceState::Deleted), Eq(FileExistenceState::Deleted));
}

TEST_F(ConverterTest, ConvertFileStateNullReturnsNullopt)
{
RecordProperty("Description", "convertFileState returns nullopt when passed nullptr.");
auto result = details::convertFileState(nullptr);
EXPECT_THAT(result.has_value(), IsFalse());
}

TEST_F(ConverterTest, ConvertFileStateValid)
{
RecordProperty("Description", "convertFileState maps file_path and an explicit state correctly.");
::flatbuffers::FlatBufferBuilder fbb;
auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::Deleted);
fbb.Finish(fs);
const auto* ptr = ::flatbuffers::GetRoot<fb::FileState>(fbb.GetBufferPointer());

auto result = details::convertFileState(ptr);
ASSERT_THAT(result.has_value(), IsTrue());
EXPECT_THAT(result->file_path, Eq("/tmp/ready"));
EXPECT_THAT(result->state, Eq(FileExistenceState::Deleted));
}

TEST_F(ConverterTest, ConvertFileStateDefaultsToExists)
{
RecordProperty("Description", "convertFileState defaults state to Exists when not specified.");
::flatbuffers::FlatBufferBuilder fbb;
auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready");
fbb.Finish(fs);
const auto* ptr = ::flatbuffers::GetRoot<fb::FileState>(fbb.GetBufferPointer());

auto result = details::convertFileState(ptr);
ASSERT_THAT(result.has_value(), IsTrue());
EXPECT_THAT(result->state, Eq(FileExistenceState::Exists));
}

TEST_F(ConverterTest, ConvertReadyConditionWithFileState)
{
RecordProperty("Description", "convertReadyCondition maps a present file_state alongside process_state.");
::flatbuffers::FlatBufferBuilder fbb;
auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::Exists);
auto rc = fb::CreateReadyCondition(fbb, fb::ProcessState::Running, fs);
fbb.Finish(rc);
const auto* ptr = ::flatbuffers::GetRoot<fb::ReadyCondition>(fbb.GetBufferPointer());

auto result = details::convertReadyCondition(ptr);
ASSERT_THAT(result.has_value(), IsTrue());
EXPECT_THAT(result->process_state, Eq(ProcessState::Running));
ASSERT_THAT(result->file_state.has_value(), IsTrue());
EXPECT_THAT(result->file_state->file_path, Eq("/tmp/ready"));
EXPECT_THAT(result->file_state->state, Eq(FileExistenceState::Exists));
}

TEST_F(ConverterTest, ConvertReadyConditionWithoutFileStateLeavesNullopt)
{
RecordProperty("Description", "convertReadyCondition leaves file_state as nullopt when absent.");
::flatbuffers::FlatBufferBuilder fbb;
auto rc = fb::CreateReadyCondition(fbb, fb::ProcessState::Running);
fbb.Finish(rc);
const auto* ptr = ::flatbuffers::GetRoot<fb::ReadyCondition>(fbb.GetBufferPointer());

auto result = details::convertReadyCondition(ptr);
ASSERT_THAT(result.has_value(), IsTrue());
EXPECT_THAT(result->file_state.has_value(), IsFalse());
}

TEST_F(ConverterTest, ConvertSandboxValid)
{
RecordProperty("Description", "convertSandbox maps all fields including optional ones.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ enum ProcessState : byte {
Terminated = 1
}

// Specifies the required existence state of a watched file.
enum FileExistenceState : byte {
Exists = 0,
Deleted = 1
}

// Scheduling policy for a component's initial thread.
enum SchedulingPolicy : byte {
OTHER = 0,
Expand Down Expand Up @@ -53,9 +59,18 @@ table ApplicationProfile {
alive_supervision:ComponentAliveSupervision; // optional
}

// Defines a ready condition based on the existence state of a file at a given path.
table FileState {
// Absolute path to the file being watched.
file_path:string (required); // required
// Existence state of the file. Defaults to Exists if not specified.
state:FileExistenceState = Exists; // optional, defaults to Exists

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.

Currently, all the default values are in scripts/config_mapping/lifecycle_config.py

I think this mapping is not existing in the PR

}

// Defines the conditions that determine when the component enters the ready state.
table ReadyCondition {
process_state:ProcessState = null; // required
file_state:FileState; // optional
}

// Defines essential characteristics of a software component.
Expand Down
Loading