Skip to content
Merged
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
7 changes: 7 additions & 0 deletions src/ros2_medkit_gateway/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,12 @@ if(BUILD_TESTING)
ament_add_gtest(test_error_info test/test_error_info.cpp)
target_link_libraries(test_error_info gateway_ros2)

# Parameter error precedence tests (multi-node GET aggregation, live GatewayNode)
ament_add_gtest(test_parameter_error_precedence test/test_parameter_error_precedence.cpp)
target_link_libraries(test_parameter_error_precedence gateway_ros2)
medkit_target_dependencies(test_parameter_error_precedence rclcpp)
medkit_set_test_domain(test_parameter_error_precedence)

# Add ROS 2 subscription executor tests
ament_add_gtest(test_ros2_subscription_executor test/test_ros2_subscription_executor.cpp)
target_link_libraries(test_ros2_subscription_executor gateway_ros2)
Expand Down Expand Up @@ -1051,6 +1057,7 @@ if(BUILD_TESTING)
test_tls_config
test_fault_manager
test_error_info
test_parameter_error_precedence
test_lifecycle_status_helpers
test_ros2_lifecycle_state_reader
test_ros2_subscription_executor
Expand Down
94 changes: 75 additions & 19 deletions src/ros2_medkit_gateway/src/http/handlers/config_handlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,16 @@ const NodeConfigInfo * find_node_for_app(const std::vector<NodeConfigInfo> & nod
return nullptr;
}

/// Error classification result for parameter operations.
struct ErrorClassification {
int status_code;
/// HTTP status + SOVD error code for a failed parameter operation.
struct ParameterErrorClassification {
int status_code = 500;
std::string error_code;
};

/// Map a structured `ParameterErrorCode` to an HTTP status + SOVD error code.
/// Identical mapping to the legacy `classify_error_code` helper.
ErrorClassification classify_error_code(ParameterErrorCode error_code) {
ErrorClassification result;
ParameterErrorClassification classify_error_code(ParameterErrorCode error_code) {
ParameterErrorClassification result;

switch (error_code) {
case ParameterErrorCode::NOT_FOUND:
Expand Down Expand Up @@ -165,14 +165,14 @@ ErrorClassification classify_error_code(ParameterErrorCode error_code) {
/// Classify a ParameterResult into HTTP status + error code. Prefers the
/// structured `error_code` field and falls back to string-matching the legacy
/// error messages for backward compatibility with older transports.
ErrorClassification classify_parameter_error(const ParameterResult & result) {
ParameterErrorClassification classify_parameter_error(const ParameterResult & result) {
// Prefer structured error code
if (result.error_code != ParameterErrorCode::NONE) {
return classify_error_code(result.error_code);
}

// Fallback to string parsing for backward compatibility
ErrorClassification classification;
ParameterErrorClassification classification;
const auto & error_message = result.error_message;

if (error_message.find("not found") != std::string::npos ||
Expand Down Expand Up @@ -312,6 +312,66 @@ void apply_fan_out_observability(dto::ConfigListXMedkit & xm,
}
}

/// Severity rank for order-independent folding: node-unavailable (503) is the
/// most actionable verdict (retry), then other server errors, then non-404
/// client errors; NOT_FOUND ranks lowest so it can never mask a real failure.
int severity_rank(int status_code) {
if (status_code == 404) {
return 0;
}
if (status_code == 503) {
return 3;
}
if (status_code >= 500) {
return 2;
}
return 1; // non-404 4xx
}

/// Folds per-node parameter lookup failures so the error surfaced when no
/// node succeeds does not depend on node iteration order. Failures are
/// ranked by `severity_rank`; the highest rank folded so far is kept, and
/// within a rank the first failure folded wins.
class ParameterErrorAccumulator {
public:
/// Fold one failed ParameterResult.
void add(const ParameterResult & result) {
auto err = classify_parameter_error(result);
const int rank = severity_rank(err.status_code);
if (rank > 0) {
all_not_found_ = false;
}
// Strictly-greater keeps the first failure folded at the winning rank, so
// the surfaced status never depends on node iteration order.
if (rank > worst_rank_) {
worst_ = result;
classification_ = std::move(err);
worst_rank_ = rank;
}
}

/// True when every folded failure classified as 404.
bool all_not_found() const {
return all_not_found_;
}

/// The failure to surface: the highest-severity failure folded so far.
const ParameterResult & worst() const {
return worst_;
}

/// Classification of worst().
const ParameterErrorClassification & classification() const {
return classification_;
}

private:
ParameterResult worst_;
ParameterErrorClassification classification_;
int worst_rank_ = -1;
bool all_not_found_ = true;
};

} // namespace

// ===========================================================================
Expand Down Expand Up @@ -476,10 +536,10 @@ http::Result<dto::ConfigurationReadValue> ConfigHandlers::get_configuration(cons

// For non-aggregated entities (or aggregated entities without a prefix) we
// probe every backing node for the parameter and return the first success.
// Track errors so a non-404 from any node (e.g. unavailable) wins over a
// pure "not found" verdict in the no-success branch.
ParameterResult last_result;
bool all_not_found = true;
// Failures fold through ParameterErrorAccumulator, which ranks them by
// severity (503 > other 5xx > non-404 4xx > 404), so the surfaced error is
// the same regardless of node iteration order.
ParameterErrorAccumulator errors;

for (const auto & node_info : agg_configs.nodes) {
auto result = config_mgr->get_parameter(node_info.node_fqn, parsed.param_name);
Expand All @@ -491,23 +551,19 @@ http::Result<dto::ConfigurationReadValue> ConfigHandlers::get_configuration(cons
return make_read_value(entity_id, node_info.node_fqn, parsed.param_name, source_app, result.data);
}

last_result = result;
auto err = classify_parameter_error(result);
if (err.status_code != 404) {
all_not_found = false;
}
errors.add(result);
}

if (all_not_found) {
if (errors.all_not_found()) {
return tl::unexpected(make_error(404, ERR_RESOURCE_NOT_FOUND, "Parameter not found",
json{{"entity_id", entity_id}, {"id", param_id}}));
}

// Some node reported a non-404 (e.g. unavailable); surface that.
auto err = classify_parameter_error(last_result);
const auto & err = errors.classification();
return tl::unexpected(
make_error(err.status_code, err.error_code, "Failed to get parameter from any node",
json{{"details", last_result.error_message}, {"entity_id", entity_id}, {"id", param_id}}));
json{{"details", errors.worst().error_message}, {"entity_id", entity_id}, {"id", param_id}}));
}

// ===========================================================================
Expand Down
Loading
Loading