From e5d6f5741b17b9fd0fc4afc51286f5befe3f0d5e Mon Sep 17 00:00:00 2001 From: Maciej Kaszynski Date: Mon, 3 Aug 2026 08:40:02 +0100 Subject: [PATCH 1/5] Adding new config field --- .../docs/user_guide/configuration.rst | 15 +++ .../src/configuration/component_config.hpp | 25 ++++- .../src/daemon/src/configuration/config.hpp | 2 + .../config_schema/launch_manager.schema.json | 30 +++++- .../details/flatbuffer_config_loader_UT.cpp | 49 +++++++++- .../details/flatbuffer_type_converters.cpp | 73 +++++++++++--- .../details/flatbuffer_type_converters.hpp | 7 +- .../details/flatbuffer_type_converters_UT.cpp | 95 ++++++++++++++++++- .../src/configuration/details/lm_flatcfg.fbs | 22 ++++- 9 files changed, 289 insertions(+), 29 deletions(-) diff --git a/score/launch_manager/docs/user_guide/configuration.rst b/score/launch_manager/docs/user_guide/configuration.rst index a51caa11da..1678691a93 100644 --- a/score/launch_manager/docs/user_guide/configuration.rst +++ b/score/launch_manager/docs/user_guide/configuration.rst @@ -208,6 +208,21 @@ 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) + * **Description:** Specifies a ready condition based on the existence state of a file at a given path. + * **Properties:** + * **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. + * **Default:** ``"Exists"`` + * **polling_interval** (integer, optional) + * **Description:** Specifies the time interval, in milliseconds, at which the **Launch Manager** checks the file existence state. + * **Constraint:** Must be greater than 0. + * **Default:** ``10`` .. _lm_conf_deployment_config_object_: diff --git a/score/launch_manager/src/daemon/src/configuration/component_config.hpp b/score/launch_manager/src/daemon/src/configuration/component_config.hpp index f3cc4e538b..2f5a60c630 100644 --- a/score/launch_manager/src/daemon/src/configuration/component_config.hpp +++ b/score/launch_manager/src/daemon/src/configuration/component_config.hpp @@ -18,6 +18,8 @@ #include #include #include +#include +#include #include "score/mw/launch_manager/configuration/environment_config.hpp" #include "score/mw/launch_manager/configuration/recovery_action_config.hpp" @@ -48,17 +50,30 @@ struct ApplicationProfile std::optional alive_supervision; }; -enum class ProcessState : uint8_t +enum class FileExistenceState : uint8_t { - Running = 0, - Terminated = 1 + Exists = 0, + Deleted, }; -struct ReadyCondition + + +struct FileState { - ProcessState process_state{ProcessState::Running}; + std::string file_path; + FileExistenceState state{FileExistenceState::Exists}; + std::chrono::milliseconds polling_interval{10}; }; + +enum class ProcessState : std::uint8_t +{ + Running = 0, + Terminated = 1 +}; + +using ReadyCondition = std::variant; + struct ComponentProperties { std::string binary_name; diff --git a/score/launch_manager/src/daemon/src/configuration/config.hpp b/score/launch_manager/src/daemon/src/configuration/config.hpp index 0925536253..3444b9b784 100644 --- a/score/launch_manager/src/daemon/src/configuration/config.hpp +++ b/score/launch_manager/src/daemon/src/configuration/config.hpp @@ -14,10 +14,12 @@ #define CONFIG_HPP #include +#include #include #include #include #include +#include #include #include "score/mw/launch_manager/configuration/component_config.hpp" diff --git a/score/launch_manager/src/daemon/src/configuration/config_schema/launch_manager.schema.json b/score/launch_manager/src/daemon/src/configuration/config_schema/launch_manager.schema.json index 371630d73b..990a9411dc 100644 --- a/score/launch_manager/src/daemon/src/configuration/config_schema/launch_manager.schema.json +++ b/score/launch_manager/src/daemon/src/configuration/config_schema/launch_manager.schema.json @@ -89,6 +89,34 @@ "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", + "pattern": "^/.*", + "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." + }, + "polling_interval": { + "type": "integer", + "exclusiveMinimum": 0, + "description": "Specifies the time interval, in milliseconds, at which the Launch Manager checks the file existence state." + } + }, + "required": [ + "file_path" + ], + "additionalProperties": false } }, "required": [], @@ -488,4 +516,4 @@ "initial_run_target" ], "additionalProperties": false -} \ No newline at end of file +} diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_config_loader_UT.cpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_config_loader_UT.cpp index 8dd30bb0e6..d2dbc5de14 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_config_loader_UT.cpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_config_loader_UT.cpp @@ -31,10 +31,12 @@ namespace namespace fb = score::mw::lifecycle::internal::configuration::fb; using ::testing::Eq; +using ::testing::FieldsAre; using ::testing::IsFalse; using ::testing::IsNull; using ::testing::IsTrue; using ::testing::StrEq; +using ::testing::VariantWith; const score::filesystem::Path kTestPath{"/tmp/test_config.bin"}; @@ -261,13 +263,58 @@ TEST_F(FlatbufferConfigLoaderTest, LoadSingleComponent) ASSERT_THAT(comp.component_properties.process_arguments.size(), Eq(1U)); EXPECT_THAT(comp.component_properties.process_arguments[0], Eq("--verbose")); ASSERT_THAT(comp.component_properties.ready_condition.has_value(), IsTrue()); - EXPECT_THAT(comp.component_properties.ready_condition->process_state, Eq(ProcessState::Running)); + EXPECT_THAT(*comp.component_properties.ready_condition, VariantWith(Eq(ProcessState::Running))); EXPECT_THAT(comp.deployment_config.ready_timeout_ms, Eq(1500U)); EXPECT_THAT(comp.deployment_config.shutdown_timeout_ms, Eq(2500U)); EXPECT_THAT(comp.deployment_config.bin_dir, Eq("/opt/bin")); 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, std::nullopt, 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>{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, + VariantWith( + FieldsAre(Eq("/tmp/ready"), Eq(FileExistenceState::Exists), Eq(std::chrono::milliseconds{10})))); +} + TEST_F(FlatbufferConfigLoaderTest, LoadRunTargets) { RecordProperty("Description", "Loads run targets with dependencies and transition timeout."); diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp index 3b5df14b54..9fd5bd7c0a 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp @@ -103,6 +103,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: + return FileExistenceState::Exists; + } + SCORE_LANGUAGE_FUTURECPP_UNREACHABLE(); +} + score::cpp::expected convertSchedulingPolicy(fb::SchedulingPolicy policy) { switch (policy) @@ -296,19 +308,57 @@ score::cpp::expected convertApplicatio return result; } -score::cpp::expected convertReadyCondition(const fb::ReadyCondition* fb_rc) +std::optional 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()), + std::chrono::milliseconds{fb_fs->polling_interval()}}; +} + +std::optional convertReadyCondition(const fb::ReadyCondition* fb_rc) { - ReadyCondition result{}; - if (fb_rc != nullptr) + if (fb_rc == nullptr) + { + return std::nullopt; + } + + const bool has_process_state = fb_rc->process_state().has_value(); + const bool has_file_state = fb_rc->file_state() != nullptr; + + if (has_process_state && has_file_state) + { + LM_LOG_ERROR() << "ReadyCondition cannot have both process_state and file_state set"; + return std::nullopt; + } + + if (!has_process_state && !has_file_state) + { + LM_LOG_ERROR() << "ReadyCondition must have either process_state or file_state set"; + return std::nullopt; + } + + if (has_process_state) + { + return convertProcessState(*fb_rc->process_state()); + } + else { - auto process_state = requireScalarValue(fb_rc->process_state(), "ReadyCondition::process_state"); - if (!process_state.has_value()) + auto file_state = convertFileState(fb_rc->file_state()); + if (!file_state.has_value()) { - return score::cpp::make_unexpected(process_state.error()); + LM_LOG_ERROR() << "FileState conversion failed"; + return std::nullopt; } - result.process_state = convertProcessState(*process_state); + return *file_state; } - return result; } score::cpp::expected convertComponentProperties( @@ -334,12 +384,7 @@ score::cpp::expected convertComponent result.process_arguments = convertStringVector(fb_cp->process_arguments()); if (fb_cp->ready_condition() != nullptr) { - auto ready_cond = convertReadyCondition(fb_cp->ready_condition()); - if (!ready_cond.has_value()) - { - return score::cpp::make_unexpected(ready_cond.error()); - } - result.ready_condition = std::move(*ready_cond); + result.ready_condition = convertReadyCondition(fb_cp->ready_condition()); } } return result; diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.hpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.hpp index 4952b6ae64..2525200062 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.hpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.hpp @@ -66,6 +66,10 @@ score::cpp::expected 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 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 convertSchedulingPolicy(fb::SchedulingPolicy policy); @@ -102,8 +106,7 @@ score::cpp::expected validateRange(int64_t value, [[nodiscard]] score::cpp::expected convertApplicationProfile( const fb::ApplicationProfile* fb_ap); /// @brief Converts a FlatBuffer ReadyCondition to the config equivalent. -[[nodiscard]] score::cpp::expected convertReadyCondition( - const fb::ReadyCondition* fb_rc); +[[nodiscard]] std::optional convertReadyCondition(const fb::ReadyCondition* fb_rc); /// @brief Converts a FlatBuffer ComponentProperties to the config equivalent. [[nodiscard]] score::cpp::expected convertComponentProperties( const fb::ComponentProperties* fb_cp); diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp index ffcb589ed5..1412264a93 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp @@ -560,7 +560,14 @@ TEST_F(ConverterTest, ConvertApplicationProfileMissingSelfTerminatingReturnsErro EXPECT_THAT(result.error(), Eq(IConfigLoader::Error::InvalidFormat)); } -TEST_F(ConverterTest, ConvertReadyConditionValid) +TEST_F(ConverterTest, ConvertReadyConditionNullReturnsNullopt) +{ + RecordProperty("Description", "convertReadyCondition with nullptr returns nullopt."); + auto result = details::convertReadyCondition(nullptr); + EXPECT_THAT(result.has_value(), IsFalse()); +} + +TEST_F(ConverterTest, ConvertReadyConditionWithProcessState) { RecordProperty("Description", "convertReadyCondition maps process_state correctly."); ::flatbuffers::FlatBufferBuilder fbb; @@ -570,12 +577,40 @@ TEST_F(ConverterTest, ConvertReadyConditionValid) auto result = convertReadyCondition(ptr); ASSERT_THAT(result.has_value(), IsTrue()); - EXPECT_THAT(result->process_state, Eq(ProcessState::Terminated)); + EXPECT_THAT(*result, ::testing::VariantWith(ProcessState::Terminated)); +} + +TEST_F(ConverterTest, ConvertReadyConditionWithFileState) +{ + RecordProperty("Description", "convertReadyCondition maps file_state correctly."); + ::flatbuffers::FlatBufferBuilder fbb; + auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::Exists); + auto rc = fb::CreateReadyCondition(fbb, ::flatbuffers::nullopt /*process_state*/, fs); + fbb.Finish(rc); + const auto* ptr = ::flatbuffers::GetRoot(fbb.GetBufferPointer()); + + auto result = details::convertReadyCondition(ptr); + ASSERT_THAT(result.has_value(), IsTrue()); + EXPECT_THAT(*result, ::testing::VariantWith(::testing::Field(&FileState::file_path, Eq("/tmp/ready")))); } -TEST_F(ConverterTest, ConvertReadyConditionMissingProcessStateReturnsError) +TEST_F(ConverterTest, ConvertReadyConditionWithBothStatesReturnsNullopt) { - RecordProperty("Description", "Missing process_state returns InvalidFormat."); + RecordProperty("Description", "convertReadyCondition with both process_state and file_state returns nullopt."); + ::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(fbb.GetBufferPointer()); + + auto result = details::convertReadyCondition(ptr); + ASSERT_THAT(result.has_value(), IsFalse()); + EXPECT_EQ(result, std::nullopt); +} + +TEST_F(ConverterTest, ConvertReadyConditionWithNeitherStateReturnsNullopt) +{ + RecordProperty("Description", "convertReadyCondition with neither process_state nor file_state returns nullopt."); ::flatbuffers::FlatBufferBuilder fbb; auto rc = fb::CreateReadyCondition(fbb); fbb.Finish(rc); @@ -583,7 +618,57 @@ TEST_F(ConverterTest, ConvertReadyConditionMissingProcessStateReturnsError) auto result = convertReadyCondition(ptr); ASSERT_THAT(result.has_value(), IsFalse()); - EXPECT_THAT(result.error(), Eq(IConfigLoader::Error::InvalidFormat)); + EXPECT_EQ(result, std::nullopt); +} + +TEST_F(ConverterTest, ConvertFileExistenceStateMapsDeath) +{ + RecordProperty("Description", "convertFileExistenceState Fires an assertion if an undefined enum is given."); + EXPECT_DEATH( + static_cast(details::convertFileExistenceState( + static_cast(static_cast(fb::FileExistenceState::MAX) + 1))), + ".*"); +} + +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(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(fbb.GetBufferPointer()); + + auto result = details::convertFileState(ptr); + ASSERT_THAT(result.has_value(), IsTrue()); + EXPECT_THAT(result->state, Eq(FileExistenceState::Exists)); } TEST_F(ConverterTest, ConvertSandboxValid) diff --git a/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs b/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs index 83529b05ce..76cdf9229f 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs +++ b/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs @@ -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, @@ -53,9 +59,23 @@ 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 + // Time in ms to wait between each poll if the file is present. + polling_interval: uint32 = 10; //optional, defaults to 10ms +} + // Defines the conditions that determine when the component enters the ready state. +// Either process_state or file_state should be set, but not both. table ReadyCondition { - process_state:ProcessState = null; // required + // Required state of the component's POSIX process. + process_state:ProcessState = null; // optional + // File existence state condition. + file_state:FileState; // optional } // Defines essential characteristics of a software component. From 1c42437c6ba56e99f3350f166cd838ae68b1b72d Mon Sep 17 00:00:00 2001 From: Maciej Kaszynski Date: Thu, 13 Aug 2026 14:06:37 +0100 Subject: [PATCH 2/5] Using seconds --- .../docs/user_guide/configuration.rst | 10 ++++---- .../src/configuration/component_config.hpp | 9 +++---- .../config_schema/launch_manager.schema.json | 25 +++++++++++++------ .../details/flatbuffer_type_converters.cpp | 13 +++++----- .../details/flatbuffer_type_converters_UT.cpp | 7 +++--- .../src/configuration/details/lm_flatcfg.fbs | 6 ++--- 6 files changed, 40 insertions(+), 30 deletions(-) diff --git a/score/launch_manager/docs/user_guide/configuration.rst b/score/launch_manager/docs/user_guide/configuration.rst index 1678691a93..1aadffcf6b 100644 --- a/score/launch_manager/docs/user_guide/configuration.rst +++ b/score/launch_manager/docs/user_guide/configuration.rst @@ -209,7 +209,7 @@ component_properties (object) * ``"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) - * **Description:** Specifies a ready condition based on the existence state of a file at a given path. + * **Description:** Specifies a ready condition based on the existence of a file at a given path. * **Properties:** * **file_path** (string, required) * **Description:** Specifies the absolute path to the file being watched. @@ -217,12 +217,12 @@ component_properties (object) * **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. + * ``"NotExisting"``: The component is ready when the file at ``file_path`` does not exist. * **Default:** ``"Exists"`` - * **polling_interval** (integer, optional) - * **Description:** Specifies the time interval, in milliseconds, at which the **Launch Manager** checks the file existence state. + * **polling_interval** (number, optional) + * **Description:** Specifies the time interval, in seconds (e.g., ``0.3`` for 300 milliseconds), at which the **Launch Manager** checks the file existence state. * **Constraint:** Must be greater than 0. - * **Default:** ``10`` + * **Default:** ``0.01`` .. _lm_conf_deployment_config_object_: diff --git a/score/launch_manager/src/daemon/src/configuration/component_config.hpp b/score/launch_manager/src/daemon/src/configuration/component_config.hpp index 2f5a60c630..8c1c52ce3a 100644 --- a/score/launch_manager/src/daemon/src/configuration/component_config.hpp +++ b/score/launch_manager/src/daemon/src/configuration/component_config.hpp @@ -14,12 +14,12 @@ #define COMPONENT_CONFIG_HPP #include +#include #include #include #include -#include #include -#include +#include #include "score/mw/launch_manager/configuration/environment_config.hpp" #include "score/mw/launch_manager/configuration/recovery_action_config.hpp" @@ -53,11 +53,9 @@ struct ApplicationProfile enum class FileExistenceState : uint8_t { Exists = 0, - Deleted, + NotExisting, }; - - struct FileState { std::string file_path; @@ -65,7 +63,6 @@ struct FileState std::chrono::milliseconds polling_interval{10}; }; - enum class ProcessState : std::uint8_t { Running = 0, diff --git a/score/launch_manager/src/daemon/src/configuration/config_schema/launch_manager.schema.json b/score/launch_manager/src/daemon/src/configuration/config_schema/launch_manager.schema.json index 990a9411dc..98b598d8fe 100644 --- a/score/launch_manager/src/daemon/src/configuration/config_schema/launch_manager.schema.json +++ b/score/launch_manager/src/daemon/src/configuration/config_schema/launch_manager.schema.json @@ -92,25 +92,25 @@ }, "file_state": { "type": "object", - "description": "Specifies a ready condition based on the existence state of a file at a given path.", + "description": "Specifies a ready condition based on the existence of a file at a given path.", "properties": { "file_path": { "type": "string", - "pattern": "^/.*", + "pattern": "^/(?:[^/]+(?:/[^/]+)*)$", "description": "Specifies the absolute path to the file being watched." }, "state": { "type": "string", "enum": [ "Exists", - "Deleted" + "NotExisting" ], - "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." + "description": "Specifies the required existence of the file. 'Exists': the file must be present at 'file_path'. 'NotExisting': the file must be absent from 'file_path'. Defaults to 'Exists' if not specified." }, "polling_interval": { - "type": "integer", + "type": "number", "exclusiveMinimum": 0, - "description": "Specifies the time interval, in milliseconds, at which the Launch Manager checks the file existence state." + "description": "Specifies the time interval, in seconds (e.g., '0.3' for 300 milliseconds), at which the Launch Manager checks the file existence. Defaults to 10 milliseconds." } }, "required": [ @@ -119,7 +119,18 @@ "additionalProperties": false } }, - "required": [], + "oneOf": [ + { + "required": [ + "process_state" + ] + }, + { + "required": [ + "file_state" + ] + } + ], "additionalProperties": false } }, diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp index 9fd5bd7c0a..97ed4b74c4 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp @@ -107,8 +107,8 @@ FileExistenceState convertFileExistenceState(fb::FileExistenceState fb_state) { switch (fb_state) { - case fb::FileExistenceState::Deleted: - return FileExistenceState::Deleted; + case fb::FileExistenceState::NotExisting: + return FileExistenceState::NotExisting; case fb::FileExistenceState::Exists: return FileExistenceState::Exists; } @@ -317,10 +317,11 @@ std::optional convertFileState(const fb::FileState* fb_fs) 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()), - std::chrono::milliseconds{fb_fs->polling_interval()}}; + const auto polling_interval_seconds = fb_fs->polling_interval(); + const auto polling_interval_ms = + std::chrono::duration_cast(std::chrono::duration(polling_interval_seconds)); + + return FileState{fb_fs->file_path()->str(), convertFileExistenceState(fb_fs->state()), polling_interval_ms}; } std::optional convertReadyCondition(const fb::ReadyCondition* fb_rc) diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp index 1412264a93..2253cca1e0 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp @@ -634,7 +634,8 @@ 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)); + EXPECT_THAT( + details::convertFileExistenceState(fb::FileExistenceState::NotExisting), Eq(FileExistenceState::NotExisting)); } TEST_F(ConverterTest, ConvertFileStateNullReturnsNullopt) @@ -648,14 +649,14 @@ 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); + auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::NotExisting); fbb.Finish(fs); const auto* ptr = ::flatbuffers::GetRoot(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)); + EXPECT_THAT(result->state, Eq(FileExistenceState::NotExisting)); } TEST_F(ConverterTest, ConvertFileStateDefaultsToExists) diff --git a/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs b/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs index 76cdf9229f..c5c6366383 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs +++ b/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs @@ -29,7 +29,7 @@ enum ProcessState : byte { // Specifies the required existence state of a watched file. enum FileExistenceState : byte { Exists = 0, - Deleted = 1 + NotExisting = 1 } // Scheduling policy for a component's initial thread. @@ -65,8 +65,8 @@ table FileState { file_path:string (required); // required // Existence state of the file. Defaults to Exists if not specified. state:FileExistenceState = Exists; // optional, defaults to Exists - // Time in ms to wait between each poll if the file is present. - polling_interval: uint32 = 10; //optional, defaults to 10ms + // Time in seconds to wait between each poll if the file is present. + polling_interval: double = 0.01; //optional, defaults to 0.01s (10ms) } // Defines the conditions that determine when the component enters the ready state. From d0719c43674bb6bf7492c81bc5b79171aa405af1 Mon Sep 17 00:00:00 2001 From: Maciej Kaszynski Date: Tue, 18 Aug 2026 09:20:14 +0100 Subject: [PATCH 3/5] Making ReadyConditions non optional --- .../src/configuration/component_config.hpp | 2 +- .../details/flatbuffer_config_loader_UT.cpp | 13 +-- .../details/flatbuffer_type_converters.cpp | 65 +++++++------ .../details/flatbuffer_type_converters.hpp | 9 +- .../details/flatbuffer_type_converters_UT.cpp | 91 +++++++++++------ .../details/process_info_node.cpp | 32 +++--- scripts/config_mapping/lifecycle_config.py | 47 ++++++++- scripts/config_mapping/unit_tests.py | 97 ++++++++++++++++++- 8 files changed, 272 insertions(+), 84 deletions(-) diff --git a/score/launch_manager/src/daemon/src/configuration/component_config.hpp b/score/launch_manager/src/daemon/src/configuration/component_config.hpp index 8c1c52ce3a..d087688520 100644 --- a/score/launch_manager/src/daemon/src/configuration/component_config.hpp +++ b/score/launch_manager/src/daemon/src/configuration/component_config.hpp @@ -77,7 +77,7 @@ struct ComponentProperties ApplicationProfile application_profile; std::vector depends_on; std::vector process_arguments; - std::optional ready_condition; + ReadyCondition ready_condition; }; struct Sandbox { diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_config_loader_UT.cpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_config_loader_UT.cpp index d2dbc5de14..a630747d7d 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_config_loader_UT.cpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_config_loader_UT.cpp @@ -262,8 +262,7 @@ TEST_F(FlatbufferConfigLoaderTest, LoadSingleComponent) EXPECT_THAT(comp.component_properties.depends_on[0], Eq("other_comp")); ASSERT_THAT(comp.component_properties.process_arguments.size(), Eq(1U)); EXPECT_THAT(comp.component_properties.process_arguments[0], Eq("--verbose")); - ASSERT_THAT(comp.component_properties.ready_condition.has_value(), IsTrue()); - EXPECT_THAT(*comp.component_properties.ready_condition, VariantWith(Eq(ProcessState::Running))); + EXPECT_THAT(comp.component_properties.ready_condition, VariantWith(Eq(ProcessState::Running))); EXPECT_THAT(comp.deployment_config.ready_timeout_ms, Eq(1500U)); EXPECT_THAT(comp.deployment_config.shutdown_timeout_ms, Eq(2500U)); EXPECT_THAT(comp.deployment_config.bin_dir, Eq("/opt/bin")); @@ -308,9 +307,8 @@ TEST_F(FlatbufferConfigLoaderTest, LoadSingleComponentWithFileState) 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, + comp.component_properties.ready_condition, VariantWith( FieldsAre(Eq("/tmp/ready"), Eq(FileExistenceState::Exists), Eq(std::chrono::milliseconds{10})))); } @@ -667,7 +665,8 @@ TEST_F(FlatbufferConfigLoaderTest, OptionalWatchdogAbsent) TEST_F(FlatbufferConfigLoaderTest, OptionalReadyConditionAbsent) { - RecordProperty("Description", "When no ready_condition is present on a component, it is nullopt."); + RecordProperty( + "Description", "When no ready_condition is present on a component, it defaults to ProcessState::Running."); ::flatbuffers::FlatBufferBuilder fbb; @@ -681,7 +680,9 @@ TEST_F(FlatbufferConfigLoaderTest, OptionalReadyConditionAbsent) auto result = loadBuffer(buildConfigWithComponents(fbb, comps)); ASSERT_THAT(result.has_value(), IsTrue()); - EXPECT_THAT(result->components()[0].component_properties.ready_condition.has_value(), IsFalse()); + EXPECT_THAT( + result->components()[0].component_properties.ready_condition, + VariantWith(Eq(ProcessState::Running))); } // ============================================================================ diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp index 97ed4b74c4..4f685cf6f4 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp @@ -308,28 +308,27 @@ score::cpp::expected convertApplicatio return result; } -std::optional convertFileState(const fb::FileState* fb_fs) +score::cpp::expected 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"); + fb_fs.file_path(), "FileState::file_path must never be nullptr as it is required in the schema"); - const auto polling_interval_seconds = fb_fs->polling_interval(); - const auto polling_interval_ms = - std::chrono::duration_cast(std::chrono::duration(polling_interval_seconds)); - - return FileState{fb_fs->file_path()->str(), convertFileExistenceState(fb_fs->state()), polling_interval_ms}; + auto polling_interval_ms = secondsToMs(fb_fs.polling_interval()); + if (!polling_interval_ms.has_value()) + { + LM_LOG_ERROR() << "Invalid value for FileState::polling_interval"; + return score::cpp::make_unexpected(polling_interval_ms.error()); + } + return FileState{ + fb_fs.file_path()->str(), + convertFileExistenceState(fb_fs.state()), + std::chrono::milliseconds{*polling_interval_ms}}; } -std::optional convertReadyCondition(const fb::ReadyCondition* fb_rc) +score::cpp::expected convertReadyCondition(const fb::ReadyCondition* fb_rc) { - if (fb_rc == nullptr) - { - return std::nullopt; - } + SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE( + fb_rc != nullptr, "No ReadyCondition is confitured, this should have been defaulted with the script."); const bool has_process_state = fb_rc->process_state().has_value(); const bool has_file_state = fb_rc->file_state() != nullptr; @@ -337,29 +336,28 @@ std::optional convertReadyCondition(const fb::ReadyCondition* fb if (has_process_state && has_file_state) { LM_LOG_ERROR() << "ReadyCondition cannot have both process_state and file_state set"; - return std::nullopt; - } - - if (!has_process_state && !has_file_state) - { - LM_LOG_ERROR() << "ReadyCondition must have either process_state or file_state set"; - return std::nullopt; + return score::cpp::make_unexpected(IConfigLoader::Error::InvalidFormat); } if (has_process_state) { - return convertProcessState(*fb_rc->process_state()); + return ReadyCondition{convertProcessState(*fb_rc->process_state())}; } - else + + if (has_file_state) { - auto file_state = convertFileState(fb_rc->file_state()); + auto file_state = convertFileState(*(fb_rc->file_state())); if (!file_state.has_value()) { - LM_LOG_ERROR() << "FileState conversion failed"; - return std::nullopt; + LM_LOG_ERROR() << "Invalid value for ReadyCondition::file_state"; + return score::cpp::make_unexpected(file_state.error()); } - return *file_state; - } + + // convertFileState only returns nullopt for a nullptr input, which is ruled out above + return ReadyCondition{*file_state}; + }; + + SCORE_LANGUAGE_FUTURECPP_UNREACHABLE(); } score::cpp::expected convertComponentProperties( @@ -385,7 +383,12 @@ score::cpp::expected convertComponent result.process_arguments = convertStringVector(fb_cp->process_arguments()); if (fb_cp->ready_condition() != nullptr) { - result.ready_condition = convertReadyCondition(fb_cp->ready_condition()); + auto ready_condition = convertReadyCondition(fb_cp->ready_condition()); + if (!ready_condition.has_value()) + { + return score::cpp::make_unexpected(ready_condition.error()); + } + result.ready_condition = std::move(ready_condition.value()); } } return result; diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.hpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.hpp index 2525200062..259344d163 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.hpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.hpp @@ -66,8 +66,8 @@ score::cpp::expected 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 convertFileState(const fb::FileState* fb_fs); +/// @brief Converts a FlatBuffer FileState table to the config equivalent, or nullopt if absent. +[[nodiscard]] score::cpp::expected 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. @@ -105,8 +105,9 @@ std::optional convertFileState(const fb::FileState* fb_fs); /// @brief Converts a FlatBuffer ApplicationProfile to the config equivalent. [[nodiscard]] score::cpp::expected convertApplicationProfile( const fb::ApplicationProfile* fb_ap); -/// @brief Converts a FlatBuffer ReadyCondition to the config equivalent. -[[nodiscard]] std::optional convertReadyCondition(const fb::ReadyCondition* fb_rc); +/// @brief Converts a FlatBuffer ReadyCondition to the config equivalent, or nullopt if not configured. +[[nodiscard]] score::cpp::expected convertReadyCondition( + const fb::ReadyCondition* fb_rc); /// @brief Converts a FlatBuffer ComponentProperties to the config equivalent. [[nodiscard]] score::cpp::expected convertComponentProperties( const fb::ComponentProperties* fb_cp); diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp index 2253cca1e0..a44a445de7 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp @@ -560,11 +560,10 @@ TEST_F(ConverterTest, ConvertApplicationProfileMissingSelfTerminatingReturnsErro EXPECT_THAT(result.error(), Eq(IConfigLoader::Error::InvalidFormat)); } -TEST_F(ConverterTest, ConvertReadyConditionNullReturnsNullopt) +TEST_F(ConverterTest, ConvertReadyConditionNullDeath) { - RecordProperty("Description", "convertReadyCondition with nullptr returns nullopt."); - auto result = details::convertReadyCondition(nullptr); - EXPECT_THAT(result.has_value(), IsFalse()); + RecordProperty("Description", "convertReadyCondition fires an assertion when passed nullptr."); + EXPECT_DEATH(static_cast(convertReadyCondition(nullptr)), ".*"); } TEST_F(ConverterTest, ConvertReadyConditionWithProcessState) @@ -589,43 +588,59 @@ TEST_F(ConverterTest, ConvertReadyConditionWithFileState) fbb.Finish(rc); const auto* ptr = ::flatbuffers::GetRoot(fbb.GetBufferPointer()); - auto result = details::convertReadyCondition(ptr); + auto result = convertReadyCondition(ptr); ASSERT_THAT(result.has_value(), IsTrue()); EXPECT_THAT(*result, ::testing::VariantWith(::testing::Field(&FileState::file_path, Eq("/tmp/ready")))); } -TEST_F(ConverterTest, ConvertReadyConditionWithBothStatesReturnsNullopt) +TEST_F(ConverterTest, ConvertReadyConditionWithBothStatesReturnsError) { - RecordProperty("Description", "convertReadyCondition with both process_state and file_state returns nullopt."); + RecordProperty( + "Description", "convertReadyCondition with both process_state and file_state returns InvalidFormat."); ::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(fbb.GetBufferPointer()); - auto result = details::convertReadyCondition(ptr); + auto result = convertReadyCondition(ptr); ASSERT_THAT(result.has_value(), IsFalse()); - EXPECT_EQ(result, std::nullopt); + EXPECT_THAT(result.error(), Eq(IConfigLoader::Error::InvalidFormat)); } -TEST_F(ConverterTest, ConvertReadyConditionWithNeitherStateReturnsNullopt) +TEST_F(ConverterTest, ConvertReadyConditionWithNeitherStateDeath) { - RecordProperty("Description", "convertReadyCondition with neither process_state nor file_state returns nullopt."); + RecordProperty( + "Description", + "convertReadyCondition fires an assertion if neither process_state nor file_state is configured, as the " + "configuration script always defaults one of them."); ::flatbuffers::FlatBufferBuilder fbb; auto rc = fb::CreateReadyCondition(fbb); fbb.Finish(rc); const auto* ptr = ::flatbuffers::GetRoot(fbb.GetBufferPointer()); + EXPECT_DEATH(static_cast(convertReadyCondition(ptr)), ".*"); +} + +TEST_F(ConverterTest, ConvertReadyConditionWithInvalidPollingIntervalReturnsError) +{ + RecordProperty("Description", "convertReadyCondition propagates an invalid FileState::polling_interval."); + ::flatbuffers::FlatBufferBuilder fbb; + auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::Exists, -1.0 /*polling_interval*/); + auto rc = fb::CreateReadyCondition(fbb, ::flatbuffers::nullopt /*process_state*/, fs); + fbb.Finish(rc); + const auto* ptr = ::flatbuffers::GetRoot(fbb.GetBufferPointer()); + auto result = convertReadyCondition(ptr); ASSERT_THAT(result.has_value(), IsFalse()); - EXPECT_EQ(result, std::nullopt); + EXPECT_THAT(result.error(), Eq(IConfigLoader::Error::InvalidFormat)); } TEST_F(ConverterTest, ConvertFileExistenceStateMapsDeath) { RecordProperty("Description", "convertFileExistenceState Fires an assertion if an undefined enum is given."); EXPECT_DEATH( - static_cast(details::convertFileExistenceState( + static_cast(convertFileExistenceState( static_cast(static_cast(fb::FileExistenceState::MAX) + 1))), ".*"); } @@ -633,43 +648,65 @@ TEST_F(ConverterTest, ConvertFileExistenceStateMapsDeath) TEST_F(ConverterTest, ConvertFileExistenceStateMapsBothValues) { RecordProperty("Description", "convertFileExistenceState maps both enum values correctly."); - EXPECT_THAT(details::convertFileExistenceState(fb::FileExistenceState::Exists), Eq(FileExistenceState::Exists)); + EXPECT_THAT(convertFileExistenceState(fb::FileExistenceState::Exists), Eq(FileExistenceState::Exists)); EXPECT_THAT( - details::convertFileExistenceState(fb::FileExistenceState::NotExisting), Eq(FileExistenceState::NotExisting)); -} - -TEST_F(ConverterTest, ConvertFileStateNullReturnsNullopt) -{ - RecordProperty("Description", "convertFileState returns nullopt when passed nullptr."); - auto result = details::convertFileState(nullptr); - EXPECT_THAT(result.has_value(), IsFalse()); + convertFileExistenceState(fb::FileExistenceState::NotExisting), Eq(FileExistenceState::NotExisting)); } TEST_F(ConverterTest, ConvertFileStateValid) { - RecordProperty("Description", "convertFileState maps file_path and an explicit state correctly."); + RecordProperty("Description", "convertFileState maps file_path, an explicit state and polling_interval correctly."); ::flatbuffers::FlatBufferBuilder fbb; - auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::NotExisting); + auto fs = + fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::NotExisting, 0.3 /*polling_interval*/); fbb.Finish(fs); const auto* ptr = ::flatbuffers::GetRoot(fbb.GetBufferPointer()); - auto result = details::convertFileState(ptr); + auto result = convertFileState(*ptr); ASSERT_THAT(result.has_value(), IsTrue()); EXPECT_THAT(result->file_path, Eq("/tmp/ready")); EXPECT_THAT(result->state, Eq(FileExistenceState::NotExisting)); + EXPECT_THAT(result->polling_interval, Eq(std::chrono::milliseconds{300})); } TEST_F(ConverterTest, ConvertFileStateDefaultsToExists) { - RecordProperty("Description", "convertFileState defaults state to Exists when not specified."); + RecordProperty("Description", "convertFileState defaults state to Exists and polling_interval to 10ms."); ::flatbuffers::FlatBufferBuilder fbb; auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready"); fbb.Finish(fs); const auto* ptr = ::flatbuffers::GetRoot(fbb.GetBufferPointer()); - auto result = details::convertFileState(ptr); + auto result = convertFileState(*ptr); ASSERT_THAT(result.has_value(), IsTrue()); EXPECT_THAT(result->state, Eq(FileExistenceState::Exists)); + EXPECT_THAT(result->polling_interval, Eq(std::chrono::milliseconds{10})); +} + +TEST_F(ConverterTest, ConvertFileStateNegativePollingIntervalReturnsError) +{ + RecordProperty("Description", "convertFileState returns InvalidFormat for a negative polling_interval."); + ::flatbuffers::FlatBufferBuilder fbb; + auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::Exists, -0.5 /*polling_interval*/); + fbb.Finish(fs); + const auto* ptr = ::flatbuffers::GetRoot(fbb.GetBufferPointer()); + + auto result = convertFileState(*ptr); + ASSERT_THAT(result.has_value(), IsFalse()); + EXPECT_THAT(result.error(), Eq(IConfigLoader::Error::InvalidFormat)); +} + +TEST_F(ConverterTest, ConvertFileStateSubMillisecondPollingIntervalReturnsError) +{ + RecordProperty("Description", "convertFileState returns InvalidFormat for a sub-millisecond polling_interval."); + ::flatbuffers::FlatBufferBuilder fbb; + auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::Exists, 0.0001 /*polling_interval*/); + fbb.Finish(fs); + const auto* ptr = ::flatbuffers::GetRoot(fbb.GetBufferPointer()); + + auto result = convertFileState(*ptr); + ASSERT_THAT(result.has_value(), IsFalse()); + EXPECT_THAT(result.error(), Eq(IConfigLoader::Error::InvalidFormat)); } TEST_F(ConverterTest, ConvertSandboxValid) diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp index fd43b78851..418af14417 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_info_node.cpp @@ -53,20 +53,26 @@ IComponent::RequestResult ProcessInfoNode::tryReportCompletion(score::mw::lifecy { ProcessState desired_state{}; - SCORE_LANGUAGE_FUTURECPP_ASSERT_MESSAGE( - config_.component_properties.ready_condition.has_value() == true, - "component has no ready condition, config should have created a default one"); - auto ready_condition = config_.component_properties.ready_condition.value(); + const auto& ready_condition = config_.component_properties.ready_condition; + + std::visit( + [&desired_state](auto&& arg) { + using ReadyCondT = std::decay_t; + if constexpr (std::is_same_v) + { + switch (arg) + { + case configuration::ProcessState::Running: + desired_state = ProcessState::kRunning; + break; + case configuration::ProcessState::Terminated: + desired_state = ProcessState::kTerminated; + break; + } + } + }, + ready_condition); - switch (ready_condition.process_state) - { - case configuration::ProcessState::Running: - desired_state = ProcessState::kRunning; - break; - case configuration::ProcessState::Terminated: - desired_state = ProcessState::kTerminated; - break; - } if (new_state == ProcessState::kFailed) { // Didn't reach running or startup diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py index 9bde317b1d..1ee449102a 100644 --- a/scripts/config_mapping/lifecycle_config.py +++ b/scripts/config_mapping/lifecycle_config.py @@ -90,7 +90,16 @@ def report_error(message): # There are various dictionaries in the config where only a single entry is allowed. # We do not want to merge the defaults with the user specified values for these dictionaries. -not_merging_dicts = ["ready_recovery_action", "recovery_action"] +not_merging_dicts = ["ready_recovery_action", "recovery_action", "ready_condition"] + + +# Defaults for the optional fields of a "file_state" ready condition. +# note: these are only default when a file_state is configured so it's no in +# the main default config. +file_state_defaults: Dict[str, Any] = { + "state": "Exists", + "polling_interval": 0.01, +} def load_json_file(file_path: str) -> Dict[str, Any]: @@ -106,6 +115,21 @@ def get_working_dir(deployment_config): return deployment_config.get("working_dir", deployment_config["bin_dir"]) +def apply_file_state_defaults(ready_condition): + """Fill in the optional fields of a "file_state" ready condition with + their defaults. Only done if a "file_state" ready condition is configured. + """ + file_state = ready_condition.get("file_state") + is_configured = isinstance(file_state, dict) + if not is_configured: + return + + # user config takes precedence over the defaults + merged = dict(file_state_defaults) + merged.update(file_state) + ready_condition["file_state"] = {**merged} + + def preprocess_defaults(global_defaults, config): """ This function takes the input configuration and fills in any missing fields with default values. @@ -169,6 +193,12 @@ def dict_merge_recursive(dict_a, dict_b): component_config.get("deployment_config", {}), ) + apply_file_state_defaults( + new_config["components"][component_name]["component_properties"].get( + "ready_condition", {} + ) + ) + # If the application_type is not supervised, remove alive_supervision # from component_properties even if it was merged from defaults. app_type = new_config["components"][component_name]["component_properties"][ @@ -495,6 +525,21 @@ def get_process_dependencies( def custom_validations(config): success = True + # A ready condition is either process state or file state (right now), but + # never on both. + for component_name, component_config in config["components"].items(): + ready_condition = component_config["component_properties"].get( + "ready_condition", {} + ) + has_process_state = "process_state" in ready_condition + has_file_state = "file_state" in ready_condition + if has_process_state and has_file_state: + report_error( + f"Component '{component_name}': ready_condition must configure either " + '"process_state" or "file_state", but not both.' + ) + success = False + if "fallback_run_target" in config["run_targets"]: report_error( 'RunTarget name "fallback_run_target" is reserved, please choose a different name.' diff --git a/scripts/config_mapping/unit_tests.py b/scripts/config_mapping/unit_tests.py index 922438fddf..b8eb8cce71 100644 --- a/scripts/config_mapping/unit_tests.py +++ b/scripts/config_mapping/unit_tests.py @@ -450,6 +450,81 @@ def test_preprocessing_no_defaults_section(): assert result["components"]["c1"]["deployment_config"]["bin_dir"] == "/opt" +def _config_with_file_state(file_state): + return { + "schema_version": 1, + "components": { + "c1": { + "component_properties": { + "binary_name": "c1", + "ready_condition": {"file_state": file_state}, + } + } + }, + "run_targets": {"Startup": {}}, + "initial_run_target": "Startup", + "fallback_run_target": {"transition_timeout": 1}, + } + + +def test_preprocessing_file_state_defaults(): + """ + A file_state ready condition only requires a file_path, state and + polling_interval are filled in with their defaults. + """ + config = _config_with_file_state({"file_path": "/tmp/ready"}) + result = preprocess_defaults(score_defaults, config) + ready_condition = result["components"]["c1"]["component_properties"][ + "ready_condition" + ] + assert ready_condition == { + "file_state": { + "file_path": "/tmp/ready", + "state": "Exists", + "polling_interval": 0.01, + } + } + + +def test_preprocessing_file_state_defaults_overridden(): + """ + User specified file_state values take precedence over the defaults. + """ + config = _config_with_file_state( + {"file_path": "/tmp/ready", "state": "NotExisting", "polling_interval": 0.5} + ) + result = preprocess_defaults(score_defaults, config) + file_state = result["components"]["c1"]["component_properties"]["ready_condition"][ + "file_state" + ] + assert file_state["state"] == "NotExisting" + assert file_state["polling_interval"] == 0.5 + + +def test_preprocessing_file_state_defaults_not_applied_for_process_state(): + """ + Without a file_state ready condition, no file_state defaults are added. + """ + config = { + "schema_version": 1, + "components": { + "c1": { + "component_properties": { + "binary_name": "c1", + "ready_condition": {"process_state": "Terminated"}, + } + } + }, + "run_targets": {"Startup": {}}, + "initial_run_target": "Startup", + "fallback_run_target": {"transition_timeout": 1}, + } + result = preprocess_defaults(score_defaults, config) + assert result["components"]["c1"]["component_properties"]["ready_condition"] == { + "process_state": "Terminated" + } + + # --------------------------------------------------------------------------- # check_cyclic_dependencies # --------------------------------------------------------------------------- @@ -592,7 +667,8 @@ def full_valid_config(): "components": { "app1": { "component_properties": { - "application_profile": {"application_type": "REPORTING"} + "application_profile": {"application_type": "REPORTING"}, + "ready_condition": {"process_state": "Running"}, } } }, @@ -644,6 +720,25 @@ def test_custom_validations_recovery_target_not_fallback(full_valid_config): assert custom_validations(full_valid_config) is False +def test_custom_validations_ready_condition_file_state(full_valid_config): + """A ready condition based on the file state alone is valid.""" + full_valid_config["components"]["app1"]["component_properties"][ + "ready_condition" + ] = {"file_state": {"file_path": "/tmp/ready"}} + assert custom_validations(full_valid_config) is True + + +def test_custom_validations_ready_condition_both_states(full_valid_config): + """process_state and file_state must not be configured at the same time.""" + full_valid_config["components"]["app1"]["component_properties"][ + "ready_condition" + ] = { + "process_state": "Running", + "file_state": {"file_path": "/tmp/ready"}, + } + assert custom_validations(full_valid_config) is False + + def test_custom_validations_missing_fallback_run_target(full_valid_config): """fallback_run_target is mandatory.""" del full_valid_config["fallback_run_target"] From ea7c91f3f6e791ea297a0bada808fc645d9bc972 Mon Sep 17 00:00:00 2001 From: Maciej Kaszynski <161459353+MaciejKaszynski@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:12:08 +0100 Subject: [PATCH 4/5] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Nicolas Fußberger <145956508+NicolasFussberger@users.noreply.github.com> Signed-off-by: Maciej Kaszynski <161459353+MaciejKaszynski@users.noreply.github.com> --- .../src/configuration/details/flatbuffer_type_converters.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp index 4f685cf6f4..83709cb232 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp @@ -328,7 +328,7 @@ score::cpp::expected convertFileState(const fb: score::cpp::expected convertReadyCondition(const fb::ReadyCondition* fb_rc) { SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE( - fb_rc != nullptr, "No ReadyCondition is confitured, this should have been defaulted with the script."); + fb_rc != nullptr, "No ReadyCondition is configured, this should have been defaulted with the script."); const bool has_process_state = fb_rc->process_state().has_value(); const bool has_file_state = fb_rc->file_state() != nullptr; From c23eefe67804b10c217bc90848551f29cbf26ebc Mon Sep 17 00:00:00 2001 From: Maciej Kaszynski Date: Wed, 19 Aug 2026 14:33:40 +0100 Subject: [PATCH 5/5] Addressing review comments --- .../src/configuration/component_config.hpp | 4 ++-- .../details/flatbuffer_config_loader_UT.cpp | 3 ++- .../details/flatbuffer_type_converters.cpp | 3 +++ .../details/flatbuffer_type_converters_UT.cpp | 24 +++++++++++++++---- .../src/configuration/details/lm_flatcfg.fbs | 2 +- .../expected_output/lm_config_gen.json | 12 ++++++++-- .../full_config_test/input/lm_config.json | 16 +++++++++++-- 7 files changed, 51 insertions(+), 13 deletions(-) diff --git a/score/launch_manager/src/daemon/src/configuration/component_config.hpp b/score/launch_manager/src/daemon/src/configuration/component_config.hpp index d087688520..ab17ef8726 100644 --- a/score/launch_manager/src/daemon/src/configuration/component_config.hpp +++ b/score/launch_manager/src/daemon/src/configuration/component_config.hpp @@ -59,8 +59,8 @@ enum class FileExistenceState : uint8_t struct FileState { std::string file_path; - FileExistenceState state{FileExistenceState::Exists}; - std::chrono::milliseconds polling_interval{10}; + FileExistenceState state; + std::chrono::milliseconds polling_interval; }; enum class ProcessState : std::uint8_t diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_config_loader_UT.cpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_config_loader_UT.cpp index a630747d7d..db0269bfd4 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_config_loader_UT.cpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_config_loader_UT.cpp @@ -277,7 +277,8 @@ TEST_F(FlatbufferConfigLoaderTest, LoadSingleComponentWithFileState) 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 file_state = + fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::Exists, 0.01 /*polling_interval*/); auto ready_cond = fb::CreateReadyCondition(fbb, std::nullopt, file_state); auto comp_props = fb::CreateComponentProperties( fbb, bin_name, app_profile, 0 /*depends_on*/, 0 /*process_arguments*/, ready_cond); diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp index 83709cb232..1f5724bbd3 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters.cpp @@ -312,6 +312,9 @@ score::cpp::expected convertFileState(const fb: { SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE( fb_fs.file_path(), "FileState::file_path must never be nullptr as it is required in the schema"); + SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE( + fb_fs.polling_interval() != 0.0, + "No FileState::polling_interval is configured, this should have been defaulted with the script."); auto polling_interval_ms = secondsToMs(fb_fs.polling_interval()); if (!polling_interval_ms.has_value()) diff --git a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp index a44a445de7..3591f82cbe 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp +++ b/score/launch_manager/src/daemon/src/configuration/details/flatbuffer_type_converters_UT.cpp @@ -583,7 +583,7 @@ TEST_F(ConverterTest, ConvertReadyConditionWithFileState) { RecordProperty("Description", "convertReadyCondition maps file_state correctly."); ::flatbuffers::FlatBufferBuilder fbb; - auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::Exists); + auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::Exists, 0.01 /*polling_interval*/); auto rc = fb::CreateReadyCondition(fbb, ::flatbuffers::nullopt /*process_state*/, fs); fbb.Finish(rc); const auto* ptr = ::flatbuffers::GetRoot(fbb.GetBufferPointer()); @@ -649,8 +649,7 @@ TEST_F(ConverterTest, ConvertFileExistenceStateMapsBothValues) { RecordProperty("Description", "convertFileExistenceState maps both enum values correctly."); EXPECT_THAT(convertFileExistenceState(fb::FileExistenceState::Exists), Eq(FileExistenceState::Exists)); - EXPECT_THAT( - convertFileExistenceState(fb::FileExistenceState::NotExisting), Eq(FileExistenceState::NotExisting)); + EXPECT_THAT(convertFileExistenceState(fb::FileExistenceState::NotExisting), Eq(FileExistenceState::NotExisting)); } TEST_F(ConverterTest, ConvertFileStateValid) @@ -671,9 +670,10 @@ TEST_F(ConverterTest, ConvertFileStateValid) TEST_F(ConverterTest, ConvertFileStateDefaultsToExists) { - RecordProperty("Description", "convertFileState defaults state to Exists and polling_interval to 10ms."); + RecordProperty("Description", "convertFileState defaults state to Exists if it is not set."); ::flatbuffers::FlatBufferBuilder fbb; - auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready"); + // state is omitted from the buffer since it matches the schema default + auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready", fb::FileExistenceState::Exists, 0.01 /*polling_interval*/); fbb.Finish(fs); const auto* ptr = ::flatbuffers::GetRoot(fbb.GetBufferPointer()); @@ -683,6 +683,20 @@ TEST_F(ConverterTest, ConvertFileStateDefaultsToExists) EXPECT_THAT(result->polling_interval, Eq(std::chrono::milliseconds{10})); } +TEST_F(ConverterTest, ConvertFileStateWithoutPollingIntervalDeath) +{ + RecordProperty( + "Description", + "convertFileState fires an assertion if polling_interval is not configured, as the configuration script " + "always defaults it."); + ::flatbuffers::FlatBufferBuilder fbb; + auto fs = fb::CreateFileStateDirect(fbb, "/tmp/ready"); + fbb.Finish(fs); + const auto* ptr = ::flatbuffers::GetRoot(fbb.GetBufferPointer()); + + EXPECT_DEATH(static_cast(convertFileState(*ptr)), ".*"); +} + TEST_F(ConverterTest, ConvertFileStateNegativePollingIntervalReturnsError) { RecordProperty("Description", "convertFileState returns InvalidFormat for a negative polling_interval."); diff --git a/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs b/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs index c5c6366383..23d352c1a1 100644 --- a/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs +++ b/score/launch_manager/src/daemon/src/configuration/details/lm_flatcfg.fbs @@ -66,7 +66,7 @@ table FileState { // Existence state of the file. Defaults to Exists if not specified. state:FileExistenceState = Exists; // optional, defaults to Exists // Time in seconds to wait between each poll if the file is present. - polling_interval: double = 0.01; //optional, defaults to 0.01s (10ms) + polling_interval: double; // required } // Defines the conditions that determine when the component enters the ready state. diff --git a/scripts/config_mapping/tests/full_config_test/expected_output/lm_config_gen.json b/scripts/config_mapping/tests/full_config_test/expected_output/lm_config_gen.json index 24629f86bc..16a8645ac0 100644 --- a/scripts/config_mapping/tests/full_config_test/expected_output/lm_config_gen.json +++ b/scripts/config_mapping/tests/full_config_test/expected_output/lm_config_gen.json @@ -86,7 +86,11 @@ "--option" ], "ready_condition": { - "process_state": "Running" + "file_state": { + "state": "Exists", + "polling_interval": 0.01, + "file_path": "/var/run/b/ready" + } } }, "deployment_config": { @@ -143,7 +147,11 @@ ], "process_arguments": [], "ready_condition": { - "process_state": "Running" + "file_state": { + "state": "NotExisting", + "polling_interval": 0.5, + "file_path": "/var/run/c/startup.lock" + } } }, "deployment_config": { diff --git a/scripts/config_mapping/tests/full_config_test/input/lm_config.json b/scripts/config_mapping/tests/full_config_test/input/lm_config.json index 9ec08636a6..33f2c2e701 100644 --- a/scripts/config_mapping/tests/full_config_test/input/lm_config.json +++ b/scripts/config_mapping/tests/full_config_test/input/lm_config.json @@ -127,7 +127,12 @@ "application_type": "Native", "is_self_terminating": true }, - "process_arguments": ["-b", "--option"] + "process_arguments": ["-b", "--option"], + "ready_condition": { + "file_state": { + "file_path": "/var/run/b/ready" + } + } }, "deployment_config": { "bin_dir": "/opt/apps/b", @@ -146,7 +151,14 @@ "application_type": "Reporting", "is_self_terminating": false }, - "depends_on": ["component_a"] + "depends_on": ["component_a"], + "ready_condition": { + "file_state": { + "file_path": "/var/run/c/startup.lock", + "state": "NotExisting", + "polling_interval": 0.5 + } + } }, "deployment_config": { "bin_dir": "/opt/apps/c",