Skip to content

Commit

Permalink
feat: throw an error when there is not initialized database loaded yet
Browse files Browse the repository at this point in the history
  • Loading branch information
fengelniederhammer committed Feb 26, 2024
1 parent a2499af commit 19a8c40
Show file tree
Hide file tree
Showing 12 changed files with 285 additions and 90 deletions.
2 changes: 1 addition & 1 deletion .run/silo--api.run.xml → .run/silo --api.run.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="silo --api" type="CMakeRunConfiguration" factoryName="Application" PROGRAM_PARAMS="--api --dataDirectory output/" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" WORKING_DIR="file://.." PASS_PARENT_ENVS_2="true" PROJECT_NAME="SILO" TARGET_NAME="siloApi" CONFIG_NAME="Debug" RUN_TARGET_PROJECT_NAME="SILO" RUN_TARGET_NAME="siloApi">
<configuration default="false" name="silo --api" type="CMakeRunConfiguration" factoryName="Application" PROGRAM_PARAMS="--api --dataDirectory output/ --estimatedStartupTimeInMinutes 2" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" WORKING_DIR="file://.." PASS_PARENT_ENVS_2="true" PROJECT_NAME="SILO" TARGET_NAME="siloApi" CONFIG_NAME="Debug" RUN_TARGET_PROJECT_NAME="SILO" RUN_TARGET_NAME="siloApi">
<envs>
<env name="SPDLOG_LEVEL" value="debug" />
</envs>
Expand Down
7 changes: 7 additions & 0 deletions include/silo_api/database_mutex.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,16 @@ class FixedDatabase {
const silo::Database& database;
};

class UninitializedDatabaseException : public std::runtime_error {
public:
UninitializedDatabaseException()
: std::runtime_error("Database not initialized yet") {}
};

class DatabaseMutex {
std::shared_mutex mutex;
silo::Database database;
bool is_initialized = false;

public:
DatabaseMutex() = default;
Expand Down
16 changes: 15 additions & 1 deletion include/silo_api/error_request_handler.h
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
#pragma once

#include <chrono>
#include <memory>
#include <optional>
#include <string>

#include <Poco/Net/HTTPRequestHandler.h>
#include <nlohmann/json.hpp>

namespace silo_api {

struct StartupConfig {
std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds> start_time;
std::optional<std::chrono::minutes> estimated_startup_time;
};

struct ErrorResponse {
std::string error;
std::string message;
Expand All @@ -18,14 +25,21 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(ErrorResponse, error, message);
class ErrorRequestHandler : public Poco::Net::HTTPRequestHandler {
private:
std::unique_ptr<Poco::Net::HTTPRequestHandler> wrapped_handler;
const StartupConfig& startup_config;

public:
explicit ErrorRequestHandler(Poco::Net::HTTPRequestHandler* wrapped_handler);
explicit ErrorRequestHandler(
Poco::Net::HTTPRequestHandler* wrapped_handler,
const StartupConfig& startup_config
);

void handleRequest(
Poco::Net::HTTPServerRequest& request,
Poco::Net::HTTPServerResponse& response
) override;

private:
std::optional<std::string> computeRetryAfter();
};

}; // namespace silo_api
5 changes: 4 additions & 1 deletion include/silo_api/request_handler_factory.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
#include <Poco/Net/HTTPRequestHandlerFactory.h>
#include <Poco/Net/HTTPServerRequest.h>

#include "silo_api/error_request_handler.h"

namespace silo_api {
class DatabaseMutex;
} // namespace silo_api
Expand All @@ -13,9 +15,10 @@ namespace silo_api {
class SiloRequestHandlerFactory : public Poco::Net::HTTPRequestHandlerFactory {
private:
silo_api::DatabaseMutex& database;
const StartupConfig startup_config;

public:
SiloRequestHandlerFactory(silo_api::DatabaseMutex& database);
SiloRequestHandlerFactory(silo_api::DatabaseMutex& database, StartupConfig startup_config);

Poco::Net::HTTPRequestHandler* createRequestHandler(const Poco::Net::HTTPServerRequest& request);

Expand Down
85 changes: 62 additions & 23 deletions src/silo_api/api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@
#include "silo_api/request_handler_factory.h"
#include "silo_api/runtime_config.h"

static const std::string ESTIMATED_STARTUP_TIME_IN_MINUTES_OPTION = "estimatedStartupTimeInMinutes";
static const std::string PREPROCESSING_CONFIG_OPTION = "preprocessingConfig";
static const std::string DATABASE_CONFIG_OPTION = "databaseConfig";
static const std::string DATA_DIRECTORY_OPTION = "dataDirectory";
static const std::string API_OPTION = "api";
static const std::string PREPROCESSING_OPTION = "preprocessing";

silo::preprocessing::PreprocessingConfig preprocessingConfig(
const Poco::Util::AbstractConfiguration& config
) {
Expand All @@ -43,9 +50,9 @@ silo::preprocessing::PreprocessingConfig preprocessingConfig(
}

silo::preprocessing::OptionalPreprocessingConfig user_preprocessing_config;
if (config.hasProperty("preprocessingConfig")) {
if (config.hasProperty(PREPROCESSING_CONFIG_OPTION)) {
user_preprocessing_config = silo::preprocessing::PreprocessingConfigReader().readConfig(
config.getString("preprocessingConfig")
config.getString(PREPROCESSING_CONFIG_OPTION)
);
} else if (std::filesystem::exists("./preprocessing_config.yaml")) {
user_preprocessing_config =
Expand All @@ -59,8 +66,9 @@ silo::preprocessing::PreprocessingConfig preprocessingConfig(
}

silo::config::DatabaseConfig databaseConfig(const Poco::Util::AbstractConfiguration& config) {
if (config.hasProperty("databaseConfig")) {
return silo::config::ConfigRepository().getValidatedConfig(config.getString("databaseConfig")
if (config.hasProperty(DATABASE_CONFIG_OPTION)) {
return silo::config::ConfigRepository().getValidatedConfig(
config.getString(DATABASE_CONFIG_OPTION)
);
}
SPDLOG_DEBUG("databaseConfig not found in config file. Using default value: databaseConfig.yaml"
Expand All @@ -72,12 +80,12 @@ std::filesystem::path dataDirectory(
const Poco::Util::AbstractConfiguration& config,
const silo_api::RuntimeConfig& runtime_config
) {
if (config.hasProperty("dataDirectory")) {
if (config.hasProperty(DATA_DIRECTORY_OPTION)) {
SPDLOG_DEBUG(
"Using dataDirectory passed via command line argument: {}",
config.getString("dataDirectory")
config.getString(DATA_DIRECTORY_OPTION)
);
return config.getString("dataDirectory");
return config.getString(DATA_DIRECTORY_OPTION);
}
if (runtime_config.data_directory.has_value()) {
SPDLOG_DEBUG(
Expand Down Expand Up @@ -107,45 +115,64 @@ class SiloServer : public Poco::Util::ServerApplication {
);

options.addOption(
Poco::Util::Option("preprocessingConfig", "pc", "path to the preprocessing config file")
Poco::Util::Option(
PREPROCESSING_CONFIG_OPTION, "pc", "path to the preprocessing config file"
)
.required(false)
.repeatable(false)
.argument("PATH")
.binding("preprocessingConfig")
.binding(PREPROCESSING_CONFIG_OPTION)
);

options.addOption(
Poco::Util::Option("databaseConfig", "dc", "path to the database config file")
Poco::Util::Option(DATABASE_CONFIG_OPTION, "dc", "path to the database config file")
.required(false)
.repeatable(false)
.argument("PATH")
.binding("databaseConfig")
.binding(DATABASE_CONFIG_OPTION)
);

options.addOption(Poco::Util::Option("dataDirectory", "d", "path to the preprocessed data")
.required(false)
.repeatable(false)
.argument("PATH")
.binding("dataDirectory"));
options.addOption(
Poco::Util::Option(DATA_DIRECTORY_OPTION, "d", "path to the preprocessed data")
.required(false)
.repeatable(false)
.argument("PATH")
.binding(DATA_DIRECTORY_OPTION)
);

options.addOption(
Poco::Util::Option("api", "a", "Execution mode: start the SILO web interface")
Poco::Util::Option(API_OPTION, "a", "Execution mode: start the SILO web interface")
.required(false)
.repeatable(false)
.binding("api")
.binding(API_OPTION)
.group("executionMode")
);

options.addOption(Poco::Util::Option(
"preprocessing",
PREPROCESSING_OPTION,
"p",
"Execution mode: trigger the preprocessing pipeline to generate a "
"partitioned dataset that can be read by the database"
)
.required(false)
.repeatable(false)
.binding("preprocessing")
.binding(PREPROCESSING_OPTION)
.group("executionMode"));

options.addOption(
Poco::Util::Option(
ESTIMATED_STARTUP_TIME_IN_MINUTES_OPTION,
"t",
"Estimated time in minutes that the initial loading of the database takes. "
"As long as no database is loaded yet, SILO will throw a 503 error. "
"This option allows SILO to compute a Retry-After header for the 503 response. ",
false
)
.required(false)
.repeatable(false)
.argument("MINUTES", true)
.binding(ESTIMATED_STARTUP_TIME_IN_MINUTES_OPTION)
);
}

int main(const std::vector<std::string>& args) override {
Expand All @@ -156,11 +183,11 @@ class SiloServer : public Poco::Util::ServerApplication {
return Application::EXIT_USAGE;
}

if (config().hasProperty("api")) {
if (config().hasProperty(API_OPTION)) {
return handleApi();
}

if (config().hasProperty("preprocessing")) {
if (config().hasProperty(PREPROCESSING_OPTION)) {
return handlePreprocessing();
}

Expand All @@ -171,6 +198,18 @@ class SiloServer : public Poco::Util::ServerApplication {
}

private:
silo_api::StartupConfig getStartupConfig() {
const auto now = std::chrono::system_clock::now();
const auto estimated_startup_time_in_minutes =
config().hasProperty(ESTIMATED_STARTUP_TIME_IN_MINUTES_OPTION)
? std::optional(
std::chrono::minutes(config().getInt(ESTIMATED_STARTUP_TIME_IN_MINUTES_OPTION))
)
: std::nullopt;

return {now, estimated_startup_time_in_minutes};
}

int handleApi() {
SPDLOG_INFO("Starting SILO API");
const int port = 8081;
Expand All @@ -189,7 +228,7 @@ class SiloServer : public Poco::Util::ServerApplication {
const silo_api::DatabaseDirectoryWatcher watcher(data_directory, database_mutex);

Poco::Net::HTTPServer server(
new silo_api::SiloRequestHandlerFactory(database_mutex),
new silo_api::SiloRequestHandlerFactory(database_mutex, getStartupConfig()),
server_socket,
new Poco::Net::HTTPServerParams
);
Expand Down
22 changes: 13 additions & 9 deletions src/silo_api/database_directory_watcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -121,15 +121,19 @@ void silo_api::DatabaseDirectoryWatcher::checkDirectoryForData(Poco::Timer& /*ti
}

{
const auto fixed_database = database_mutex.getDatabase();
if (fixed_database.database.getDataVersion() >= most_recent_database_state->second) {
SPDLOG_TRACE(
"Do not update data version to {} in path {}. Its version is not newer than the "
"current version",
most_recent_database_state->second.toString(),
most_recent_database_state->first.string()
);
return;
try {
const auto fixed_database = database_mutex.getDatabase();
if (fixed_database.database.getDataVersion() >= most_recent_database_state->second) {
SPDLOG_TRACE(
"Do not update data version to {} in path {}. Its version is not newer than the "
"current version",
most_recent_database_state->second.toString(),
most_recent_database_state->first.string()
);
return;
}
} catch (const silo_api::UninitializedDatabaseException& exception) {
SPDLOG_DEBUG("No database loaded yet - continuing to load initial database.");
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/silo_api/database_mutex.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@ silo_api::FixedDatabase::FixedDatabase(
void silo_api::DatabaseMutex::setDatabase(silo::Database&& new_database) {
const std::unique_lock lock(mutex);
database = std::move(new_database);
is_initialized = true;
}

silo_api::FixedDatabase silo_api::DatabaseMutex::getDatabase() {
if (!is_initialized) {
throw silo_api::UninitializedDatabaseException();
}
std::shared_lock<std::shared_mutex> lock(mutex);
return {database, std::move(lock)};
}
71 changes: 60 additions & 11 deletions src/silo_api/error_request_handler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,82 @@
#include <spdlog/spdlog.h>
#include <nlohmann/json.hpp>

#include "silo_api/database_mutex.h"

namespace silo_api {
ErrorRequestHandler::ErrorRequestHandler(Poco::Net::HTTPRequestHandler* wrapped_handler)
: wrapped_handler(wrapped_handler) {}
ErrorRequestHandler::ErrorRequestHandler(
Poco::Net::HTTPRequestHandler* wrapped_handler,
const StartupConfig& startup_config
)
: wrapped_handler(wrapped_handler),
startup_config(startup_config) {}

void ErrorRequestHandler::handleRequest(
Poco::Net::HTTPServerRequest& request,
Poco::Net::HTTPServerResponse& response
) {
try {
wrapped_handler->handleRequest(request, response);
} catch (const silo_api::UninitializedDatabaseException& exception) {
SPDLOG_INFO("Caught exception: {}", exception.what());

response.setStatusAndReason(Poco::Net::HTTPResponse::HTTP_SERVICE_UNAVAILABLE);
std::string message = "Database not initialized yet.";

const auto retry_after = computeRetryAfter();
if (retry_after.has_value()) {
response.set("Retry-After", retry_after.value());
message += " Please try again after " + retry_after.value() + " seconds.";
}
std::ostream& out_stream = response.send();
out_stream << nlohmann::json(ErrorResponse{"Service Temporarily Unavailable", message});
} catch (const std::exception& exception) {
SPDLOG_ERROR("Caught exception: {}", exception.what());

response.setStatus(Poco::Net::HTTPResponse::HTTP_INTERNAL_SERVER_ERROR);
response.setStatusAndReason(Poco::Net::HTTPResponse::HTTP_INTERNAL_SERVER_ERROR);
std::ostream& out_stream = response.send();
out_stream << nlohmann::json(ErrorResponse{"Internal server error", exception.what()});
out_stream << nlohmann::json(ErrorResponse{"Internal Server Error", exception.what()});
} catch (const std::string& ex) {
SPDLOG_ERROR(ex);
response.setStatusAndReason(Poco::Net::HTTPResponse::HTTP_INTERNAL_SERVER_ERROR);
std::ostream& out_stream = response.send();
out_stream << nlohmann::json(ErrorResponse{"Internal Server Error", ex});
} catch (...) {
SPDLOG_ERROR("Query cancelled with uncatchable (...) exception");
response.setStatusAndReason(Poco::Net::HTTPResponse::HTTP_INTERNAL_SERVER_ERROR);
std::ostream& out_stream = response.send();

const auto exception = std::current_exception();
const auto* message = exception ? abi::__cxa_current_exception_type()->name() : "null";
SPDLOG_ERROR("Caught something unexpected: {}", message);
if (exception) {
const auto* message = abi::__cxa_current_exception_type()->name();
SPDLOG_ERROR("current_exception: {}", message);
out_stream << nlohmann::json(ErrorResponse{"Internal Server Error", message});
} else {
out_stream << nlohmann::json(
ErrorResponse{"Internal Server Error", "non recoverable error message"}
);
}
}
}

std::optional<std::string> ErrorRequestHandler::computeRetryAfter() {
if (!startup_config.estimated_startup_time.has_value()) {
return std::nullopt;
}

response.setStatus(Poco::Net::HTTPResponse::HTTP_INTERNAL_SERVER_ERROR);
const auto now = std::chrono::system_clock::now();

std::ostream& out_stream = response.send();
out_stream << nlohmann::json(
ErrorResponse{"Internal server error", "Caught something: " + std::string(message)}
);
const auto startup_time_end =
startup_config.start_time + startup_config.estimated_startup_time.value();

const auto remaining_startup_time_in_seconds =
std::chrono::duration_cast<std::chrono::seconds>(startup_time_end - now).count();

if (remaining_startup_time_in_seconds <= 0) {
return std::nullopt;
}

return std::to_string(remaining_startup_time_in_seconds);
}

} // namespace silo_api
Loading

0 comments on commit 19a8c40

Please sign in to comment.