Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: exceptions during command calls / handling #210

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions include/framework/everest.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,43 @@
using TelemetryMap = std::map<std::string, TelemetryEntry>;
using UnsubscribeToken = std::function<void()>;

enum class ErrorType {
MessageParsing,
SchemaValidation,
HandlerException,
Timeout,
Shutdown,
Unknown
};

struct ErrorMessage {
ErrorType type;

Check notice on line 46 in include/framework/everest.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

include/framework/everest.hpp#L46

struct member 'ErrorMessage::type' is never used.
std::string msg;

Check notice on line 47 in include/framework/everest.hpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

include/framework/everest.hpp#L47

struct member 'ErrorMessage::msg' is never used.
};

namespace conversions {
std::string error_type_to_string(ErrorType error_type);
ErrorType string_to_error_type(const std::string& error_type_string);
} // namespace conversions

void to_json(nlohmann::json& j, const ErrorMessage& e);
void from_json(const nlohmann::json& j, ErrorMessage& e);

/// \brief Result of a command
struct CmdResult {
std::optional<json> result;
std::optional<ErrorMessage> error;

// CmdResult(std::optional<json> result = std::nullopt, std::optional<ErrorMessage> error = std::nullopt) :
// result(result), error(error) {
// }
};

class EverestCmdError : public Everest::EverestBaseRuntimeError {
public:
using EverestBaseRuntimeError::EverestBaseRuntimeError;
};

namespace error {
struct ErrorDatabaseMap;
struct ErrorManagerImpl;
Expand Down
110 changes: 98 additions & 12 deletions lib/everest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,54 @@ namespace Everest {
const auto remote_cmd_res_timeout_seconds = 300;
const std::array<std::string, 3> TELEMETRY_RESERVED_KEYS = {{"connector_id"}};

namespace conversions {
constexpr auto ERROR_TYPE_MESSAGE_PARSING = "MessageParsing";
constexpr auto ERROR_TYPE_SCHEMA_VALIDATION = "SchemaValidation";
constexpr auto ERROR_TYPE_HANDLER_EXCEPTION = "HandlerException";
constexpr auto ERROR_TYPE_TIMEOUT = "Timeout";
constexpr auto ERROR_TYPE_SHUTDOWN = "Shutdown";
constexpr auto ERROR_TYPE_UNKNOWN = "Unknown";
std::string error_type_to_string(ErrorType error_type) {
switch (error_type) {
case ErrorType::MessageParsing:
return ERROR_TYPE_MESSAGE_PARSING;
break;
case ErrorType::SchemaValidation:
return ERROR_TYPE_SCHEMA_VALIDATION;
break;
case ErrorType::HandlerException:
return ERROR_TYPE_HANDLER_EXCEPTION;
break;
case ErrorType::Timeout:
return ERROR_TYPE_TIMEOUT;
break;
case ErrorType::Shutdown:
return ERROR_TYPE_SHUTDOWN;
break;
case ErrorType::Unknown:
return ERROR_TYPE_UNKNOWN;
break;
}

return ERROR_TYPE_UNKNOWN;
}
ErrorType string_to_error_type(const std::string& error_type_string) {
if (error_type_string == ERROR_TYPE_MESSAGE_PARSING) {
return ErrorType::MessageParsing;
} else if (error_type_string == ERROR_TYPE_SCHEMA_VALIDATION) {
return ErrorType::SchemaValidation;
} else if (error_type_string == ERROR_TYPE_HANDLER_EXCEPTION) {
return ErrorType::HandlerException;
} else if (error_type_string == ERROR_TYPE_TIMEOUT) {
return ErrorType::Timeout;
} else if (error_type_string == ERROR_TYPE_SHUTDOWN) {
return ErrorType::Shutdown;
}

return ErrorType::Unknown;
}
} // namespace conversions

Everest::Everest(std::string module_id_, const Config& config_, bool validate_data_with_schema,
const std::string& mqtt_server_socket_path, const std::string& mqtt_server_address,
int mqtt_server_port, const std::string& mqtt_everest_prefix, const std::string& mqtt_external_prefix,
Expand Down Expand Up @@ -354,8 +402,8 @@ json Everest::call_cmd(const Requirement& req, const std::string& cmd_name, json

std::string call_id = boost::uuids::to_string(boost::uuids::random_generator()());

std::promise<json> res_promise;
std::future<json> res_future = res_promise.get_future();
std::promise<CmdResult> res_promise;
std::future<CmdResult> res_future = res_promise.get_future();

Handler res_handler = [this, &res_promise, call_id, connection, cmd_name, return_type](json data) {
auto& data_id = data.at("id");
Expand All @@ -368,7 +416,14 @@ json Everest::call_cmd(const Requirement& req, const std::string& cmd_name, json
"Incoming res {} for {}->{}()", data_id,
this->config.printable_identifier(connection["module_id"], connection["implementation_id"]), cmd_name);

res_promise.set_value(std::move(data["retval"]));
if (data.contains("error")) {
EVLOG_error << fmt::format(
"Received error {} for {}->{}()", data.at("error"),
this->config.printable_identifier(connection["module_id"], connection["implementation_id"]), cmd_name);
res_promise.set_value(CmdResult{std::nullopt, data.at("error")});
} else {
res_promise.set_value(CmdResult{std::move(data["retval"]), std::nullopt});
}
};

const auto cmd_topic =
Expand All @@ -393,7 +448,7 @@ json Everest::call_cmd(const Requirement& req, const std::string& cmd_name, json
res_future_status = res_future.wait_until(res_wait);
} while (res_future_status == std::future_status::deferred);

json result;
CmdResult result;
if (res_future_status == std::future_status::timeout) {
EVLOG_AND_THROW(EverestTimeoutError(fmt::format(
"Timeout while waiting for result of {}->{}()",
Expand All @@ -404,7 +459,14 @@ json Everest::call_cmd(const Requirement& req, const std::string& cmd_name, json
}
this->mqtt_abstraction.unregister_handler(cmd_topic, res_token);

return result;
if (result.error.has_value()) {
auto& error = result.error.value();
throw EverestCmdError(fmt::format("{}: {}", conversions::error_type_to_string(error.type), error.msg));
} else if (not result.result.has_value()) {
throw EverestCmdError("Command did not return result");
} else {
return result.result.value();
}
}

void Everest::publish_var(const std::string& impl_id, const std::string& var_name, json value) {
Expand Down Expand Up @@ -846,6 +908,11 @@ void Everest::provide_cmd(const std::string impl_id, const std::string cmd_name,
this->config.printable_identifier(this->module_id, impl_id), cmd_name,
fmt::join(arg_names, ","));

json res_data = json({});
// FIXME: this id lookup might fail -> return MessageParsing error
res_data["id"] = data["id"];
std::optional<ErrorMessage> error;

// check data and ignore it if not matching (publishing it should have
// been prohibited already)
if (this->validate_data_with_schema) {
Expand All @@ -865,19 +932,26 @@ void Everest::provide_cmd(const std::string impl_id, const std::string cmd_name,
} catch (const std::exception& e) {
EVLOG_warning << fmt::format("Ignoring incoming cmd '{}' because not matching manifest schema: {}",
cmd_name, e.what());
return;
error = ErrorMessage{ErrorType::SchemaValidation, e.what()};
}
}

// publish results
json res_data = json({});
res_data["id"] = data["id"];

// call real cmd handler
res_data["retval"] = handler(data["args"]);
try {
if (not error.has_value()) {
res_data["retval"] = handler(data["args"]);
}
} catch (const std::exception& e) {
EVLOG_verbose << fmt::format("Exception during handling of: {}->{}({}): {}",
this->config.printable_identifier(this->module_id, impl_id), cmd_name,
fmt::join(arg_names, ","), e.what());
error = ErrorMessage{ErrorType::HandlerException, e.what()};
}

// check retval agains manifest
if (this->validate_data_with_schema) {
if (not error.has_value() && this->validate_data_with_schema) {
try {
// only use validator on non-null return types
if (!(res_data["retval"].is_null() &&
Expand All @@ -893,12 +967,14 @@ void Everest::provide_cmd(const std::string impl_id, const std::string cmd_name,
EVLOG_warning << fmt::format("Ignoring return value of cmd '{}' because the validation of the result "
"failed: {}\ndefinition: {}\ndata: {}",
cmd_name, e.what(), cmd_definition, res_data);
return;
error = ErrorMessage{ErrorType::SchemaValidation, e.what()};
}
}

EVLOG_verbose << fmt::format("RETVAL: {}", res_data["retval"].dump());
res_data["origin"] = this->module_id;
if (error.has_value()) {
res_data["error"] = error.value();
}

json res_publish_data = json::object({{"name", cmd_name}, {"type", "result"}, {"data", res_data}});

Expand Down Expand Up @@ -1076,4 +1152,14 @@ bool Everest::check_arg(ArgumentType arg_types, json manifest_arg) {
}
return true;
}

// TODO fix these conversions
void to_json(nlohmann::json& j, const ErrorMessage& e) {
j = {{"type", conversions::error_type_to_string(e.type)}, {"msg", e.msg}};
}

void from_json(const nlohmann::json& j, ErrorMessage& e) {
e.type = conversions::string_to_error_type(j.at("type"));
e.msg = j.at("msg");
}
} // namespace Everest
Loading