Определение неиспользуемых настроек в библиотеке libconfig

    Введение


    Большие приложения используют конфиги для передачи настроек. И часто случаетя так, что редактирование и удаление фич приводит к рассинхронизации между кодом приложения и тем, что хранится в этих самых настройках. Попросту, в последних оседают данные, которыми вы больше никогда не воспользуетесь. Такие настройки в идеале хотелось бы отслеживать и помечать как deprecated, либо вовсе удалять.


    Проблема


    Так исторически сложилось, что большая часть конфигов внутри нашего проекта была написана на смеси json и yaml и парсилась с использованием библиотеки libconfig. Переписывать соответствующий код и содержимое конфигов, например на yaml, желания никакого не было, особенно когда есть куча других интересных и более сложных задач. Да и та часть библиотеки, что написана на C, сама по себе хороша: стабильна и богата функционалом (в обертке на C++ все не так однозначно).


    В один прекрасный день мы озаботились тем, чтобы выяснить, сколько мусора у нас накопилось в конфигурационных файлах. И такой опции у libconfig, к сожалению, не оказалось. Сначала мы попытались форкнуть проект на github и внести правки в ту часть, что написана на C++ (всевозможные методы lookup и operator[]), автоматизировав процесс выставления флага visited для ноды. Но это привело бы к очень большому патчу, принятие которого наверняка затянулось бы. И тогда выбор пал в сторону написания своей собственной обертки на C++, не затрагивая ядро libconfig.


    С точки зрения использования на выходе у нас получилось следующее:


    #include <variti/util/config.hpp>
    
    #include <iostream>
    #include <cassert>
    
    int main(int argc, char* argv[])
    {
      using namespace variti;
      using namespace variti::util;
      assert(argc = 2);
      config conf(
        [](const config_setting& st) {
          if (!st.visited())
            std::cerr << "config not visited: " << st.path() << "\n";
        });
      conf.load(argv[1]);
      auto root = conf.root();
      root["module"]["name"].to_string();
      return 0;
    }

    laptop :: work/configpp/example ‹master*› % cat config_example1.conf                                                                                                                                                                         
    version = "1.0";
    module: {
      name = "module1";
      submodules = (
        { name = "submodule1"; },
        { name = "submodule2"; }
      );
    };
    laptop :: work/configpp/example ‹master*› % ./config-example1 config_example1.conf                                                                                                                                                           
    config not visited: root.module.submodules.0.name
    config not visited: root.module.submodules.1.name

    В коде примера мы обратились к настройке module.name. К настройкам module.submodules.0.name и module.submodules.1.name не обращались. Об этом нам и сообщается в логе.


    Обертывание


    Как это реализовать, если флага visited или чего-то подобного нет внутри libconfig? Разработчики библиотеки подумали заранее и добавили возможность прицепить к ноде config_setting_t хуку, которая проставляется с помощью функции config_setting_set_hook и читается с помощью config_setting_get_hook.


    Определим эту хуку как:


    struct config_setting_hook
    {
      bool visited{false};
    };

    Внутри libconfig есть две основных структуры: config_t и config_setting_t. Первая предоставляет доступ ко всему конфигу в целом и возвращает указатель на рутовую ноду config_setting_t, вторая — доступ к родительской и дочерним нодам, а также значению внутри текущей ноды.


    Обернем обе структуры в соответствующие классы — хэндлы.


    Хэндл вокруг config_t:


    using config_notify = std::function<void(const config_setting&)>;
    
    struct config
      : boost::noncopyable
    {
      config(config_notify n = nullptr);
     ~config();
    
      void load(const std::string& filename);
    
      config_setting root() const;
    
      config_notify n;
      config_t* h;
    };

    Обратим внимание, что в конструктор config передается функция, которая будет вызвана в деструкторе в момент обхода всех крайних нод. Как она может быть использована — можно увидеть в примере выше.


    Хэндл вокруг config_setting_t:


    struct config_setting
      : boost::noncopyable
    {
      config_setting(config_setting_t* h, bool visit = false);
     ~config_setting();
    
      bool to_bool() const;
      std::int32_t to_int32() const;
      std::int64_t to_int64() const;
      double to_double() const;
      std::string to_string() const;
    
      bool is_bool() const;
      bool is_int32() const;
      bool is_int64() const;
      bool is_double() const;
      bool is_string() const;
      bool is_group() const;
      bool is_array() const;
      bool is_list() const;
      bool is_scalar() const;
      bool is_root() const;
    
      std::string path() const;
    
      std::size_t size() const;
    
      bool exists(const std::string& name) const;
    
      config_setting parent() const;
    
      config_setting lookup(const std::string& name, bool visit = false) const;
      config_setting lookup(std::size_t indx, bool visit = false) const;
    
      config_setting operator[](const std::string& name) const;
      config_setting operator[](std::size_t indx) const;
    
      std::string filename() const;
      std::size_t fileline() const;
    
      bool visited() const;
    
      config_setting_t* h;
    };

    Основная магия кроется в методах lookup. Предполагается, что флаг visited ноды устанавливается через последний аргумент под названием visit, который по умолчанию равен false. Вы вправе сами указывать это значение. Но так как наиболее частый доступ к нодам все же происходит через operator[], то внутри него метод lookup вызывается с visit, равным true. Таким образом, ноды, для которых вы вызовете operator[], будут автоматом помечены как visited. Более того, как visited будет помечена вся цепочка нод от текущей и до рутовой.


    Перейдем к реализации. Покажем полностью ее для класса config:


    config::config(config_notify n)
      : n(n)
    {
      h = (config_t*)malloc(sizeof(config_t));
      config_init(h);
      config_set_destructor(h,
        [](void* p) {
          delete reinterpret_cast<config_setting_hook*>(p);
        });
    }
    
    config::~config()
    {
      if (n)
        for_each(root(), n);
      config_destroy(h);
      free(h);
    }
    
    void config::load(const std::string& filename)
    {
      if (!config_read_file(h, filename.c_str()))
        throw std::runtime_error(std::string("config read file error: ") + filename);
    }
    
    config_setting config::root() const
    {
      return config_setting(config_root_setting(h));
    }

    И частично для config_setting:


    config_setting::config_setting(config_setting_t* h, bool visit)
      : h(h)
    {
      assert(h);
      if (!config_setting_get_hook(h))
        hook(h, new config_setting_hook())
      if (visit)
        visit_up(h);
    }
    
    config_setting::~config_setting()
    {
      h = nullptr;
    }
    
    std::size_t config_setting::size() const
    {
      return config_setting_length(h);
    }
    
    config_setting config_setting::parent() const
    {
      return config_setting(config_setting_parent(h));
    }
    
    bool config_setting::exists(const std::string& name) const
    {
      if (!is_group())
        return false;
      return config_setting_get_member(h, name.c_str());
    }
    
    config_setting config_setting::lookup(const std::string& name, bool visit) const
    {
      assert(is_group());
      auto p = config_setting_get_member(h, name.c_str());
      if (!p)
        throw_not_found(*this);
      return config_setting(p, visit);
    }
    
    config_setting config_setting::lookup(std::size_t indx, bool visit) const
    {
      assert(is_group() || is_array() || is_list());
      auto p = config_setting_get_elem(h, indx);
      if (!p)
        throw_not_found(*this);
      return config_setting(p, visit);
    }
    
    config_setting config_setting::operator[](const std::string& name) const
    {
      return lookup(name, true);
    }
    
    config_setting config_setting::operator[](std::size_t indx) const
    {
      return lookup(indx, true);
    }
    
    bool config_setting::visited() const
    {
      return boost::algorithm::starts_with(path(), "root") ||
             boost::algorithm::starts_with(path(), "root.version") ||
             hook(h)->visited;
    }

    Отдельно рассмотрим хэлперы для работы с хукой:


    void hook(config_setting_t* h, config_setting_hook* k)
    {
      config_setting_set_hook(h, k);
    }
    
    config_setting_hook* hook(config_setting_t* h)
    {
      return reinterpret_cast<config_setting_hook*>(config_setting_get_hook(h));
    }
    
    void visit_up(config_setting_t* h)
    {
      for (; !config_setting_is_root(h) && !hook(h)->visited; h = config_setting_parent(h))
        hook(h)->visited = true;
    }

    И хэлпер для обхода крайних нод:


    template <typename F>
    void for_each(const config_setting& st, F f)
    {
      if (st.size())
        for (std::size_t i = 0; i < st.size(); ++i)
          for_each(st.lookup(i), f);
      else
        f(st);
    }

    Вывод


    Получился красивый и более гибкий, на наш взгляд, код. Но мы не забросили мысль о том, чтобы внести аналогичные изменения в оригинальную библиотеку libconfig, а, точнее, в ее интерфейс, написанный на C++. Сейчас готовится пулл-реквест, ну а мы уже работаем и чистим наши конфиги от неиспользуемых настроек.


    Приложение


    Ознакомиться с исходным кодом можно тут!

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 14

      0
      А как вы работаете с конфигами? Я в разных проектах попробовал 2 разных способа
      1) Все значения из конфига считываются в функции main. И потом раздаются всем необходимым классам. Недостаток: при сложной иерархии классов очень сложно докинуть каждую настройку до нужного класса
      2) Каждый класс берет из конфига ровно те настройки, которые ему нужны. Пока склоняюсь к такому варианту, но, возможно в нем есть какието минусы
        0
        Есть единый реестр. Каждый модуль сам считывает из него свои настройки на страте и при релоаде. Если обновление настроек происходит чаще, то для наиболее дешевой синхронизации мы используем atomic_ptr на shared_ptr, в котором размещается либо variti::util::config, либо структура, куда уже вычитаны данные из variti::util::config (преобразованы в более удобный для работы вид).
          0
          А как с thread safety? Как я понимаю, libconfig не thread-safe даже на чтение. Или у вас разрешено перечитывание конфига только из основного потока?
            0
            Вся конфигурация зачитывается либо из главного потока на старте, либо из потока, в котором обрабатывается сигнал sighup. Если коротко про thread-safety, то:
            1. что-то read-only
            2. что-то зачитывается редко и может быть синхронизировано через mutex
            3. что-то зачитывается часто и тогда mutex не подходит (я написал выше про atomic_ptr на shared_ptr)
        0
        <удалено>
          0
          мы конфиги из Монги вычитываем. Удобно и быстро, доступно сразу на всех серверах. Чем Ваш подход удобнее?
            0
            Из mongodb красиво и удобно, да… но:
            1. так исторически сложилось
            2. а сложилось, потому что нам важно перезагружать или релоадить сервис быстро и в 100% случаев (mongodb может быть не доступен по своим причинам)
              0
              1. ОК, понимаю
              2. зачем релоадить сервис?
              В монге админкой обновили конфиги и всё. И просто запросы в неё.
              У нас в фоновом режиме периодически она запрашивается и обновляется хэш-мапа, из кот. потом получаются конфиги. Если недоступна (кот. происходит практически никогда), то хеш мапа не обновлена и всё.
                0
                Релоад (sighup) или обновление в фоне по событию от mongodb — есть какая-то разница? У нас выглядит примерно так (к вопросу о том, насколько mongodb будет безотказно работать):
                1. конфиги раскладываются админкой (часто)
                2. конфиги раскладываются клиентом из личного кабинета (постоянно)
                3. архитектура распределенная (несколько кластеров в нескольких странах)
                  0
                  каким образом раскладывается на множество серверов? Используете какое-то готовое решение? Или на каждом из серверов какая-то ручка, кот. получает новый конфиг по сети?
                    0
                    Второе
                    0
                    + sighup как вызывается на удалённом сервере?
                      0
                      Управление сервисами через salt ssh.
              0
              <удалено>

              Only users with full accounts can post comments. Log in, please.