diff --git a/.run/silo--api.run.xml b/.run/silo --api.run.xml similarity index 57% rename from .run/silo--api.run.xml rename to .run/silo --api.run.xml index d62b633b8..734ce51dd 100644 --- a/.run/silo--api.run.xml +++ b/.run/silo --api.run.xml @@ -1,5 +1,5 @@ - + diff --git a/include/silo_api/database_mutex.h b/include/silo_api/database_mutex.h index 1bfecf20b..6a1bdd8e2 100644 --- a/include/silo_api/database_mutex.h +++ b/include/silo_api/database_mutex.h @@ -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; diff --git a/include/silo_api/error_request_handler.h b/include/silo_api/error_request_handler.h index d68948ad3..3e4d633b3 100644 --- a/include/silo_api/error_request_handler.h +++ b/include/silo_api/error_request_handler.h @@ -1,6 +1,8 @@ #pragma once +#include #include +#include #include #include @@ -8,6 +10,11 @@ namespace silo_api { +struct StartupConfig { + std::chrono::time_point start_time; + std::optional estimated_startup_time; +}; + struct ErrorResponse { std::string error; std::string message; @@ -18,14 +25,21 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(ErrorResponse, error, message); class ErrorRequestHandler : public Poco::Net::HTTPRequestHandler { private: std::unique_ptr 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 computeRetryAfter(); }; }; // namespace silo_api diff --git a/include/silo_api/request_handler_factory.h b/include/silo_api/request_handler_factory.h index 29ce65916..a6bddb8b0 100644 --- a/include/silo_api/request_handler_factory.h +++ b/include/silo_api/request_handler_factory.h @@ -4,6 +4,8 @@ #include #include +#include "silo_api/error_request_handler.h" + namespace silo_api { class DatabaseMutex; } // namespace silo_api @@ -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); diff --git a/src/silo_api/api.cpp b/src/silo_api/api.cpp index 32076457f..48a21ddb1 100644 --- a/src/silo_api/api.cpp +++ b/src/silo_api/api.cpp @@ -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 ) { @@ -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 = @@ -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" @@ -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( @@ -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& args) override { @@ -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(); } @@ -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; @@ -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 ); diff --git a/src/silo_api/database_directory_watcher.cpp b/src/silo_api/database_directory_watcher.cpp index 9cc2eb17b..c970d5e88 100644 --- a/src/silo_api/database_directory_watcher.cpp +++ b/src/silo_api/database_directory_watcher.cpp @@ -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."); } } diff --git a/src/silo_api/database_mutex.cpp b/src/silo_api/database_mutex.cpp index 7bf858ccb..0d3eedccb 100644 --- a/src/silo_api/database_mutex.cpp +++ b/src/silo_api/database_mutex.cpp @@ -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 lock(mutex); return {database, std::move(lock)}; } diff --git a/src/silo_api/error_request_handler.cpp b/src/silo_api/error_request_handler.cpp index ea1ec8b74..79a45e495 100644 --- a/src/silo_api/error_request_handler.cpp +++ b/src/silo_api/error_request_handler.cpp @@ -10,9 +10,15 @@ #include #include +#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, @@ -20,23 +26,66 @@ void ErrorRequestHandler::handleRequest( ) { try { wrapped_handler->handleRequest(request, response); + } catch (const silo_api::UninitializedDatabaseException& exception) { + SPDLOG_INFO("Caught exception: {}", exception.what()); + + response.setStatus(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); std::ostream& out_stream = response.send(); out_stream << nlohmann::json(ErrorResponse{"Internal server error", exception.what()}); + } catch (const std::string& ex) { + SPDLOG_ERROR(ex); + response.setStatus(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.setStatus(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"} + ); + } + } +} - response.setStatus(Poco::Net::HTTPResponse::HTTP_INTERNAL_SERVER_ERROR); +std::optional ErrorRequestHandler::computeRetryAfter() { + if (!startup_config.estimated_startup_time.has_value()) { + return std::nullopt; + } - std::ostream& out_stream = response.send(); - out_stream << nlohmann::json( - ErrorResponse{"Internal server error", "Caught something: " + std::string(message)} - ); + const auto now = std::chrono::system_clock::now(); + + 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(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 diff --git a/src/silo_api/error_request_handler.test.cpp b/src/silo_api/error_request_handler.test.cpp index f65eeb61a..a8fd0ea37 100644 --- a/src/silo_api/error_request_handler.test.cpp +++ b/src/silo_api/error_request_handler.test.cpp @@ -15,10 +15,15 @@ class MockRequestHandler : public Poco::Net::HTTPRequestHandler { ); }; +const silo_api::StartupConfig TEST_STARTUP_CONFIG = { + std::chrono::system_clock::now(), + std::nullopt +}; + TEST(ErrorRequestHandler, handlesRuntimeErrors) { auto* wrapped_handler_mock = new MockRequestHandler; - auto under_test = silo_api::ErrorRequestHandler(wrapped_handler_mock); + auto under_test = silo_api::ErrorRequestHandler(wrapped_handler_mock, TEST_STARTUP_CONFIG); ON_CALL(*wrapped_handler_mock, handleRequest) .WillByDefault(testing::Throw(std::runtime_error("my error message"))); @@ -36,7 +41,7 @@ TEST(ErrorRequestHandler, handlesRuntimeErrors) { TEST(ErrorRequestHandler, handlesOtherErrors) { auto* wrapped_handler_mock = new MockRequestHandler; - auto under_test = silo_api::ErrorRequestHandler(wrapped_handler_mock); + auto under_test = silo_api::ErrorRequestHandler(wrapped_handler_mock, TEST_STARTUP_CONFIG); ON_CALL(*wrapped_handler_mock, handleRequest) .WillByDefault(testing::Throw( @@ -58,7 +63,7 @@ TEST(ErrorRequestHandler, doesNothingIfNoExceptionIsThrown) { const auto* wrapped_request_handler_message = "A message that the actual handler would write"; auto* wrapped_handler_mock = new MockRequestHandler; - auto under_test = silo_api::ErrorRequestHandler(wrapped_handler_mock); + auto under_test = silo_api::ErrorRequestHandler(wrapped_handler_mock, TEST_STARTUP_CONFIG); EXPECT_CALL(*wrapped_handler_mock, handleRequest).Times(testing::AtLeast(1)); diff --git a/src/silo_api/query_handler.cpp b/src/silo_api/query_handler.cpp index 8e97bb4f9..74c69e3a8 100644 --- a/src/silo_api/query_handler.cpp +++ b/src/silo_api/query_handler.cpp @@ -44,32 +44,6 @@ void QueryHandler::post( response.setStatus(Poco::Net::HTTPResponse::HTTP_BAD_REQUEST); std::ostream& out_stream = response.send(); out_stream << nlohmann::json(ErrorResponse{"Bad request", ex.what()}); - } catch (const std::exception& ex) { - SPDLOG_ERROR(ex.what()); - response.setStatus(Poco::Net::HTTPResponse::HTTP_INTERNAL_SERVER_ERROR); - std::ostream& out_stream = response.send(); - out_stream << nlohmann::json(ErrorResponse{"Internal Server Error", ex.what()}); - } catch (const std::string& ex) { - SPDLOG_ERROR(ex); - response.setStatus(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"); - const auto exception = std::current_exception(); - if (exception) { - const auto* message = abi::__cxa_current_exception_type()->name(); - SPDLOG_ERROR("current_exception: {}", message); - response.setStatus(Poco::Net::HTTPResponse::HTTP_INTERNAL_SERVER_ERROR); - std::ostream& out_stream = response.send(); - out_stream << nlohmann::json(ErrorResponse{"Internal Server Error", message}); - } else { - response.setStatus(Poco::Net::HTTPResponse::HTTP_INTERNAL_SERVER_ERROR); - std::ostream& out_stream = response.send(); - out_stream << nlohmann::json( - ErrorResponse{"Internal Server Error", "non recoverable error message"} - ); - } } } diff --git a/src/silo_api/request_handler_factory.cpp b/src/silo_api/request_handler_factory.cpp index 94903e528..cec16f8f4 100644 --- a/src/silo_api/request_handler_factory.cpp +++ b/src/silo_api/request_handler_factory.cpp @@ -14,14 +14,19 @@ namespace silo_api { -SiloRequestHandlerFactory::SiloRequestHandlerFactory(silo_api::DatabaseMutex& database) - : database(database) {} +SiloRequestHandlerFactory::SiloRequestHandlerFactory( + silo_api::DatabaseMutex& database, + StartupConfig startup_config +) + : database(database), + startup_config(startup_config) {} Poco::Net::HTTPRequestHandler* SiloRequestHandlerFactory::createRequestHandler( const Poco::Net::HTTPServerRequest& request ) { - return new silo_api::LoggingRequestHandler(new silo_api::ErrorRequestHandler(routeRequest(request - ))); + return new silo_api::LoggingRequestHandler( + new silo_api::ErrorRequestHandler(routeRequest(request), startup_config) + ); } Poco::Net::HTTPRequestHandler* SiloRequestHandlerFactory::routeRequest( diff --git a/src/silo_api/request_handler_factory.test.cpp b/src/silo_api/request_handler_factory.test.cpp index 002e726a4..6fa594995 100644 --- a/src/silo_api/request_handler_factory.test.cpp +++ b/src/silo_api/request_handler_factory.test.cpp @@ -42,16 +42,28 @@ class RequestHandlerTestFixture : public ::testing::Test { RequestHandlerTestFixture() : database_mutex(), request(silo_api::test::MockRequest(response)), - under_test(database_mutex) {} + under_test(database_mutex, {std::chrono::system_clock::now(), std::nullopt}) {} - void processRequest() { + void processRequest(silo_api::SiloRequestHandlerFactory& handler_factory) { std::unique_ptr request_handler( - under_test.createRequestHandler(request) + handler_factory.createRequestHandler(request) ); request_handler->handleRequest(request, response); } + + void processRequest() { processRequest(under_test); } }; +silo_api::StartupConfig getStartupConfigWithStarted5MinutesAgo( + std::optional estimated_startup_time = std::nullopt +) { + const std::chrono::time_point point = std::chrono::system_clock::now(); + const auto five_minutes_ago = point - std::chrono::minutes(5); + return {five_minutes_ago, estimated_startup_time}; +} + +static const int FOUR_MINUTES_IN_SECONDS = 240; + TEST_F(RequestHandlerTestFixture, handlesGetInfoRequest) { EXPECT_CALL(database_mutex.mock_database, getDatabaseInfo) .WillRepeatedly(testing::Return(silo::DatabaseInfo{1, 2, 3})); @@ -146,8 +158,6 @@ TEST_F(RequestHandlerTestFixture, returnsMethodNotAllowedOnGetQuery) { } TEST_F(RequestHandlerTestFixture, givenRequestToUnknownUrl_thenReturnsNotFound) { - auto under_test = silo_api::SiloRequestHandlerFactory(database_mutex); - request.setURI("/doesNotExist"); processRequest(); @@ -159,4 +169,88 @@ TEST_F(RequestHandlerTestFixture, givenRequestToUnknownUrl_thenReturnsNotFound) ); } +TEST_F( + RequestHandlerTestFixture, + givenDuringStartupTime_whenPostingQueryOnUninitializedDatabase_returnsServiceUnavailable +) { + request.setMethod("POST"); + request.setURI("/query"); + + silo_api::DatabaseMutex real_database_mutex; + + auto under_test = silo_api::SiloRequestHandlerFactory( + real_database_mutex, getStartupConfigWithStarted5MinutesAgo(std::chrono::minutes{10}) + ); + + processRequest(under_test); + + const auto retry_after = std::stoi(response.get("Retry-After")); + + EXPECT_EQ(response.getStatus(), Poco::Net::HTTPResponse::HTTP_SERVICE_UNAVAILABLE); + EXPECT_GT(retry_after, FOUR_MINUTES_IN_SECONDS); + EXPECT_THAT(response.out_stream.str(), testing::HasSubstr("Database not initialized yet")); +} + +TEST_F( + RequestHandlerTestFixture, + givenStartupTimeIsOver_whenPostingQueryOnUninitializedDatabase_returnsServiceUnavailable +) { + request.setMethod("POST"); + request.setURI("/query"); + + silo_api::DatabaseMutex real_database_mutex; + + auto under_test = silo_api::SiloRequestHandlerFactory( + real_database_mutex, getStartupConfigWithStarted5MinutesAgo(std::chrono::minutes{1}) + ); + + processRequest(under_test); + + EXPECT_EQ(response.getStatus(), Poco::Net::HTTPResponse::HTTP_SERVICE_UNAVAILABLE); + EXPECT_THROW(response.get("Retry-After"), Poco::NotFoundException); + EXPECT_THAT(response.out_stream.str(), testing::HasSubstr("Database not initialized yet")); +} + +TEST_F( + RequestHandlerTestFixture, + givenDuringStartupTime_whenGettingInfoOfUnititializedDatabase_returnsServiceUnavailable +) { + request.setMethod("GET"); + request.setURI("/info"); + + silo_api::DatabaseMutex real_database_mutex; + + auto under_test = silo_api::SiloRequestHandlerFactory( + real_database_mutex, getStartupConfigWithStarted5MinutesAgo(std::chrono::minutes{10}) + ); + + processRequest(under_test); + + const auto retry_after = std::stoi(response.get("Retry-After")); + + EXPECT_EQ(response.getStatus(), Poco::Net::HTTPResponse::HTTP_SERVICE_UNAVAILABLE); + EXPECT_GT(retry_after, FOUR_MINUTES_IN_SECONDS); + EXPECT_THAT(response.out_stream.str(), testing::HasSubstr("Database not initialized yet")); +} + +TEST_F(RequestHandlerTestFixture, postingQueryOnInitializedDatabase_isSuccessfull) { + request.setMethod("POST"); + request.setURI("/query"); + request.in_stream + << R"({"action":{"type": "Aggregated"}, "filterExpression": {"type": "True"}})"; + + silo_api::DatabaseMutex real_database_mutex; + silo::Database new_database; + real_database_mutex.setDatabase(std::move(new_database)); + + auto under_test = silo_api::SiloRequestHandlerFactory( + real_database_mutex, getStartupConfigWithStarted5MinutesAgo(std::chrono::minutes{10}) + ); + + processRequest(under_test); + + EXPECT_EQ(response.getStatus(), Poco::Net::HTTPResponse::HTTP_OK); + EXPECT_EQ(response.out_stream.str(), R"({"queryResult":[{"count":0}]})"); +} + // NOLINTEND(bugprone-unchecked-optional-access)