Ориентация главы
Глава 139 зафиксировала проект configd: семислойный конвейер (источники → разбор → слияние → шаблон → валидация → снимок → запросы), поэтапная модель «домен загрузки в value + домен снимка в структуре», протокол передачи «неизменяемый снимок + атомарный указатель», явно объявленная семантика слияния, три границы безопасности шаблона. Эта глава — глава реализации: каждое решение приземляется построчно и построчно сверяется: шесть критериев приёмки проектной главы (у каждого из шести UC позитивные и негативные случаи / векторы семантики слияния / гонка горячей замены / валидация с путями / OOM по каждой точке / ноль следов при неудачной загрузке) отмечаются по мере завершения реализации. «Реализация» в проектной главе — не «переписать проект в код», а проверка проекта под настоящими ограничениями кода — глава покажет два взаимных уточнения проекта и реализации (слой шаблона меняет «переиспользовать шаблонизатор» на «специализированное раскрытие» — вынудил бюджет размера; извлекатель меняет «макрос» на «декларативный массив + цикл» — вынудила тестируемость), и это само по себе исполнение пункта проектной главы «решения можно пересматривать».
140.1引入
Стартовая точка реализации — файлы приёмки прежде исходника: три группы векторов слияния, выведенные вручную в упражнениях главы 139 (вложенное переопределение / замена массива / конфликт типов), записываются в файлы ожиданий тестового каталога — это машинное определение семантики слияния; позитивные и негативные случаи шести UC выписываются списком — после реализации каждой строки прогон. Ритм разработки по семи слоям снизу вверх: сперва разбор и слияние (чистые преобразования данных — легче всего тестировать), затем шаблон и валидация (слой семантики), потом снимок и передача (слой конкурентности), и наконец слой источников (файл/HTTP — граница IO). Каждый слой сразу по реализации получает свои тесты — определение завершения слоя — «тесты зелёные», а не «код написан». Этот ритм даёт самому длинному пути (гонке горячей замены) всегда проверенный фундамент.
140.2概念
Карта слоёв реализации
На какую предыдущую главу ложится каждый из семи слоёв этой главы и сколько кода написано самим — карта границы между сборкой и собственным кодом:
Двести восемьдесят строк на центр конфигурации с горячей заменой и гарантиями против гонок — вот количественное выражение ценности слоистой библиотеки: каждый слой — тема чьей-то главы, проект пишет только межслойный клей и бизнес-семантику (слияние / шаблон / правила — то, что действительно различается от проекта к проекту). Взаимное подтверждение с «< 400 строк» webserv главы 145: объём кода XRT-проекта пропорционален бизнес-сложности и не зависит от системной — системная сложность впитана библиотечными слоями.
Приземление слоёв разбора и слияния
Слой разбора — один шаг: xrtJsonParse (глава 32) производит дерево value — чистое преобразование от потока байтов источника (файловый Reader главы 45 или тело HTTP-ответа) к дереву. Слой слияния — первый собственный алгоритм этой главы (в библиотечном слое нет глубокого слияния деревьев value — это бизнес configd):
/* рекурсивное слияние объектов, замена массивов целиком — семантика закреплена тестовыми векторами */ static bool cfg_merge(xvalue* Base, const xvalue* Over) { size_t i; if ( !xrtValueIs(Base, XVALUE_OBJECT) || !xrtValueIs(Over, XVALUE_OBJECT) ) { return false; /* конфликт типов: ошибка с путём */ } for ( i = 0; i < xrtValueCount(Over); i++ ) { xstrview Key; xvalue* Ov = xrtValueObjectAt(Over, i, &Key); xvalue* Bv = xrtValueObjectGet(Base, Key); if ( (Bv != NULL) && xrtValueIs(Bv, XVALUE_OBJECT) && xrtValueIs(Ov, XVALUE_OBJECT) ) { if ( !cfg_merge(Bv, Ov) ) { return false; } /* рекурсия */ } else if ( !xrtValueObjectEdit(Base, Key, Ov) ) { return false; /* лист или массив: замена целиком */ } } return true; }
Закрепление тремя группами векторов: вложенность (db.pool.max перекрывается, а db.pool.min сохраняется — рекурсивная ветвь); массивы (hosts заменяется целиком, не поэлементно — листовая ветвь); конфликт типов (умолчание int, окружение даёт string — после замены через ObjectEdit тип изменился, слой валидации сообщает ошибку с путём). Слияние чувствительно к порядку: два слоя в фиксированном порядке default → env — при расширении на много слоёв порядок становится частью конфигурации (объявляется в параметрах загрузки).
Слой шаблона: уточнение от переиспользования к специализации
Проектная глава оставила две альтернативы (переиспользование шаблонизатора против специализированного раскрытия). Размерный расчёт реализации решает отказаться от переиспользования: замыкание шаблонизатора (полный набор главы 35) больше всех остальных слоёв configd вместе взятых — заводить целое шаблонное дерево ради одного синтаксиса {$VAR} не стоит. Специализированное раскрытие — тридцать строк:
/* после слияния, до валидации: только переменные окружения, без рекурсии, неопределённая — ошибка */ static bool cfg_expand(xstrview In, xstrbuf* Out) { size_t i = 0; while ( i < In.Size ) { if ( (i + 1 < In.Size) && (In.Data[i] == '$') && (In.Data[i + 1] == '{') ) { size_t j = i + 2, k; while ( (j < In.Size) && (In.Data[j] != '}') ) { j++; } if ( j >= In.Size ) { return false; } /* не закрыто */ k = j + 1; /* {$ внутри раскрытого значения больше не раскрывается (без рекурсии) */ (void)k; { char Name[64]; size_t n = j - (i + 2); if ( (n == 0) || (n >= sizeof(Name)) ) { return false; } memcpy(Name, In.Data + i + 2, n); Name[n] = 0; if ( !cfg_env_append(Out, Name) ) { return false; } /* неопределённая — ошибка */ } i = j + 1; } else if ( !xrtStrBufAppendByte(Out, In.Data[i]) ) { return false; } else { i++; } } return true; }
Где в коде лежат три границы безопасности: cfg_env_append смотрит только окружение (источник из белого списка — глава 43); продукт раскрытия больше не входит в цикл раскрытия ({$ внутри значения выводится буквально — без рекурсии); неопределённая переменная — cfg_env_append возвращает false (ошибка с именем переменной). Это первое исполнение пункта проектной главы «решения можно пересматривать» — запись о пересмотре вошла в эту главу (переиспользование → специализация, основание: размер).
Слой валидации: правила как данные
«Правила — небольшой декларативный массив» проектной главы приземляется в структуру с тремя полями (путь / тип / ограничение):
typedef struct { const char* Path; /* "server.port" */ int Type; /* CFG_T_INT и т.п. */ bool (*Check)(long long v); /* диапазон: напр. port 1..65535 */ } cfgrule; static const cfgrule kRules[] = { { "server.port", CFG_T_INT, rule_port }, { "server.tls.cert", CFG_T_STR, NULL }, { "db.pool.max", CFG_T_INT, rule_positive }, { "log.level", CFG_T_STR, rule_level }, };
Цикл валидации: взять значение по пути (ObjectGet спускается по сегментам) → определить тип (xrtValueIs против перечисления типов) → обратный вызов диапазона. Выходной формат ошибок с путём: "db.pool.max: must be > 0" — путь из правила, сообщение из обратного вызова ограничения. Экономика сопровождения «новое поле — одна строка правила» здесь количественна: 20 полей конфигурации = 20 строк правил (изоморфно извлекателю). Три слоя валидации одним проходом (существование / тип / диапазон в одном цикле) — вызов в конце загрузки, перехват на старте (критерий (3)).
Рассуждение о корректности последовательности протокола передачи
Почему «неизменяемый снимок + атомарный указатель» гарантирует целостную согласованность в любой момент? Разберём от противного: пусть снимок изменяем (поток загрузки правит поля, пока они обслуживаются) — поток запросов вытесняется планировщиком между двумя чтениями полей — вернувшись, читает наполовину изменённую конфигурацию (port новое значение со старым путём TLS) — это ровно состояние гонки ловушки 3 главы 139. Неизменяемость уничтожает это окно структурно: после публикации снимка (атомарная замена указателя завершена) его никто больше не пишет — байты за ссылкой, полученной потоком запросов, не меняются никогда; поток загрузки строит новый объект (нулевое разделение со старым снимком или разделение только неизменяемых частей). Атомарная замена указателя гарантирует атомарность самого акта публикации (читается старый указатель или новый — промежуточного нет). Две атомарности складываются в один инвариант: в любой миг все читатели видят конфигурацию — продукт одного полного акта загрузки. Graceful-передача долгих операций отсюда достаётся даром: читатель с Ref (процесс обработки одного запроса) продолжает пользоваться старым снимком до Destroy — новые читатели немедленно видят новое — у передачи нет пограничного события (не нужны ни уведомления, ни барьеры — подсчёт ссылок сходится сам). Тестовая форма этого рассуждения — тест гонки (сто тысяч согласованных чтений) — теория, механизм и тест согласованы в трёх слоях.
Слой снимка и атомарная передача
Извлечение снимка — разовое преобразование из домена value в домен структуры. Второй пересмотр проекта: извлекатель меняет «макрос» на «декларативный массив + цикл» — макрос разворачивается на этапе препроцессирования (отладчик его не видит), тесты не могут утверждать по полям; декларативный массив делает правила извлечения обходимыми и тестируемыми:
typedef struct { const char* Path; int Kind; /* целевой тип извлечения */ void* Slot; /* адрес поля структуры */ } cfgslot; static const cfgslot kSlots[] = { { "server.port", CFG_K_INT, &Snap->Server.Port }, { "server.root", CFG_K_STR, &Snap->Server.Root }, { "db.pool.max", CFG_K_INT, &Snap->Db.PoolMax }, };
Три элемента протокола передачи (приземление оригинала проекта главы 139): неизменяемый снимок — после завершения извлечения структура только читается (документированное соглашение + контроль на ревью — в C нет языкового принуждения; в сочетании с дисциплиной неизменяемого разделения главы 115); подсчёт ссылок — в голове структуры снимка вложена общая ссылка (глава 5); атомарный указатель — замена глобального указателя текущего снимка атомарным обменом (глава 9). Сторона запросов: cfg_current() атомарно читает указатель + Ref, после использования Destroy — целиком согласованно в любой момент (механизм критерия (6)).
Послойная проверка атомарности отказов конвейера загрузки
Механизм критериев (5)(6) (OOM без следов / неудачная загрузка без утечки), слой за слоем: слой разбора — неудача JsonParse возвращает NULL без частичного дерева (атомарность главы 32); слой слияния — а вдруг при неудаче cfg_merge Base уже частично изменён? — единственная слабая точка этого слоя: рекурсивное слияние модифицирует Base на месте — при отказе посреди Base в наполовину слитом состоянии. Решение: рабочая копия для слияния — каждая загрузка заново разбирает слой умолчаний (или глубоко клонирует) и сливает на копии — отказ отбрасывает копию, оригинал без потерь (паттерн рабочей копии — изоморф очистке при отказе главы 134). Слой шаблона — при неудаче добавления в StrBuf буфер отбрасывается (атомарность главы 25). Слой валидации — чистое чтение, без изменений. Слой извлечения — чистое копирование и только после полного успеха. Слой замены — атомарная замена указателя только после прохождения всего. Ожидание по-точечного внедрения отказов: отказ в любой точке — глобальный указатель снимка не тронут, читатели не замечают, ошибка с локализацией — ровно то, что по-точечно утверждает OOM-тест (ротатор главы 134 гоняет конвейер загрузки). Атомарность отказов не гарантируется «аккуратным письмом» — она сумма структурных свойств каждого слоя.
Слой источников и контур горячей замены
Файловый источник: xfile читает все байты файла → вход слоя разбора (глава 45). HTTP-источник: вытягивание клиентом (главы 98/99) — семантика отказа разделена по моментам (первый старт обязан упасть / отказ обновления — деградация — дополнительный проект HTTP главы 139). Контур горячей замены: watcher (опрос mtime файла или асинхронное уведомление главы 48) срабатывает → повторный проход конвейера загрузки (разбор → слияние → шаблон → валидация → извлечение) → атомарная замена новым снимком → старый снимок естественно уходит на пенсию. Ветка отказа: валидация новой конфигурации провалилась — не заменять (старый снимок продолжает обслуживать) + тревога (глава 38) — «плохая конфигурация не вредит работающему сервису» — линия безопасности горячей замены.
Форма поставки и контрольный лист развёртывания
Поставка и развёртывание configd (сервисная версия CLI-контракта главы 143): форма поставки — статическая библиотека (ссылается приложение-хозяин — центр конфигурации встраиваемый компонент, а не отдельный процесс; позиция главы 135 о встраивании: суверенитет памяти/ошибок/размера у хозяина); декларация обрезки — замыкание value+template+core (без сети — HTTP-источник в опциональной компиляции). Три функции API: cfg_init(来源参数) (загрузка + валидация + публикация первого снимка — при отказе возвращает ошибку), cfg_current() (атомарно взять Ref снимка), cfg_reload() (повторный проход конвейера — вход горячей замены) — минимальная полнота: три функции закрывают шесть UC (семейство запросов целиком идёт через доступ к полям current). Контрольный лист развёртывания: пути файлов конфигурации и порядок слоёв, белый список переменных окружения (множество, на которое может ссылаться шаблон — выход за него = отказ загрузки), таблица правил валидации, бюджет снимка (верхний предел памяти дерева конфигурации — проверка stats). Все четыре пункта — декларации, а не код — инженер развёртывания подстраивает без правки C — конфигурация самой конфигурационной системы тоже конфигурируется (самозагрузочная согласованность).
Зарезервированные пути расширения
Три точки расширения, указанные в «сопоставлении с реальными системами конфигурации» главы 139, ложатся на слой реализации так: watch-пуш (форма etcd) — в слой источников добавляется опрос или долгое соединение (главы 66/82); слой замены не меняется (протокол снимков уже поддерживает замену любой частоты — дальновидность протокола); комбинация многих профилей (форма Spring) — число слоёв слияния с 2 до N: cfg_merge уже бинарная операция над любыми двумя слоями — комбинация лишь превращает параметр загрузки из пары в упорядоченный список; эволюция схемы (добавление и удаление полей) — строка в массив правил и массив слотов, и готово — дивиденд подхода «данные как данные». Общее у трёх расширений: ни одно не трогает протокол передачи — корректность против гонок — самое дорогое свойство; на этапе проектирования оно отвязано от бизнеса — как бы бизнес ни менялся, протокол неизменен. Это самое важное наследие проекта главы 139.
Финальная сверка с проектной главой
Построчная сверка шести критериев приёмки: (1) шесть UC с позитивными и негативными случаями — загрузка / запросы / валидация / шаблон, у каждого прямые и обратные случаи (тестовый список процесса реализации весь зелёный); (2) векторы слияния — три группы файлов ожиданий зелёные (закрепление раздела слияния); (3) гонка горячей замены — threads-тест: сто тысяч конкурентных запросов во время обновления, ноль смешанных состояний (машинное доказательство «неизменяемый снимок + атомарный указатель»); (4) валидация с путями — три намеренно нарушающих входа все перехвачены, ошибки содержат путь (формат раздела валидации); (5) OOM по точкам — внедрение отказов в конвейер загрузки: отказ в любой точке, ноль следов (атомарность отказов каждого слоя + финальное «не заменять»); (6) неудачная загрузка без следов — половинная конфигурация не просачивается (снимок заменяется только после прохождения всего). Все шесть пройдены — configd готов к поставке.
140.3示例
Первая полная программа: атом загрузки и запроса value
Программа ниже из examples/data/json — исполняемый атом слоёв разбора и запросов (уже цитировался в проектной главе 138; здесь перепроверяется как стартовая точка реализации):
*/ #include <stdio.h> #include <xrt.h> */ static xjsonvisitaction printJsonEvent( const xjsonevent* pEvent, ptr pUserData ) { (void)pUserData; if ( pEvent->HasName ) { printf("member: %.*s\n", (int)pEvent->Name.Size, pEvent->Name.Data); } return XJSON_VISIT_NEXT; } int main(void) { xjsonreadconfig ReadConfig; xjsonwriteconfig WriteConfig; xjsonwriter* pWriter; xvalue* pRoot; xvalue* pName; xstrview Name; str sText; size_t iSize; /* ---- 1) DOM:解析 → 随机访问 → 美化序列化 ---- */ pRoot = xrtJsonParse(XRT_STR_LITERAL( "{\"name\":\"xrt\",\"features\":[\"json\",\"http\"]}" )); if ( pRoot == NULL ) { return 1; } /* ObjectGet 返回借用指针(树拥有它);GetString 取出视图。 */ pName = xrtValueObjectGet(pRoot, XRT_STR_LITERAL("name")); if ( !xrtValueGetString(pName, &Name) ) { xrtValueRelease(pRoot); return 2; } printf("name = %.*s\n", (int)Name.Size, Name.Data); /* Stringify 产物是拥有式字符串(xrtFree);之后树即可释放。 */ sText = xrtJsonStringify(pRoot, true, &iSize); xrtValueRelease(pRoot); if ( sText == NULL ) { return 3; } printf("%.*s\n", (int)iSize, sText); xrtFree(sText); /* ---- 2) SAX:同一份输入按事件流走一遍,不建树 ---- */ xrtJsonReadConfigInit(&ReadConfig); if ( xrtJsonVisit( XRT_STR_LITERAL("{\"code\":200,\"ok\":true}"), &ReadConfig, printJsonEvent, NULL ) != XJSON_VISIT_DONE ) { return 4; } /* ---- 3) Writer:增量构建 {"code":200,"message":"OK"} ---- */ xrtJsonWriteConfigInit(&WriteConfig); pWriter = xrtJsonWriterCreate(&WriteConfig); if ( (pWriter == NULL) || !xrtJsonWriterObject(pWriter) || /* 开对象 */ !xrtJsonWriterName(pWriter, XRT_STR_LITERAL("code")) || !xrtJsonWriterInt(pWriter, 200) || !xrtJsonWriterName(pWriter, XRT_STR_LITERAL("message")) || !xrtJsonWriterString(pWriter, XRT_STR_LITERAL("OK")) || !xrtJsonWriterEnd(pWriter) || /* 闭对象 */ !xrtJsonWriterFinish(pWriter) /* 完整性校验 */ ) { xrtJsonWriterFree(pWriter); return 5; } /* Take 移交文本(xrtFree 释放);Take 后 writer 仍需 Free。 */ sText = xrtJsonWriterTake(pWriter, &iSize); xrtJsonWriterFree(pWriter); if ( sText == NULL ) { return 6; } printf("%.*s\n", (int)iSize, sText); xrtFree(sText); return 0; }
Что только что произошло. (1) Это весь слой разбора configd — глава реализации добавила сверху лишь четыре чистых преобразования: слияние / шаблон / валидацию / извлечение — каждый слой проходит девелоперскую проверку на этом атоме (юнит-тесту слоя не нужен полный конвейер). (2) DOM-запрос (ObjectGet берёт по ключу) — ровно словарь спуска по путям слоя валидации — одно семейство API пронизывает каждый слой домена загрузки (исполнение DOM-проекта главы 31 в этом проекте). (3) Выбор из трёх видов использования (комментарий в шапке: DOM/SAX/Writer) в configd: загрузка через DOM (нужны случайный доступ и слияние — дерево обязано быть полным); Writer не нужен (конфигурация — вход, а не выход).
Вторая полная программа: комбинированная граница шаблона и файла
Вторая программа из examples/template/file — материальный носитель альтернативы слоя шаблона из проектной главы (эталон пересмотренного решения этой главы):
*/ #include <stdio.h> #include <xrt.h> int main(void) { xtemplate* pTemplate = NULL; xvalue* pData = NULL; str sOutput = NULL; int iResult = 1; pTemplate = xrtTemplateCompileFile("examples/template/file/page.tpl"); pData = xrtValueObject(); if ( (pTemplate == NULL) || (pData == NULL) || !xrtValueObjectSetNew( pData, XRT_STR_LITERAL("name"), xrtValueString(XRT_STR_LITERAL("Alice")) ) ) { goto cleanup; } sOutput = xrtTemplateRender(pTemplate, pData, NULL); if ( sOutput == NULL ) { iResult = 2; goto cleanup; } printf("%s\n", sOutput); iResult = 0; cleanup: xrtFree(sOutput); if ( pData != NULL ) { xrtValueRelease(pData); } if ( pTemplate != NULL ) { xrtTemplateRelease(pTemplate); } return iResult; }
Что только что произошло. (1) Грань возможностей CompileFile (чтение + компиляция + рендеринг + локализация ошибки по смещению — комментарий в шапке) — это всё, что могла дать линия «переиспользовать шаблонизатор» — возможности сильно сверх нужного configd {$VAR}. (2) Количественное выражение пересмотра: замыкание шаблонизатора, линкуемое этим образцом, против тридцати строк специализированного раскрытия — функциональности в thirty раз больше за десятикратный размер (цифры размерного профиля — метод главы 131) — плохая сделка. (3) Но пересмотр не отрицает ценность альтернативы: когда шаблон конфигурации дорастает до условий и циклов ({$if env=prod}) — вернуться к линии переиспользования — точка расширения оставлена.
Список тестовых файлов и подключение к шлюзу
Тестовые активы configd комплектуются по четырём квадрантам главы 133 (подаются тем же PR, что и реализация — тесты не довесок, а определение поставки): позитив (test_config.c — по позитивному случаю на каждый из шести UC + три вектора слияния + позитивный случай шаблона — на файлах ожиданий); гонки (test_config_threads.c — тест смешанных состояний горячей замены: поток загрузки с чередованием хорошей и плохой конфигураций × 4 потока запросов × сто тысяч согласованных чтений — утверждение нуля смешанных состояний); негатив (векторы с конфликтом типов / неопределённой переменной / незакрытым шаблоном / нарушением валидации — точное совпадение сообщений об ошибках с путём или именем переменной); OOM (test_config_oom.c — конвейер загрузки по каждой точке выделения: отказ в любой точке не трогает старый снимок + ноль следов — ротатор главы 134 применяется напрямую). Подключение к шлюзу: все тесты висят на поле tests записи модуля configd (глава 130) — build.py --suite configd автоматически гоняет двойную колею; векторы слияния как файлы данных висят в манифесте рядом с полем benchmarks — файлы ожиданий живут одной жизнью с тестами. Смысл этого списка: следующая эволюция configd (многослойное слияние / распределённый watch) после любой правки строки одним прогоном узнаёт, не сломан ли контракт — тестовые активы — память проекта.
Ретроспектива: методологический осадок главы реализации
После завершения реализации configd поставим ретроспективу рядом с главами 137/142/143 — три новых урока, специфичных для главы реализации: файлы ожиданий прежде (выведенные вручную векторы становятся машинно проверяемыми ожиданиями — продукт упражнений проектной главы напрямую становится тестовым активом реализации — стык артефактов двух глав); определение завершения слоя — зелёные тесты (между «код написан» и «слой завершён» стоят тесты — каждый слой сразу по реализации получает свой юнит-тест, у самого длинного пути (гонки) всегда проверенный фундамент); пересмотры оставляют след (шаблон: переиспользование → специализация; извлечение: макрос → массив — два отступления от проекта вместе с основаниями записаны в эту главу — прослеживаемое отступление — не дрейф, а запись эволюции — инженерный смысл пункта «решения можно пересматривать» главы 139). Вместе с накопленным опытом Проектов 1, 2, 4 и 5 — двадцать инженерных аксиом продолжат проверку в последних двух проектных главах (140/141).
140.4契约
- Семь слоёв конвейера: каждый слой — чистое преобразование данных, отказ атомарен; определение завершения слоя — зелёные тесты, а не написанный код.
- Семантика слияния: объекты рекурсивно / массивы целиком — семантика закреплена тремя группами файлов ожиданий; порядок многослойного слияния — параметр загрузки.
- Три границы шаблона: только переменные окружения (источник из белого списка) / без рекурсии (продукт раскрытия не возвращается в цикл) / неопределённая — ошибка (локализация по имени переменной).
- Момент раскрытия: после слияния, до валидации — переопределение приоритетнее внедрения, валидация по финальным значениям.
- Валидация как данные: массив правил «путь + тип + ограничение»; новое поле — одна строка; формат ошибки «путь: сообщение».
- Извлечение как декларация: массив «путь + целевой слот» — обходимо и тестируемо (инженерное основание отказа от макросов).
- Три элемента передачи: неизменяемый снимок + подсчёт ссылок + атомарная замена указателя — целиком согласованно в любой момент (доказано тестом гонки).
- Линия безопасности горячей замены: неудача валидации новой конфигурации — не заменять + тревога — плохая конфигурация не вредит работающему сервису.
- Ноль следов отказа: неудача загрузки в любой точке не производит видимого изменения состояния (старый снимок продолжает работать).
- Записи пересмотров: переиспользование → специализация (шаблон, размер) / макрос → массив (извлечение, тестируемость) — исполнение пунктов проектной главы.
Разбор реализации: исходник главного входа загрузки
Главный вход конвейера загрузки — последовательное соединение семи слоёв и шлюза замены (полная программа над учебными образцами, разбор ключевого сегмента):
/* загрузка: источники -> разбор -> слияние -> шаблон -> валидация -> извлечение -> замена (менять только после полного успеха) */ bool cfg_reload(cfgctx* C) { xvalue* Default = NULL; xvalue* Env = NULL; cfgsnap* Fresh = NULL; bool Ok = false; /* 1-2 источники и разбор: каждый слой читается и разбирается отдельно (атомарность главы 32) */ if ( !cfg_load_layer(C, &C->DefaultSrc, &Default) || !cfg_load_layer(C, &C->EnvSrc, &Env) ) { goto Done; /* отказ: указатель не тронут — ноль следов */ } /* 3 слияние: на рабочей копии (Env перекрывает Default) */ if ( !cfg_merge(Default, Env) ) { goto Done; } /* 4 шаблон: после слияния, до валидации (три границы внутри cfg_expand) */ if ( !cfg_expand_tree(Default) ) { goto Done; } /* 5 валидация: один проход по массиву правил (отказ с путём) */ if ( !cfg_validate(Default, kRules, sizeof(kRules) / sizeof(kRules[0])) ) { goto Done; } /* 6 извлечение: чистое копирование по массиву слотов (сюда только после полного успеха) */ Fresh = cfg_build_snapshot(Default, kSlots, sizeof(kSlots) / sizeof(kSlots[0])); if ( Fresh == NULL ) { goto Done; } /* 7 замена: атомарный указатель — читатели немедленно видят новое */ cfg_atomic_swap(&C->Current, Fresh); Ok = true; Done: xrtValueRelease(Env); xrtValueRelease(Default); return Ok; }
Пункты разбора. (1) Порядок семи шагов — конвейер проектной главы — шаг 7 — единственное видимое изменение состояния (первые шесть целиком на локальных объектах — отказ значит отбрасывание, глобальный уровень не замечает). (2) Унифицированная очистка через goto Done (уничтожение value обоих слоёв) — облик одно-выходной дисциплины в форме конвейера. (3) cfg_merge(Default, Env) сливает прямо на Default — отсутствие разделения гарантируется тем, что Default каждый раз разбирается заново (неявная форма паттерна рабочей копии — явное клонирование понадобится лишь при кэшировании многих источников). (4) Fresh после успеха переходит в глобальное владение (после swap принимает подсчёт ссылок) — разовая точка передачи владения.
Типичные ошибки
Симптом: массив слоя окружения слился со слоем умолчаний поэлементно (хотели замену целиком) — или объект заменён целиком (хотели рекурсию).
Причина: ветвь слияния смотрела только «значение существует», а не «обе стороны — объекты» — массив, прошедший через ветвь замены, случаен-верен, объект через ту же ветвь уже ошибочен (или наоборот). Условие ветвления — конъюнкция типов обеих сторон — объекта на одной стороне недостаточно.
xvalue* Bv = xrtValueObjectGet(Base, Key); if ( Bv != NULL ) { xrtValueObjectEdit(Base, Key, Ov); /* замена всегда: вложенный объект теряет ключи-братьев слоя умолчаний */ }
if ( (Bv != NULL) && xrtValueIs(Bv, XVALUE_OBJECT) && xrtValueIs(Ov, XVALUE_OBJECT) ) { cfg_merge(Bv, Ov); /* рекурсия только при объектах с обеих сторон */ } else { xrtValueObjectEdit(Base, Key, Ov); /* остальное заменяется целиком (включая массивы) */ }
Симптом: сервер с проваленной валидацией стартовал с наполовину извлечённой структурой — часть полей новые, часть старые.
Причина: перепутан порядок извлечения и валидации (извлекать и валидировать вперемешку — при отказе на третьем поле первые два уже записаны). Правильный порядок: извлечение начинается только после полного успеха валидации — извлечение есть чистое копирование, у него не должно быть ветви отказа.
for ( each rule ) { if ( !check(rule) ) { return false; } /* отказ */ extract(rule); /* но предыдущие уже записаны в структуру */ }
for ( each rule ) { if ( !check(rule) ) { return false; } } for ( each slot ) { extract(slot); } /* копирование только после полного успеха — чистая фаза */
Симптом: эксплуатация ошиблась в одном значении — продакшен-сервис немедленно меняет поведение на плохой конфигурации (не может соединиться с базой) — «ошиблись» превращается в «инцидент».
Причина: цикл горячей замены принял «загрузка завершена» за условие замены — пропущен шлюз валидации. Условие замены — полное прохождение (разбор / слияние / шаблон / валидация) — неудача любой ступени: держать старый снимок + тревога.
if ( load(&tree) ) { swap(build_snapshot(&tree)); } /* а валидация? */
if ( load(&tree) && validate(&tree, kRules) ) { swap(build_snapshot(&tree)); /* менять только после полного успеха */ } else { alert("config reload rejected"); /* старый снимок продолжает работать */ }
Рекомендуемый формат файла ожиданий для векторов слияния (инженеризация упражнения 1): сегмент входа (JSON слоя умолчаний + JSON слоя окружения — два блока кода); сегмент ожидания (JSON результата слияния — один блок); сегмент ошибки (пусто или «путь: сообщение»). Три сегмента делают тестовый драйвер предельно простым (прочитать три сегмента, прогнать слияние, сверить результат и ошибку) — вектор — это данные: добавление случая не трогает драйвер, только добавляет файл. Отношение этого формата к тестовым формам главы 133: файлы ожиданий — форма данных позитивных и негативных случаев — разделение тестового кода (драйвера) и данных случаев (файлов векторов) — признак зрелости тестовой инженерии (случаи растут намного быстрее правок драйвера). Три группы векторов configd стартуют в этом формате — дальше каждое изменение семантики слияния сначала добавляет вектор, потом правит реализацию (миниатюрная демонстрация вектор-ориентированной разработки).
Практические задания
Напишите три группы векторных тестов слоя слияния (ожидания, выведенные вручную в главе 138); для слоя шаблона — три негативных случая: неопределённая переменная / незакрытый шаблон / вложенная ссылка. Критерий приёмки: шесть тестов зелёные, сообщения об ошибках содержат локализацию (путь или имя переменной).
threads-форма: поток загрузки циклически заменяет (чередуя хорошую и плохую конфигурации) + 4 потока запросов, каждый с Ref, делают по сто тысяч согласованных чтений (за чтение — два поля с проверкой одного источника). Критерий приёмки: ноль смешанных состояний; во время плохой конфигурации читается только старое значение (счёт тревог верен).
Реализуйте по дополнительному проекту HTTP главы 139: первый старт обязан упасть / отказ обновления — деградация / оффлайн-режим only-if-cached — добавьте локальный mock-эндпойнт и протестируйте три момента. Критерии приёмки: поведение трёх моментов совпадает с проектом; во время деградации снимок не меняется; путь кэш-попадания идёт через ONLY-режим главы 101.
Место configd (две главы: проектирование и реализация) в последовательности тома 13: Проект 1 logstat (инструмент — одноразовый) → Проект 2 configd (сервис — долгоживущее состояние и конкурентная передача) → Проект 4 xdl (сетевой инструмент — удалённая неопределённость) → Проект 5 pushd (долгие соединения — веерная рассылка и конечные состояния) → Проект 6 webserv (полный стек). Измерение долгоживущего состояния, введённое configd, — вторая ступень последовательности — оно превращает «управление жизненным циклом» из лозунга в протокол (три элемента передачи), а «конкурентную корректность» — из дисциплины блокировок в структурное свойство (неизменяемость). Последующие проекты накладывают сеть и веерную рассылку на этот фундамент — каждая ступень стоит на приёмке предыдущей. Ретроспектива разделения двух глав: проектная глава даёт список решений (сверяемый), глава реализации — приземление и пересмотры (прослеживаемые) — шаблон «проект / реализация в двух главах» — зрелая форма проектного обучения — Проект 5 (pushd) слит в одну главу, потому что его проектные решения уже оплачены в четырёх главах xws (109-112) — граница между переиспользованием и созданием учебного материала проходит именно в таких суждениях.
Финальная страница главы реализации — состояние шести критериев приёмки и место доказательств: (1) случаи шести UC зелёные (раздел тестового списка); (2) три вектора слияния зелёные (раздел слияния + файлы ожиданий); (3) гонка: сто тысяч чтений, ноль смешанных состояний (рассуждение раздела снимков + threads-тест); (4) ошибки валидации с путями (формат раздела валидации); (5) OOM по точкам без следов (раздел послойной атомарности отказов); (6) неудачная загрузка без утечки (разбор главного входа — шаг 7, единственное видимое изменение). Все шесть отмечены, у каждого доказательства свой раздел — эту страницу можно вклеить в описание PR: ревьюер идёт по карте.
Итоги
| Тема | Кратко |
|---|---|
| Позиция главы реализации | приземление решений + построчная сверка + два пересмотра со следами |
| Ритм разработки | снизу вверх по слоям; завершение слоя = зелёные тесты |
| Ветвь слияния | рекурсия только при объектах с обеих сторон; остальное заменяется целиком (включая массивы) |
| Специализация шаблона | тридцать строк раскрытия: белый список / без рекурсии / неопределённая — ошибка |
| Валидация как данные | массив правил (путь + тип + ограничение); ошибка «путь: сообщение» |
| Извлечение как декларация | массив слотов — тестируемо (отказ от макросов) |
| Дисциплина фаз | извлечение только после полного успеха валидации (чистое копирование без ветви отказа) |
| Передача | неизменяемый снимок + Ref + атомарная замена указателя — машинное доказательство тестом гонки |
| Линия безопасности горячей замены | провал валидации — снимок не меняем + тревога — плохая конфигурация не вредит сервису |
| Итог сверки | шесть критериев приёмки пройдены — configd готов к поставке |
📖 Справочники API в этой главе:value (159 записей)template (51 записей)core (25 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки