Source code analysis of Sylar server framework configuration module

Posted by auday1982 on Sun, 20 Feb 2022 19:38:22 +0100

Source code analysis of Sylar server framework configuration module

Overall design of configuration module

Basic functions of a configuration module:

  1. It supports defining / declaring configuration items, that is, generating an available configuration item when providing configuration name, type and optional default values. Since a configuration may be used in multiple source files, the configuration module should also support the method of declaring configuration items across files.

  2. Support updating the value of configuration items. This is well understood. When a configuration item is first defined, it may have an initial default value, but the user may have a new value to overwrite the original value.

  3. It supports loading configuration items from preset channels, generally configuration files, command-line parameters, or network servers. Here, it should support not only the loading of basic data types, but also the loading of complex data types. For example, directly load a configuration item of map type from the configuration file, or directly load a user-defined structure from a configuration file of predetermined format.

  4. Support the registration of configuration change notifications for configuration items. The configuration module should provide methods to let the program know that a configuration has been modified, so as to facilitate some operations. For example, for a network server, if the server port configuration changes, the program should restart the listening port. This function is generally realized by registering callback functions. The configuration user registers a configuration change callback function for the configuration item in advance. When the configuration item changes, the corresponding callback function is triggered to notify the caller. Since a configuration may be referenced in multiple places, the configuration change callback function should be in the form of an array.

  5. It supports setting verification methods for configuration items. When defining a configuration item, you can also specify a verification method to ensure that the configuration item will not be set to an illegal value. For example, for the configuration of file path class, you can use the verification method to ensure that the path must exist.

  6. Supports exporting the current configuration.

The configuration module of sylar realizes five functions besides the fifth function.

Mode of use

Adopt the idea of convention due to configuration. Definition is ready for use. It does not need to be resolved separately. Support change notification function. Use YAML file as configuration content. Support data types in level format, STL containers (vector,list,set,map, etc.) and user-defined types (serialization and deserialization methods need to be implemented). The usage is as follows:

static sylar::ConfigVar<int>::ptr g_tcp_connect_timeout =
    sylar::Config::Lookup("tcp.connect.timeout", 5000, "tcp connect timeout");

Defines a TCP connection timeout parameter, which can be directly used G_ tcp_ connect_ Timeout - > getvalue() gets the value of the parameter. When the configuration is modified and reloaded, the value will be updated automatically
The above configuration format is as follows

tcp:
    connect:
            timeout: 10000

Class design

ConfigVarBase => ConfigVar => Config

// Base class of configuration variable
class ConfigVarBase {};

/**
 * @brief Configure parameter template subclasses and save parameter values of corresponding types
 * @details T Specific type of parameter
 *          FromStr Convert from std::string to T-type functor
 *          ToStr Functor that converts from T to std::string
 *          std::string String in YAML format
 */
template<class T, class FromStr = LexicalCast<std::string, T>
                ,class ToStr = LexicalCast<T, std::string> >
class ConfigVar : public ConfigVarBase {};

// The management class of ConfigVar provides a convenient way to create / access ConfigVar
class Config {};

ConfigVarBase: the base class of configuration variables, which is mainly virtual methods. It defines the common members and methods of configuration items. The specific experiments are implemented by the specific subclasses of the base class. The key points are toString() method and fromString() method, which are respectively responsible for transforming the configuration information into a string and parsing the configuration from the string.
ConfigVar: configuration parameter template subclass, which is a template class. It has three template parameters: configuration item type template T, imitation function FromStr and imitation function Tostr. On the basis of ConfigVarBase, ConfigVar class adds AddListener method, delListener and other methods to add or delete configuration change callback functions.
Config: the management class of ConfigVar, which provides convenient methods to create and access ConfigVar. Its main method does not include the Lookup method. You can query the configuration item according to the name of the configuration. If the corresponding configuration is not found during the query, create a new configuration item. It also has the methods of reading the configuration from the configuration file and traversing all configuration items. Among them, the methods in config are static methods to ensure that there is only one global instance.

LexicalCast

The configuration module of sylar uses YAML CPP as the YAML parsing library through boost::lexica_cast and template specialization can realize arbitrary conversion between YAML string and basic type and STL type (can be nested with each other), and user-defined type conversion can be realized by configuring special case template.

/**
 * @brief Type conversion template class (F source type, T target type)
 */
template<class F, class T>
class LexicalCast {};

Generalized version

/**
 * @brief Type conversion template class (F source type, T target type)
 */
template <class F, class T>
class LexicalCast {
public:
    /**
     * @brief Type conversion
     * @param[in] v Source type value
     * @return Returns the v converted target type
     * @exception Throw an exception when the type is not convertible
     */
    T operator()(const F &v) {
        return boost::lexical_cast<T>(v);
    }
};

A specialized version

/**
 * @brief Type conversion template class slice specialization (convert YAML String to STD:: vector < T >)
 */
template<class T>
class LexicalCast<std::string, std::vector<T> > {
public:
    std::vector<T> operator()(const std::string& v) {
        YAML::Node node = YAML::Load(v);
        typename std::vector<T> vec;
        std::stringstream ss;
        for(size_t i = 0; i < node.size(); ++i) {
            ss.str("");
            ss << node[i];
            vec.push_back(LexicalCast<std::string, T>()(ss.str()));
        }
        return vec;
    }
};

Integration of log module and configuration module

Custom type

struct LogDefine {
    std::string name;
    LogLevel::Level level = LogLevel::UNKNOW;
    std::string formatter;
    std::vector<LogAppenderDefine> appenders;

    bool operator==(const LogDefine& oth) const {
        return name == oth.name
            && level == oth.level
            && formatter == oth.formatter
            && appenders == appenders;
    }

    bool operator<(const LogDefine& oth) const { // With the map, all need to compare the size
        return name < oth.name;
    }

    bool isValid() const {
        return !name.empty();
    }
};

Special case template

// yml =>logdefine
template<>
class LexicalCast<std::string, LogDefine> {
public:
    LogDefine operator()(const std::string& v) {
        YAML::Node n = YAML::Load(v);
        LogDefine ld;
        if(!n["name"].IsDefined()) {
            std::cout << "log config error: name is null, " << n
                      << std::endl;
            throw std::logic_error("log config name is null");
        }
        ld.name = n["name"].as<std::string>();
        ld.level = LogLevel::FromString(n["level"].IsDefined() ? n["level"].as<std::string>() : "");
        if(n["formatter"].IsDefined()) {
            ld.formatter = n["formatter"].as<std::string>();
        }

        if(n["appenders"].IsDefined()) {
            //std::cout << "==" << ld.name << " = " << n["appenders"].size() << std::endl;
            for(size_t x = 0; x < n["appenders"].size(); ++x) {
                auto a = n["appenders"][x];
                if(!a["type"].IsDefined()) {
                    std::cout << "log config error: appender type is null, " << a
                              << std::endl;
                    continue;
                }
                std::string type = a["type"].as<std::string>();
                LogAppenderDefine lad;
                if(type == "FileLogAppender") {
                    lad.type = 1;
                    if(!a["file"].IsDefined()) {
                        std::cout << "log config error: fileappender file is null, " << a
                              << std::endl;
                        continue;
                    }
                    lad.file = a["file"].as<std::string>();
                    if(a["formatter"].IsDefined()) {
                        lad.formatter = a["formatter"].as<std::string>();
                    }
                } else if(type == "StdoutLogAppender") {
                    lad.type = 2;
                    if(a["formatter"].IsDefined()) {
                        lad.formatter = a["formatter"].as<std::string>();
                    }
                } else {
                    std::cout << "log config error: appender type is invalid, " << a
                              << std::endl;
                    continue;
                }

                ld.appenders.push_back(lad);
            }
        }
        return ld;
    }
};

template<>
class LexicalCast<LogDefine, std::string> {
public:
    std::string operator()(const LogDefine& i) {
        YAML::Node n;
        n["name"] = i.name;
        if(i.level != LogLevel::UNKNOW) {
            n["level"] = LogLevel::ToString(i.level);
        }
        if(!i.formatter.empty()) {
            n["formatter"] = i.formatter;
        }

        for(auto& a : i.appenders) {
            YAML::Node na;
            if(a.type == 1) {
                na["type"] = "FileLogAppender";
                na["file"] = a.file;
            } else if(a.type == 2) {
                na["type"] = "StdoutLogAppender";
            }
            if(a.level != LogLevel::UNKNOW) {
                na["level"] = LogLevel::ToString(a.level);
            }

            if(!a.formatter.empty()) {
                na["formatter"] = a.formatter;
            }

            n["appenders"].push_back(na);
        }
        std::stringstream ss;
        ss << n;
        return ss.str();
    }
};

Mode of use

// Use config to store logdefinition and maintain log configuration information
sylar::ConfigVar<std::set<LogDefine> >::ptr g_log_defines =
    sylar::Config::Lookup("logs", std::set<LogDefine>(), "logs config");

Callback function added through constructor of static global variable

static LogIniter __log_init; // Initialization on load, before mian function
struct LogIniter {
    LogIniter() {
        g_log_defines->addListener([](const std::set<LogDefine>& old_value,
                    const std::set<LogDefine>& new_value){
            SYLAR_LOG_INFO(SYLAR_LOG_ROOT()) << "on_logger_conf_changed";
            for(auto& i : new_value) {
                auto it = old_value.find(i);
                sylar::Logger::ptr logger;
                if(it == old_value.end()) {
                    //New logger
                    logger = SYLAR_LOG_NAME(i.name);
                } else {
                    if(!(i == *it)) {
                        //Modified logger
                        logger = SYLAR_LOG_NAME(i.name);
                    } else {
                        continue;
                    }
                }
                logger->setLevel(i.level);
                //std::cout << "** " << i.name << " level=" << i.level
                //<< "  " << logger << std::endl;
                if(!i.formatter.empty()) {
                    logger->setFormatter(i.formatter);
                }

                logger->clearAppenders();
                for(auto& a : i.appenders) {
                    sylar::LogAppender::ptr ap;
                    if(a.type == 1) {
                        ap.reset(new FileLogAppender(a.file));
                    } else if(a.type == 2) {
                        ap.reset(new StdoutLogAppender);
                        // if(!sylar::EnvMgr::GetInstance()->has("d")) {
                        //     ap.reset(new StdoutLogAppender);
                        // } else {
                        //     continue;
                        // }
                    }
                    ap->setLevel(a.level);
                    if(!a.formatter.empty()) {
                        LogFormatter::ptr fmt(new LogFormatter(a.formatter));
                        if(!fmt->isError()) {
                            ap->setFormatter(fmt);
                        } else {
                            std::cout << "log.name=" << i.name << " appender type=" << a.type
                                      << " formatter=" << a.formatter << " is invalid" << std::endl;
                        }
                    }
                    logger->addAppender(ap);
                }
            }

            for(auto& i : old_value) {
                auto it = new_value.find(i);
                if(it == new_value.end()) {
                    //Delete logger
                    auto logger = SYLAR_LOG_NAME(i.name);
                    logger->setLevel((LogLevel::Level)0);
                    logger->clearAppenders();
                }
            }
        });
    }
};

Topics: C++ server