diff --git a/CMakeLists.txt b/CMakeLists.txt index 740f538..d928fa5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ # -# Copyright (C) 2018 Codership Oy +# Copyright (C) 2021 Codership Oy # cmake_minimum_required (VERSION 2.8) @@ -25,7 +25,15 @@ else() project(wsrep-lib) endif() -include(CheckIncludeFile) +if (POLICY CMP0057) + cmake_policy(SET CMP0057 NEW) +endif() + +if (POLICY CMP0077) + cmake_policy(SET CMP0077 NEW) +endif() + +include(CheckIncludeFileCXX) include(CheckLibraryExists) include(CheckCXXCompilerFlag) @@ -36,6 +44,7 @@ option(WSREP_LIB_WITH_UNIT_TESTS "Compile unit tests" ON) if (WSREP_LIB_WITH_UNIT_TESTS) # Run tests automatically by default if compiled option(WSREP_LIB_WITH_AUTO_TEST "Run unit tests automatically after build" OFF) + option(WSREP_LIB_WITH_UNIT_TESTS_EXTRA "Compile unit tests that may require additional software" OFF) endif() # Build a sample program @@ -107,14 +116,47 @@ else() set(WSREP_LIB_LIBDL "") endif() +set(MIN_BOOST_VERSION "1.54.0") if (WSREP_LIB_WITH_UNIT_TESTS) - find_package(Boost 1.54.0 REQUIRED + if (WSREP_LIB_WITH_UNIT_TESTS_EXTRA) + set(json_HEADER "boost/json/src.hpp") + # Extra tests may require packages from very recent boost which may be + # unavailable on the system. In such case download private boost distro. + check_include_file_cxx(${json_HEADER} system_json_FOUND) + if (NOT system_json_FOUND) + if (NOT WITH_BOOST) + set(WITH_BOOST "${CMAKE_SOURCE_DIR}/../boost") + endif() + set(DOWNLOAD_BOOST ON) + include (cmake/boost.cmake) + set(MIN_BOOST_VERSION "${BOOST_MAJOR}.${BOOST_MINOR}.${BOOST_PATCH}") + message(STATUS "Boost includes: ${BOOST_INCLUDE_DIR}, ver: ${MIN_BOOST_VERSION}") + find_package(Boost ${MIN_BOOST_VERSION} REQUIRED + COMPONENTS json headers + PATHS ${WITH_BOOST}/lib/cmake + NO_DEFAULT_PATH + ) + # Boost 1.76.0 comes very sloppy (and not only in JSON department) + # - need to disable some checks. + set(ADDITIONAL_CXX_FLAGS "-Wno-effc++ -Wno-conversion -Wno-suggest-override -Wno-overloaded-virtual") + set(ADDITIONAL_CXX_INCLUDES ${BOOST_INCLUDE_DIR}) + check_include_file_cxx(${json_HEADER} json_FOUND + "-I${ADDITIONAL_CXX_INCLUDES} ${ADDITIONAL_CXX_FLAGS}" + ) + if (NOT json_FOUND) + message(FATAL_ERROR "Required header 'boost/json.hpp' not found: ${json_FOUND}") + else() + include_directories(SYSTEM ${ADDITIONAL_CXX_INCLUDES}) + endif() + endif() + endif() + find_package(Boost ${MIN_BOOST_VERSION} REQUIRED unit_test_framework ) endif() if (WSREP_LIB_WITH_DBSIM) - find_package(Boost 1.54.0 REQUIRED + find_package(Boost ${MIN_BOOST_VERSION} REQUIRED program_options filesystem thread diff --git a/cmake/boost.cmake b/cmake/boost.cmake new file mode 100644 index 0000000..8c48fda --- /dev/null +++ b/cmake/boost.cmake @@ -0,0 +1,335 @@ +# Copyright (c) 2014, 2021, Oracle and/or its affiliates. +# Copyright (c) 2021, Codership OY. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2.0, +# as published by the Free Software Foundation. +# +# This program is also distributed with certain software (including +# but not limited to OpenSSL) that is licensed under separate terms, +# as designated in a particular file or component or in included license +# documentation. The authors of MySQL hereby grant you an additional +# permission to link the program and your derivative works with the +# separately licensed software that they have included with MySQL. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License, version 2.0, for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +# We need boost verision >= 1.75 to test JSON-related code. + +# Invoke with -DWITH_BOOST= or set WITH_BOOST in environment. +# If WITH_BOOST is *not* set, or is set to the special value "system", +# we assume that the correct version (see below) +# is installed on the compile host in the standard location. + +FUNCTION(GET_FILE_SIZE FILE_NAME OUTPUT_SIZE) + IF(WIN32) + FILE(TO_NATIVE_PATH "${CMAKE_SOURCE_DIR}/cmake/filesize.bat" FILESIZE_BAT) + FILE(TO_NATIVE_PATH "${FILE_NAME}" NATIVE_FILE_NAME) + + EXECUTE_PROCESS( + COMMAND "${FILESIZE_BAT}" "${NATIVE_FILE_NAME}" + RESULT_VARIABLE COMMAND_RESULT + OUTPUT_VARIABLE RESULT + OUTPUT_STRIP_TRAILING_WHITESPACE) + + ELSEIF(APPLE OR FREEBSD) + EXEC_PROGRAM(stat ARGS -f '%z' ${FILE_NAME} OUTPUT_VARIABLE RESULT) + ELSE() + EXEC_PROGRAM(stat ARGS -c '%s' ${FILE_NAME} OUTPUT_VARIABLE RESULT) + ENDIF() + SET(${OUTPUT_SIZE} ${RESULT} PARENT_SCOPE) +ENDFUNCTION() + +SET(BOOST_MAJOR "1") +SET(BOOST_MINOR "76") +SET(BOOST_PATCH "0") +SET(BOOST_PACKAGE_NAME "boost_${BOOST_MAJOR}_${BOOST_MINOR}_${BOOST_PATCH}") +SET(BOOST_TARBALL "${BOOST_PACKAGE_NAME}.tar.gz") +SET(BOOST_DOWNLOAD_URL + "https://boostorg.jfrog.io/artifactory/main/release/${BOOST_MAJOR}.${BOOST_MINOR}.${BOOST_PATCH}/source/${BOOST_TARBALL}" + ) + +MACRO(RESET_BOOST_VARIABLES) + UNSET(BOOST_INCLUDE_DIR) + UNSET(BOOST_INCLUDE_DIR CACHE) + UNSET(LOCAL_BOOST_DIR) + UNSET(LOCAL_BOOST_DIR CACHE) + UNSET(LOCAL_BOOST_ZIP) + UNSET(LOCAL_BOOST_ZIP CACHE) +ENDMACRO() + +MACRO(ECHO_BOOST_VARIABLES) + MESSAGE(STATUS "BOOST_INCLUDE_DIR ${BOOST_INCLUDE_DIR}") + MESSAGE(STATUS "LOCAL_BOOST_DIR ${LOCAL_BOOST_DIR}") + MESSAGE(STATUS "LOCAL_BOOST_ZIP ${LOCAL_BOOST_ZIP}") +ENDMACRO() + +MACRO(COULD_NOT_FIND_BOOST) + ECHO_BOOST_VARIABLES() + RESET_BOOST_VARIABLES() + MESSAGE(STATUS "Could not find (the correct version of) boost.") + MESSAGE(STATUS "wsrep-lib currently requires ${BOOST_PACKAGE_NAME}\n") + MESSAGE(FATAL_ERROR + "You can download it with -DDOWNLOAD_BOOST=1 -DWITH_BOOST=\n" + "This CMake script will look for boost in . " + "If it is not there, it will download and unpack it " + "(in that directory) for you.\n" + "You can also download boost manually, from ${BOOST_DOWNLOAD_URL}\n" + "If you are inside a firewall, you may need to use an https proxy:\n" + "export https_proxy=http://example.com:80\n" + ) +ENDMACRO() + +# Pick value from environment if not set on command line. +IF(DEFINED ENV{WITH_BOOST} AND NOT DEFINED WITH_BOOST) + SET(WITH_BOOST "$ENV{WITH_BOOST}") +ENDIF() + +# Pick value from environment if not set on command line. +IF(DEFINED ENV{BOOST_ROOT} AND NOT DEFINED WITH_BOOST) + SET(WITH_BOOST "$ENV{BOOST_ROOT}") +ENDIF() + +IF(WITH_BOOST AND WITH_BOOST STREQUAL "system") + UNSET(WITH_BOOST) + UNSET(WITH_BOOST CACHE) +ENDIF() + +# Update the cache, to make it visible in cmake-gui. +SET(WITH_BOOST ${WITH_BOOST} CACHE PATH + "Path to boost sources: a directory, or a tarball to be unzipped.") + +# If the value of WITH_BOOST changes, we must unset all dependent variables: +IF(OLD_WITH_BOOST) + IF(NOT "${OLD_WITH_BOOST}" STREQUAL "${WITH_BOOST}") + RESET_BOOST_VARIABLES() + ENDIF() +ENDIF() + +SET(OLD_WITH_BOOST ${WITH_BOOST} CACHE INTERNAL + "Previous version of WITH_BOOST" FORCE) + +IF (WITH_BOOST) + ## Did we get a full path name, including file name? + IF (${WITH_BOOST} MATCHES ".*\\.tar.gz" OR ${WITH_BOOST} MATCHES ".*\\.zip") + GET_FILENAME_COMPONENT(BOOST_DIR ${WITH_BOOST} PATH) + GET_FILENAME_COMPONENT(BOOST_ZIP ${WITH_BOOST} NAME) + FIND_FILE(LOCAL_BOOST_ZIP + NAMES ${BOOST_ZIP} + PATHS ${BOOST_DIR} + NO_DEFAULT_PATH + ) + ENDIF() + ## Did we get a path name to the directory of the .tar.gz or .zip file? + FIND_FILE(LOCAL_BOOST_ZIP + NAMES "${BOOST_PACKAGE_NAME}.tar.gz" "${BOOST_PACKAGE_NAME}.zip" + PATHS "${WITH_BOOST}" + NO_DEFAULT_PATH + ) + IF(LOCAL_BOOST_ZIP) + MESSAGE(STATUS "Local boost zip: ${LOCAL_BOOST_ZIP}") + GET_FILE_SIZE(${LOCAL_BOOST_ZIP} LOCAL_BOOST_ZIP_SIZE) + IF(LOCAL_BOOST_ZIP_SIZE EQUAL 0) + # A previous failed download has left an empty file, most likely the + # user pressed Ctrl-C to kill a hanging connection due to missing vpn + # proxy. Remove it! + MESSAGE("${LOCAL_BOOST_ZIP} is zero length. Deleting it.") + FILE(REMOVE ${WITH_BOOST}/${BOOST_TARBALL}) + UNSET(LOCAL_BOOST_ZIP) + UNSET(LOCAL_BOOST_ZIP CACHE) + ENDIF() + UNSET(LOCAL_BOOST_ZIP_ZERO_LENGTH) + ENDIF() + + ## Did we get a path name to the directory of an unzipped version? + FIND_FILE(LOCAL_BOOST_SOURCE + NAMES "${BOOST_PACKAGE_NAME}" + PATHS "${WITH_BOOST}" + NO_DEFAULT_PATH + ) + ## Did we get a path name to an unzippped version? + FIND_PATH(LOCAL_BOOST_VER + NAMES "boost/version.hpp" + PATHS "${WITH_BOOST}" + NO_DEFAULT_PATH + ) + IF(LOCAL_BOOST_VER AND NOT LOCAL_BOOST_SOURCE) + SET(LOCAL_BOOST_SOURCE ${WITH_BOOST}) + UNSET(LOCAL_BOOST_VER) + UNSET(LOCAL_BOOST_VER CACHE) + ENDIF() + IF(LOCAL_BOOST_SOURCE) + MESSAGE(STATUS "Local boost source: ${LOCAL_BOOST_SOURCE}") + ENDIF() + + ## Did we get a path to compiled boost install? + FIND_PATH(LOCAL_BOOST_LIB + NAMES "lib" + PATHS "${WITH_BOOST}" + NO_DEFAULT_PATH + ) + IF(LOCAL_BOOST_LIB) + SET(LOCAL_BOOST_BUILD ${WITH_BOOST}) + UNSET(LOCAL_BOOST_LIB) + UNSET(LOCAL_BOOST_LIB CACHE) + ENDIF() +ENDIF() + +# There is a similar option in unittest/gunit. +# But the boost tarball is much bigger, so we have a separate option. +OPTION(DOWNLOAD_BOOST "Download boost from sourceforge." OFF) +SET(DOWNLOAD_BOOST_TIMEOUT 600 CACHE STRING + "Timeout in seconds when downloading boost.") + +# If we could not find it, then maybe download it. +IF(WITH_BOOST AND NOT LOCAL_BOOST_ZIP AND NOT LOCAL_BOOST_SOURCE AND NOT LOCAL_BOOST_BUILD) + IF(NOT DOWNLOAD_BOOST) + MESSAGE(STATUS "WITH_BOOST=${WITH_BOOST}") + COULD_NOT_FIND_BOOST() + ENDIF() + # Download the tarball + MESSAGE(STATUS "Downloading ${BOOST_TARBALL} to ${WITH_BOOST}") + FILE(DOWNLOAD ${BOOST_DOWNLOAD_URL} + ${WITH_BOOST}/${BOOST_TARBALL} + TIMEOUT ${DOWNLOAD_BOOST_TIMEOUT} + STATUS ERR + SHOW_PROGRESS + ) + IF(ERR EQUAL 0) + SET(LOCAL_BOOST_ZIP "${WITH_BOOST}/${BOOST_TARBALL}") + SET(LOCAL_BOOST_ZIP "${WITH_BOOST}/${BOOST_TARBALL}" CACHE INTERNAL "") + ELSE() + MESSAGE(STATUS "Download failed, error: ${ERR}") + # A failed DOWNLOAD leaves an empty file, remove it + FILE(REMOVE ${WITH_BOOST}/${BOOST_TARBALL}) + # STATUS returns a list of length 2 + LIST(GET ERR 0 NUMERIC_RETURN) + IF(NUMERIC_RETURN EQUAL 28) + MESSAGE(FATAL_ERROR + "You can try downloading ${BOOST_DOWNLOAD_URL} manually" + " using curl/wget or a similar tool," + " or increase the value of DOWNLOAD_BOOST_TIMEOUT" + " (which is now ${DOWNLOAD_BOOST_TIMEOUT} seconds)" + ) + ENDIF() + MESSAGE(FATAL_ERROR + "You can try downloading ${BOOST_DOWNLOAD_URL} manually" + " using curl/wget or a similar tool" + ) + ENDIF() +ENDIF() + +IF(LOCAL_BOOST_ZIP AND NOT LOCAL_BOOST_SOURCE AND NOT LOCAL_BOOST_BUILD) + GET_FILENAME_COMPONENT(LOCAL_BOOST_DIR ${LOCAL_BOOST_ZIP} PATH) + IF(NOT EXISTS "${LOCAL_BOOST_DIR}/${BOOST_PACKAGE_NAME}") + GET_FILENAME_COMPONENT(BOOST_TARBALL ${LOCAL_BOOST_ZIP} NAME) + MESSAGE(STATUS "cd ${LOCAL_BOOST_DIR}; tar xfz ${BOOST_TARBALL}") + EXECUTE_PROCESS( + COMMAND ${CMAKE_COMMAND} -E tar xfz "${BOOST_TARBALL}" + WORKING_DIRECTORY "${LOCAL_BOOST_DIR}" + RESULT_VARIABLE tar_result + ) + IF (tar_result MATCHES 0) + SET(BOOST_FOUND 1 CACHE INTERNAL "") + SET(LOCAL_BOOST_SOURCE "${LOCAL_BOOST_DIR}/${BOOST_PACKAGE_NAME}") + ELSE() + MESSAGE(STATUS "WITH_BOOST ${WITH_BOOST}.") + MESSAGE(STATUS "Failed to extract files.\n" + " Please try downloading and extracting yourself.\n" + " The url is: ${BOOST_DOWNLOAD_URL}") + MESSAGE(FATAL_ERROR "Giving up.") + ENDIF() + ENDIF() +ENDIF() + +IF (LOCAL_BOOST_SOURCE AND NOT LOCAL_BOOST_BUILD) + GET_FILENAME_COMPONENT(LOCAL_BOOST_BUILD ${LOCAL_BOOST_SOURCE} PATH) + SET(BOOST_TO_BUILD "thread,filesystem,program_options,test,json") + MESSAGE(STATUS "Executing Boost configure") + EXECUTE_PROCESS( + COMMAND ./bootstrap.sh --prefix=${LOCAL_BOOST_BUILD} --with-libraries=${BOOST_TO_BUILD} + WORKING_DIRECTORY "${LOCAL_BOOST_SOURCE}" + RESULT_VARIABLE RES + ERROR_VARIABLE ERR + OUTPUT_FILE "bootstrap.log" + ERROR_FILE "bootstrap.err" + COMMAND_ECHO STDOUT + ) + IF (NOT RES MATCHES 0) + MESSAGE(FATAL_ERROR "Boost configure failed: ${ERR}. Logs at ${LOCAL_BOOST_SOURCE}/bootstrap.log, ${LOCAL_BOOST_SOURCE}/bootstrap.err") + ENDIF() + MESSAGE(STATUS "Executing Boost build") + EXECUTE_PROCESS( + COMMAND ./b2 install -q + WORKING_DIRECTORY "${LOCAL_BOOST_SOURCE}" + RESULT_VARIABLE RES + ERROR_VARIABLE ERR + OUTPUT_FILE "b2.log" + ERROR_FILE "b2.err" + COMMAND_ECHO STDOUT + ) + IF (NOT RES MATCHES 0) + MESSAGE(FATAL_ERROR "Boost build failed: ${ERR}. Logs at ${LOCAL_BOOST_SOURCE}/b2.log, ${LOCAL_BOOST_SOURCE}/b2.err") + ENDIF() + # remove the source directory so save space + FILE(REMOVE_RECURSE ${LOCAL_BOOST_SOURCE}) +ENDIF() + +# Search for the version file, first in LOCAL_BOOST_DIR or WITH_BOOST +MESSAGE(STATUS + "Looking for boost/version.hpp in ${LOCAL_BOOST_BUILD}/include") +FIND_PATH(BOOST_INCLUDE_DIR + NAMES "boost/version.hpp" + NO_DEFAULT_PATH + PATHS ${LOCAL_BOOST_BUILD}/include +) +# Then search in standard places (if not found above). +FIND_PATH(BOOST_INCLUDE_DIR + NAMES "boost/version.hpp" +) + +IF(NOT BOOST_INCLUDE_DIR) + MESSAGE(STATUS + "Looked for boost/version.hpp in ${LOCAL_BOOST_BUILD}/include and default locations") + COULD_NOT_FIND_BOOST() +ELSE() + MESSAGE(STATUS "Found ${BOOST_INCLUDE_DIR}/boost/version.hpp ") +ENDIF() + +# Verify version number. Version information looks like: +# // BOOST_VERSION % 100 is the patch level +# // BOOST_VERSION / 100 % 1000 is the minor version +# // BOOST_VERSION / 100000 is the major version +# #define BOOST_VERSION 107300 +FILE(STRINGS "${BOOST_INCLUDE_DIR}/boost/version.hpp" + BOOST_VERSION_NUMBER + REGEX "^#define[\t ]+BOOST_VERSION[\t ][0-9]+.*" +) +STRING(REGEX REPLACE + "^.*BOOST_VERSION[\t ]([0-9][0-9])([0-9][0-9])([0-9][0-9]).*$" "\\1" + BOOST_MAJOR_VERSION "${BOOST_VERSION_NUMBER}") +STRING(REGEX REPLACE + "^.*BOOST_VERSION[\t ]([0-9][0-9])([0-9][0-9])([0-9][0-9]).*$" "\\2" + BOOST_MINOR_VERSION "${BOOST_VERSION_NUMBER}") + +MESSAGE(STATUS "BOOST_VERSION_NUMBER is ${BOOST_VERSION_NUMBER}") + +MESSAGE(STATUS "BOOST_INCLUDE_DIR ${BOOST_INCLUDE_DIR}") + +# Boost gets confused about language support with Clang 7 + MSVC 15.9 +IF(WIN32_CLANG) + ADD_DEFINITIONS(-DBOOST_NO_CXX17_HDR_STRING_VIEW) +ENDIF() + +IF(LOCAL_BOOST_BUILD OR LOCAL_BOOST_ZIP) + SET(USING_LOCAL_BOOST 1) +ELSE() + SET(USING_SYSTEM_BOOST 1) +ENDIF() diff --git a/dbsim/db_params.cpp b/dbsim/db_params.cpp index 4fbefd0..40433f6 100644 --- a/dbsim/db_params.cpp +++ b/dbsim/db_params.cpp @@ -57,6 +57,9 @@ db::params db::parse_args(int argc, char** argv) ("wsrep-provider-options", po::value(¶ms.wsrep_provider_options), "wsrep provider options") + ("status-file", + po::value(¶ms.status_file), + "status output file") ("servers", po::value(¶ms.n_servers)->required(), "number of servers to start") ("topology", po::value(¶ms.topology), diff --git a/dbsim/db_params.hpp b/dbsim/db_params.hpp index 6443e13..e5df806 100644 --- a/dbsim/db_params.hpp +++ b/dbsim/db_params.hpp @@ -38,6 +38,7 @@ namespace db std::string topology; std::string wsrep_provider; std::string wsrep_provider_options; + std::string status_file; int debug_log_level; int fast_exit; int thread_instrumentation; @@ -55,6 +56,7 @@ namespace db , topology() , wsrep_provider() , wsrep_provider_options() + , status_file("status.json") , debug_log_level(0) , fast_exit(0) , thread_instrumentation() diff --git a/dbsim/db_server.cpp b/dbsim/db_server.cpp index 51ed2b4..3efef49 100644 --- a/dbsim/db_server.cpp +++ b/dbsim/db_server.cpp @@ -24,10 +24,52 @@ #include "db_simulator.hpp" #include "wsrep/logger.hpp" +#include "wsrep/reporter.hpp" + +#include +#include + +static wsrep::default_mutex logger_mtx; +static wsrep::reporter* reporter = nullptr; + +static void +logger_fn(wsrep::log::level l, const char* pfx, const char* msg) +{ + wsrep::unique_lock lock(logger_mtx); + + struct timespec time; + clock_gettime(CLOCK_REALTIME, &time); + + time_t const tt(time.tv_sec); + struct tm date; + localtime_r(&tt, &date); + + char date_str[85] = { '\0', }; + snprintf(date_str, sizeof(date_str) - 1, + "%04d-%02d-%02d %02d:%02d:%02d.%03d", + date.tm_year + 1900, date.tm_mon + 1, date.tm_mday, + date.tm_hour, date.tm_min, date.tm_sec, (int)time.tv_nsec/1000000); + +#define LOG_STR date_str << pfx << wsrep::log::to_c_string(l) << msg + if (l >= wsrep::log::error && reporter) + { + std::ostringstream os; + os << LOG_STR; + std::cerr << os.str() << std::endl; + auto const tstamp(double(time.tv_sec) + double(time.tv_nsec)*1.0e-9); + reporter->report_log_msg(wsrep::reporter::error, os.str(), tstamp); + } + else + { + std::cerr << LOG_STR << std::endl; + } +#undef LOG_STR +} db::server::server(simulator& simulator, const std::string& name, - const std::string& address) + const std::string& address, + const std::string& status_file) : simulator_(simulator) , storage_engine_(simulator_.params()) , mutex_() @@ -40,7 +82,15 @@ db::server::server(simulator& simulator, , appliers_() , clients_() , client_threads_() -{ } +{ + wsrep::log::logger_fn(logger_fn); + reporter = new wsrep::reporter(mutex_, status_file, 3); +} + +db::server::~server() +{ + delete reporter; +} void db::server::applier_thread() { @@ -129,3 +179,9 @@ wsrep::high_priority_service* db::server::streaming_applier_service() throw wsrep::not_implemented_error(); } +void db::server::log_state_change(enum wsrep::server_state::state from, + enum wsrep::server_state::state to) +{ + wsrep::log_info() << "State changed " << from << " -> " << to; + if (reporter) reporter->report_state(to, 0); +} diff --git a/dbsim/db_server.hpp b/dbsim/db_server.hpp index b64947c..63772b8 100644 --- a/dbsim/db_server.hpp +++ b/dbsim/db_server.hpp @@ -41,7 +41,9 @@ namespace db public: server(simulator& simulator, const std::string& name, - const std::string& address); + const std::string& address, + const std::string& status_file); + ~server(); void applier_thread(); void start_applier(); void stop_applier(); @@ -58,6 +60,8 @@ namespace db wsrep::client_state* local_client_state(); void release_client_state(wsrep::client_state*); wsrep::high_priority_service* streaming_applier_service(); + void log_state_change(enum wsrep::server_state::state, + enum wsrep::server_state::state); private: void start_client(size_t id); diff --git a/dbsim/db_server_service.cpp b/dbsim/db_server_service.cpp index 4fbc666..8fc58dd 100644 --- a/dbsim/db_server_service.cpp +++ b/dbsim/db_server_service.cpp @@ -154,10 +154,9 @@ void db::server_service::log_state_change( enum wsrep::server_state::state prev_state, enum wsrep::server_state::state current_state) { - - wsrep::log_info() << "State changed " - << prev_state << " -> " << current_state; + server_.log_state_change(prev_state, current_state); } + int db::server_service::wait_committing_transactions(int) { throw wsrep::not_implemented_error(); diff --git a/dbsim/db_simulator.cpp b/dbsim/db_simulator.cpp index 0971b11..6bede3d 100644 --- a/dbsim/db_simulator.cpp +++ b/dbsim/db_simulator.cpp @@ -137,7 +137,8 @@ void db::simulator::start() std::make_unique( *this, name_os.str(), - address_os.str())))); + address_os.str(), + name_os.str() + "_" + params_.status_file)))); if (it.second == false) { throw wsrep::runtime_error("Failed to add server"); diff --git a/include/wsrep/reporter.hpp b/include/wsrep/reporter.hpp new file mode 100644 index 0000000..b00adde --- /dev/null +++ b/include/wsrep/reporter.hpp @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2021 Codership Oy + * + * This file is part of wsrep-lib. + * + * Wsrep-lib is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * Wsrep-lib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with wsrep-lib. If not, see . + */ + +/** @file info.hpp + * + * Interface to report application status to external programs + * via JSON file. + */ + +#ifndef WSREP_REPORTER_HPP +#define WSREP_REPORTER_HPP + +#include "mutex.hpp" +#include "server_state.hpp" + +#include +#include + +namespace wsrep +{ + class reporter + { + public: + reporter(mutex& mutex, + const std::string& file_name, + size_t max_msg); + + virtual ~reporter(); + + // indefinite progress value + static float constexpr indefinite = -1.0f; + + void report_state(enum server_state::state state, + float progress = indefinite); + + enum log_level + { + error, + warning + }; + + // undefined timestamp value + static double constexpr undefined = 0.0; + + void report_log_msg(log_level, const std::string& msg, + double timestamp = undefined); + + private: + enum substates { + s_disconnected_disconnected, + s_disconnected_initializing, + s_disconnected_initialized, + s_connected_waiting, // to become joiner + s_joining_initialized, + s_joining_sst, + s_joining_initializing, + s_joining_ist, + s_joined_syncing, + s_synced_running, + s_donor_sending, + s_disconnecting_disconnecting, + substates_max + }; + + wsrep::mutex& mutex_; + std::string const file_name_; + char* template_; + substates state_; + float progress_; + bool initialized_; + + typedef struct { + double tstamp; + std::string msg; + } log_msg; + + std::deque err_msg_; + std::deque warn_msg_; + size_t const max_msg_; + + static void write_log_msgs(std::ostream& os, + const std::string& label, + const std::deque& msgs); + static void write_log_msg(std::ostream& os, + const log_msg& msg); + + substates substate_map(enum server_state::state state); + float progress_map(float progress) const; + void write_file(double timestamp); + + // make uncopyable + reporter(const wsrep::reporter&); + void operator=(const wsrep::reporter&); + }; /* reporter */ + +} /* wsrep */ + +#endif /* WSREP_REPORTER_HPP */ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0401494..3a5d287 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -19,5 +19,6 @@ add_library(wsrep-lib tls_service_v1.cpp transaction.cpp uuid.cpp + reporter.cpp wsrep_provider_v26.cpp) target_link_libraries(wsrep-lib wsrep_api_v26 pthread ${WSREP_LIB_LIBDL}) diff --git a/src/reporter.cpp b/src/reporter.cpp new file mode 100644 index 0000000..b6fed8b --- /dev/null +++ b/src/reporter.cpp @@ -0,0 +1,321 @@ +/* + * Copyright (C) 2021 Codership Oy + * + * This file is part of wsrep-lib. + * + * Wsrep-lib is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * Wsrep-lib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with wsrep-lib. If not, see . + */ + +#include "wsrep/reporter.hpp" +#include "wsrep/logger.hpp" + +#include +#include + +#include // strerror() +#include // mkstemp() +#include // errno +#include // write() +#include // rename(), snprintf() +#include // clock_gettime() +#include // floor() + +static std::string const TEMP_EXTENSION(".XXXXXX"); + +static inline double +timestamp() +{ + struct timespec time; + clock_gettime(CLOCK_REALTIME, &time); + return (double(time.tv_sec) + double(time.tv_nsec)*1.0e-9); +} + +wsrep::reporter::reporter(wsrep::mutex& mutex, + const std::string& file_name, + size_t const max_msg) + : mutex_(mutex) + , file_name_(file_name) + , template_(new char [file_name_.length() + TEMP_EXTENSION.length() + 1]) + , state_(wsrep::reporter::s_disconnected_disconnected) + , progress_(indefinite) + , initialized_(false) + , err_msg_() + , warn_msg_() + , max_msg_(max_msg) +{ + template_[file_name_.length() + TEMP_EXTENSION.length()] = '\0'; + write_file(timestamp()); +} + +wsrep::reporter::~reporter() +{ + delete [] template_; +} + +wsrep::reporter::substates +wsrep::reporter::substate_map(enum wsrep::server_state::state const state) +{ + switch (state) + { + case wsrep::server_state::s_disconnected: + initialized_ = false; + return s_disconnected_disconnected; + case wsrep::server_state::s_initializing: + if (s_disconnected_disconnected == state_) + return s_disconnected_initializing; + else if (s_joining_sst == state_) + return s_joining_initializing; + else + { + assert(0); + return state_; + } + case wsrep::server_state::s_initialized: + initialized_ = true; + if (s_disconnected_initializing >= state_) + return s_disconnected_initialized; + else if (s_joining_initializing == state_) + return s_joining_ist; + else + { + assert(0); + return state_; + } + case wsrep::server_state::s_connected: + return s_connected_waiting; + case wsrep::server_state::s_joiner: + if (initialized_) + return s_joining_initialized; + else + return s_joining_sst; + case wsrep::server_state::s_joined: + return s_joined_syncing; + case wsrep::server_state::s_donor: + return s_donor_sending; + case wsrep::server_state::s_synced: + return s_synced_running; + case wsrep::server_state::s_disconnecting: + return s_disconnecting_disconnecting; + default: + assert(0); + return state_; + } +} + +static float const SST_SHARE = 0.5f; // SST share of JOINING progress +static float const INIT_SHARE = 0.1f; // initialization share of JOINING progress +static float const IST_SHARE = (1.0f - SST_SHARE - INIT_SHARE); // IST share + +float +wsrep::reporter::progress_map(float const progress) const +{ + assert(progress >= 0.0f); + assert(progress <= 1.0f); + + switch (state_) + { + case s_disconnected_disconnected: + return indefinite; + case s_disconnected_initializing: + return indefinite; + case s_disconnected_initialized: + return indefinite; + case s_connected_waiting: + return indefinite; + case s_joining_initialized: + return progress; + case s_joining_sst: + return progress * SST_SHARE; + case s_joining_initializing: + return SST_SHARE + progress * INIT_SHARE; + case s_joining_ist: + return SST_SHARE + INIT_SHARE + progress * IST_SHARE; + case s_joined_syncing: + return progress; + case s_synced_running: + return 1.0; + case s_donor_sending: + return progress; + case s_disconnecting_disconnecting: + return indefinite; + default: + assert(0); + return progress; + } +} + +void +wsrep::reporter::write_log_msg(std::ostream& os, + const log_msg& msg) +{ + os << "\t\t{\n"; + os << "\t\t\t\"timestamp\": " << std::showpoint << std::setprecision(18) + << msg.tstamp << ",\n"; + os << "\t\t\t\"msg\": \"" << msg.msg << "\"\n"; + os << "\t\t}"; +} + +void +wsrep::reporter::write_log_msgs(std::ostream& os, + const std::string& label, + const std::deque& msgs) +{ + os << "\t\"" << label << "\": [\n"; + for (size_t i(0); i < msgs.size(); ++i) + { + write_log_msg(os, msgs[i]); + os << (i+1 < msgs.size() ? ",\n" : "\n"); + } + os << "\t],\n"; +} + +// write data to temporary file and then rename it to target file for atomicity +void +wsrep::reporter::write_file(double const tstamp) +{ + enum progress_type { + t_indefinite = -1, // indefinite wait + t_progressive, // measurable progress + t_final // final state + }; + + struct strings { + const char* state; + const char* comment; + progress_type type; + }; + + static const struct strings strings[substates_max] = + { + { "DISCONNECTED", "Disconnected", t_indefinite }, + { "DISCONNECTED", "Initializing", t_indefinite }, + { "DISCONNECTED", "Connecting", t_indefinite }, + { "CONNECTED", "Waiting", t_indefinite }, + { "JOINING", "Receiving state", t_progressive }, + { "JOINING", "Receiving SST", t_progressive }, + { "JOINING", "Initializing", t_progressive }, + { "JOINING", "Receiving IST", t_progressive }, + { "JOINED", "Syncing", t_progressive }, + { "SYNCED", "Operational", t_final }, + { "DONOR", "Donating SST", t_progressive }, + { "DISCONNECTING", "Disconnecting", t_indefinite } + }; + + // prepare template for mkstemp() + file_name_.copy(template_, file_name_.length()); + TEMP_EXTENSION.copy(template_ +file_name_.length(),TEMP_EXTENSION.length()); + + int const fd(mkstemp(template_)); + if (fd < 0) + { + std::cerr << "Reporter could not open temporary file `" << template_ + << "': " << strerror(errno) << " (" << errno << ")"; + return; + } + + double const seconds(floor(tstamp)); + time_t const tt = time_t(seconds); + struct tm date; + localtime_r(&tt, &date); + + char date_str[85] = { '\0', }; + snprintf(date_str, sizeof(date_str) - 1, + "%04d-%02d-%02d %02d:%02d:%02d.%03d", + date.tm_year + 1900, date.tm_mon + 1, date.tm_mday, + date.tm_hour, date.tm_min, date.tm_sec, + (int)((tstamp-seconds)*1000)); + + std::ostringstream os; + os << "{\n"; + os << "\t\"date\": \"" << date_str << "\",\n"; + os << "\t\"timestamp\": " << std::showpoint << std::setprecision(18) + << tstamp << ",\n"; + write_log_msgs(os, "errors", err_msg_); + write_log_msgs(os, "warnings", warn_msg_); + os << "\t\"status\": {\n"; + os << "\t\t\"state\": \"" << strings[state_].state << "\",\n"; + os << "\t\t\"comment\": \"" << strings[state_].comment << "\",\n"; + os << "\t\t\"progress\": " << std::showpoint << std::setprecision(6) + << progress_ << "\n"; + os << "\t}\n"; + os << "}\n"; + + std::string str(os.str()); + ssize_t err(write(fd, str.c_str(), str.length())); + if (err < 0) + { + std::cerr << "Could not write " << str.length() + << " bytes to temporary file '" + << template_ << "': " << strerror(errno) + << " (" << errno << ")"; + return; + } + + rename(template_, file_name_.c_str()); +} + +void +wsrep::reporter::report_state(enum server_state::state const s, float const p) +{ + assert(p >= -1); + assert(p <= 1); + + bool flush(false); + + wsrep::unique_lock lock(mutex_); + + substates const state(substate_map(s)); + + if (state != state_) + { + state_ = state; + flush = true; + } + + float const progress(progress_map(p)); + assert(progress >= -1); + assert(progress <= 1); + + if (progress != progress_) + { + progress_ = progress; + flush = true; + } + + if (flush) + { + write_file(timestamp()); + } +} + +void +wsrep::reporter::report_log_msg(log_level const lvl, + const std::string& msg, + double tstamp) +{ + std::deque& deque(lvl == error ? err_msg_ : warn_msg_); + + wsrep::unique_lock lock(mutex_); + + if (deque.empty() || msg != deque.back().msg) + { + if (deque.size() == max_msg_) deque.pop_front(); + + if (tstamp <= undefined) tstamp = timestamp(); + + log_msg entry({tstamp, msg}); + deque.push_back(entry); + write_file(tstamp); + } +} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index af8b6f7..366cc47 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -2,7 +2,8 @@ # Copyright (C) 2018 Codership Oy # -add_executable(wsrep-lib_test + +set(TEST_SOURCES mock_client_state.cpp mock_high_priority_service.cpp mock_storage_service.cpp @@ -22,6 +23,14 @@ add_executable(wsrep-lib_test wsrep-lib_test.cpp ) +if (WSREP_LIB_WITH_UNIT_TESTS_EXTRA) + set(TEST_SOURCES ${TEST_SOURCES} + reporter_test.cpp + ) +endif() + +add_executable(wsrep-lib_test ${TEST_SOURCES}) + target_link_libraries(wsrep-lib_test wsrep-lib) add_test(NAME wsrep-lib_test diff --git a/test/reporter_test.cpp b/test/reporter_test.cpp new file mode 100644 index 0000000..b3efe15 --- /dev/null +++ b/test/reporter_test.cpp @@ -0,0 +1,543 @@ +/* + * Copyright (C) 2021 Codership Oy + * + * This file is part of wsrep-lib. + * + * Wsrep-lib is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * Wsrep-lib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with wsrep-lib. If not, see . + */ + +#include "wsrep/reporter.hpp" + +#include + +#include + +#include +#include +#include +#include // unlink() for cleanup + +namespace json = boost::json; + +//////// HELPERS /////// + +static inline double +timestamp() +{ + struct timespec time; + clock_gettime(CLOCK_REALTIME, &time); + return (double(time.tv_sec) + double(time.tv_nsec)*1.0e-9); +} + +static json::value +read_file(const char* const filename) +{ + std::ifstream input(filename, std::ios::binary); + std::vector buffer(std::istreambuf_iterator(input), {}); + json::stream_parser parser; + json::error_code err; + + parser.write(buffer.data(), buffer.size(), err); + if (err) + { + assert(0); + return nullptr; + } + + parser.finish(err); + if (err) + { + assert(0); + return nullptr; + } + + return parser.release(); +} + +struct logs +{ + std::deque tstamp_; + std::deque msg_; +}; + +struct result +{ + logs errors_; + logs warnings_; + struct + { + std::string state_; + std::string comment_; + float progress_; + } status_; +}; + +static void +parse_result(const json::value& value, struct result& res, + const std::string& path = "") +{ + //std::cout << "Parsing " << path << ": " << value << ": " << value.kind() << std::endl; + switch (value.kind()) + { + case json::kind::object: + { + auto const obj(value.get_object()); + if (!obj.empty()) + { + for (auto it = obj.begin(); it != obj.end(); ++it) + { + std::string const key(it->key().data(), it->key().length()); + parse_result(it->value(), res, path + "." + key); + } + } + return; + } + case json::kind::array: + { + auto const arr(value.get_array()); + if (!arr.empty()) + { + for (auto it = arr.begin(); it != arr.end(); ++it) + { + parse_result(*it, res, path + ".[]"); + } + } + return; + } + case json::kind::string: + { + auto const val(value.get_string().c_str()); + if (path == ".errors.[].msg") + { + res.errors_.msg_.push_back(val); + } + else if (path == ".warnings.[].msg") + { + res.warnings_.msg_.push_back(val); + } + else if (path == ".status.state") + { + res.status_.state_ = val; + } + else if (path == ".status.comment") + { + res.status_.comment_ = val; + } + return; + } + case json::kind::uint64: + return; + case json::kind::int64: + return; + case json::kind::double_: + if (path == ".status.progress") + { + res.status_.progress_ = float(value.get_double()); + } + else if (path == ".errors.[].timestamp") + { + res.errors_.tstamp_.push_back(value.get_double()); + } + else if (path == ".warnings.[].timestamp") + { + res.warnings_.tstamp_.push_back(value.get_double()); + } + + return; + case json::kind::bool_: + return; + case json::kind::null: + return; + } + + assert(0); +} + +static bool +equal(const std::string& left, const std::string& right) +{ + return left == right; +} + +static bool +equal(double const left, double const right) +{ + return ::fabs(left - right) < 0.0001; // we are looking for ms precision +} + +template +static bool +operator!=(const std::deque& left, const std::deque& right) +{ + if (left.size() != right.size()) return true; + + for (size_t i(0); i < left.size(); ++i) + if (!equal(left[i], right[i])) return true; + + return false; +} + +static bool +operator!=(const logs& left, const logs& right) +{ + if (left.tstamp_ != right.tstamp_) return true; + return (left.msg_ != right.msg_); +} + +static bool +operator==(const result& left, const result& right) +{ + if (left.errors_ != right.errors_) return false; + if (left.warnings_ != right.warnings_) return false; + + if (left.status_.state_ != right.status_.state_) return false; + if (left.status_.comment_ != right.status_.comment_) return false; + if (left.status_.progress_ != right.status_.progress_) return false; + + return true; +} + +template +static void +print_deque(std::ostream& os,const std::deque left,const std::deque right) +{ + auto const max(std::max(left.size(), right.size())); + for (size_t i(0); i < max; ++i) + { + os << "|\t'"; + + if (i < left.size()) + os << left[i] << "'"; + else + os << "'\t"; + + if (i < right.size()) + os << "\t'" << right[i] << "'"; + else + os << "\t''"; + + if (!equal(left[i], right[i])) os << "\t!!!"; + + os << "\n"; + } +} + +static void +print_logs(std::ostream& os, const logs& left, const logs& right) +{ + os << "|\t" << left.msg_.size() << "\t" << right.msg_.size() << "\n"; + print_deque(os, left.tstamp_, right.tstamp_); + print_deque(os, left.msg_, right.msg_); +} + +// print two results against each other +static std::string +print(const result& left, const result& right, size_t it) +{ + std::ostringstream os; + + os << std::showpoint << std::setprecision(18); + + os << "Iteration " << it << "\nerrors:\n"; + print_logs(os, left.errors_, right.errors_); + os << "warnings:\n"; + print_logs(os, left.warnings_, right.warnings_); + os << "state:\n"; + os << "\t" << left.status_.state_ << "\t" << right.status_.state_ + << "\n"; + os << "\t" << left.status_.comment_ << "\t" << right.status_.comment_ + << "\n"; + os << "\t" << left.status_.progress_ << "\t" << right.status_.progress_ + << "\n"; + + return os.str(); +} + +#define VERIFY_RESULT(left, right, it) \ + BOOST_CHECK_MESSAGE(left == right, print(left, right, it)); + +static const char* +const REPORT = "report.json"; + +static auto +const indefinite(wsrep::reporter::indefinite); + +static struct logs +const LOGS_INIT = { std::deque(), std::deque() }; +static struct result +const RES_INIT = { LOGS_INIT, LOGS_INIT, + { "DISCONNECTED", "Disconnected", indefinite } }; + +static void +test_log(const char* const fname, + wsrep::reporter& rep, + result& check, + wsrep::reporter::log_level const lvl, + double const tstamp, + const std::string& msg, + size_t const iteration) +{ + // this is implementaiton detail, if it changes in the code, it needs + // to be changed here + size_t const MAX_ERROR(4); + + logs& log(lvl == wsrep::reporter::error ? check.errors_ : check.warnings_); + log.tstamp_.push_back(tstamp); + if (log.tstamp_.size() > MAX_ERROR) log.tstamp_.pop_front(); + log.msg_.push_back(msg); + if (log.msg_.size() > MAX_ERROR) log.msg_.pop_front(); + + rep.report_log_msg(lvl, msg, tstamp); + + auto value = read_file(fname); + auto res = RES_INIT; + parse_result(value, res); + VERIFY_RESULT(res, check, iteration); +} + +static size_t const MAX_MSG = 4; + +BOOST_AUTO_TEST_CASE(log_msg_test) +{ + wsrep::default_mutex m; + wsrep::reporter rep(m, REPORT, MAX_MSG); + + auto value = read_file(REPORT); + BOOST_REQUIRE(value != nullptr); + + struct result res(RES_INIT), check(RES_INIT); + parse_result(value, res); + VERIFY_RESULT(res, check, -1); + + struct entry + { + double tstamp_; + std::string msg_; + }; + std::vector msgs = + { + { 0.1, "a" }, + { 0.2, "bb" }, + { 0.3, "ccc" }, + { 0.4, "dddd" }, + { 0.5, "eeeee" }, + { 0.6, "ffffff" } + }; + for (size_t i(0); i < msgs.size(); ++i) + { + test_log(REPORT, rep, check, + wsrep::reporter::error, msgs[i].tstamp_, msgs[i].msg_, i); + test_log(REPORT, rep, check, + wsrep::reporter::warning, msgs[i].tstamp_, msgs[i].msg_, i); + } + + // test indefinite timestmap + std::string const msg("err"); + rep.report_log_msg(wsrep::reporter::error, msg); + value = read_file(REPORT); + res = RES_INIT; + parse_result(value, res); + BOOST_REQUIRE(res.errors_.tstamp_.back() > 0); + BOOST_REQUIRE(res.errors_.msg_.back() == msg); + + ::unlink(REPORT); +} + +BOOST_AUTO_TEST_CASE(state_test) +{ + using wsrep::server_state; + + wsrep::default_mutex m; + wsrep::reporter rep(m, REPORT, MAX_MSG); + double const err_tstamp(timestamp()); + std::string const err_msg("Error!"); + + struct test + { + struct + { + enum wsrep::server_state::state state; + float progress; + } input; + struct result output; + }; + + logs const ERRS_INIT = { {err_tstamp}, {err_msg} }; + + std::vector tests = + { + {{ server_state::s_disconnected, 0 }, + { ERRS_INIT, LOGS_INIT, + { "DISCONNECTED", "Disconnected", indefinite }}}, + {{ server_state::s_initializing, 0 }, + { ERRS_INIT, LOGS_INIT, + { "DISCONNECTED", "Initializing", indefinite }}}, + {{ server_state::s_initialized, 0 }, + { ERRS_INIT, LOGS_INIT, + { "DISCONNECTED", "Connecting", indefinite }}}, + {{ server_state::s_connected, 0 }, + { ERRS_INIT, LOGS_INIT, + { "CONNECTED", "Waiting", indefinite }}}, + {{ server_state::s_joiner, 0 }, + { ERRS_INIT, LOGS_INIT, + { "JOINING", "Receiving state", 0.0f }}}, + {{ server_state::s_disconnecting, 0 }, + { ERRS_INIT, LOGS_INIT, + { "DISCONNECTING", "Disconnecting", indefinite }}}, + {{ server_state::s_disconnected, 0 }, + { ERRS_INIT, LOGS_INIT, + { "DISCONNECTED", "Disconnected", indefinite }}}, + {{ server_state::s_connected, 0 }, + { ERRS_INIT, LOGS_INIT, + { "CONNECTED", "Waiting", indefinite }}}, + {{ server_state::s_joiner, 0 }, + { ERRS_INIT, LOGS_INIT, + { "JOINING", "Receiving SST", 0.0f }}}, + {{ server_state::s_initializing, 0 }, + { ERRS_INIT, LOGS_INIT, + { "JOINING", "Initializing", 0.5f }}}, + {{ server_state::s_initialized, 0 }, + { ERRS_INIT, LOGS_INIT, + { "JOINING", "Receiving IST", 0.6f }}}, + {{ server_state::s_joined, 0 }, + { ERRS_INIT, LOGS_INIT, + { "JOINED", "Syncing", 0.0f }}}, + {{ server_state::s_synced, 0 }, + { ERRS_INIT, LOGS_INIT, + { "SYNCED", "Operational", 1.0f }}}, + {{ server_state::s_donor, 0 }, + { ERRS_INIT, LOGS_INIT, + { "DONOR", "Donating SST", 0.0f }}}, + {{ server_state::s_joined, 0 }, + { ERRS_INIT, LOGS_INIT, + { "JOINED", "Syncing", 0.0f }}}, + {{ server_state::s_synced, 0 }, + { ERRS_INIT, LOGS_INIT, + { "SYNCED", "Operational", 1.0f }}}, + {{ server_state::s_disconnecting, 0 }, + { ERRS_INIT, LOGS_INIT, + { "DISCONNECTING", "Disconnecting", indefinite }}}, + }; + + rep.report_log_msg(wsrep::reporter::error, err_msg, err_tstamp); + + for (auto i(tests.begin()); i != tests.end(); ++i) + { + rep.report_state(i->input.state, i->input.progress); + auto value = read_file(REPORT); + result res(RES_INIT); + parse_result(value, res); + auto check(i->output); + VERIFY_RESULT(res, check, i - tests.begin()); + } + + ::unlink(REPORT); +} + +BOOST_AUTO_TEST_CASE(progress_test) +{ + using wsrep::server_state; + + wsrep::default_mutex m; + wsrep::reporter rep(m, REPORT, MAX_MSG); + double const warn_tstamp(timestamp()); + std::string const warn_msg("Warn!"); + + struct test + { + struct + { + enum wsrep::server_state::state state; + float progress; + } input; + struct result output; + }; + + logs const WARN_INIT = { {warn_tstamp}, {warn_msg} }; + + std::vector tests = + { + {{ server_state::s_initialized, 0 }, + { LOGS_INIT, WARN_INIT, + { "DISCONNECTED", "Connecting", indefinite }}}, + {{ server_state::s_connected, 0 }, + { LOGS_INIT, WARN_INIT, + { "CONNECTED", "Waiting", indefinite }}}, + {{ server_state::s_joiner, 0 }, + { LOGS_INIT, WARN_INIT, + { "JOINING", "Receiving state", 0.0f }}}, + {{ server_state::s_joiner, 0.5 }, + { LOGS_INIT, WARN_INIT, + { "JOINING", "Receiving state", 0.5f }}}, + {{ server_state::s_disconnected, 0 }, + { LOGS_INIT, WARN_INIT, + { "DISCONNECTED", "Disconnected", indefinite }}}, + {{ server_state::s_connected, 0 }, + { LOGS_INIT, WARN_INIT, + { "CONNECTED", "Waiting", indefinite }}}, + {{ server_state::s_joiner, 0 }, + { LOGS_INIT, WARN_INIT, + { "JOINING", "Receiving SST", 0.0f }}}, + {{ server_state::s_joiner, 0.5 }, + { LOGS_INIT, WARN_INIT, + { "JOINING", "Receiving SST", 0.25f }}}, + {{ server_state::s_initializing, 0 }, + { LOGS_INIT, WARN_INIT, + { "JOINING", "Initializing", 0.5f }}}, + {{ server_state::s_initializing, 0.5 }, + { LOGS_INIT, WARN_INIT, + { "JOINING", "Initializing", 0.55f }}}, + {{ server_state::s_initialized, 0 }, + { LOGS_INIT, WARN_INIT, + { "JOINING", "Receiving IST", 0.6f }}}, + {{ server_state::s_initialized, 0.5 }, + { LOGS_INIT, WARN_INIT, + { "JOINING", "Receiving IST", 0.8f }}}, + {{ server_state::s_joined, 0 }, + { LOGS_INIT, WARN_INIT, + { "JOINED", "Syncing", 0.0f }}}, + {{ server_state::s_joined, 0.5 }, + { LOGS_INIT, WARN_INIT, + { "JOINED", "Syncing", 0.5f }}}, + {{ server_state::s_synced, 0 }, + { LOGS_INIT, WARN_INIT, + { "SYNCED", "Operational", 1.0f }}}, + {{ server_state::s_donor, 0 }, + { LOGS_INIT, WARN_INIT, + { "DONOR", "Donating SST", 0.0f }}}, + {{ server_state::s_donor, 0.5 }, + { LOGS_INIT, WARN_INIT, + { "DONOR", "Donating SST", 0.5f }}}, + {{ server_state::s_joined, 0 }, + { LOGS_INIT, WARN_INIT, + { "JOINED", "Syncing", 0.0f }}}, + {{ server_state::s_synced, 0 }, + { LOGS_INIT, WARN_INIT, + { "SYNCED", "Operational", 1.0f }}}, + }; + + rep.report_log_msg(wsrep::reporter::warning, warn_msg, warn_tstamp); + + for (auto i(tests.begin()); i != tests.end(); ++i) + { + rep.report_state(i->input.state, i->input.progress); + auto value = read_file(REPORT); + result res(RES_INIT); + parse_result(value, res); + auto check(i->output); + VERIFY_RESULT(res, check, i - tests.begin()); + } + + ::unlink(REPORT); +}