Ориентация главы

Глава 139 зафиксировала проект configd: семислойный конвейер (источники → разбор → слияние → шаблон → валидация → снимок → запросы), поэтапная модель «домен загрузки в value + домен снимка в структуре», протокол передачи «неизменяемый снимок + атомарный указатель», явно объявленная семантика слияния, три границы безопасности шаблона. Эта глава — глава реализации: каждое решение приземляется построчно и построчно сверяется: шесть критериев приёмки проектной главы (у каждого из шести UC позитивные и негативные случаи / векторы семантики слияния / гонка горячей замены / валидация с путями / OOM по каждой точке / ноль следов при неудачной загрузке) отмечаются по мере завершения реализации. «Реализация» в проектной главе — не «переписать проект в код», а проверка проекта под настоящими ограничениями кода — глава покажет два взаимных уточнения проекта и реализации (слой шаблона меняет «переиспользовать шаблонизатор» на «специализированное раскрытие» — вынудил бюджет размера; извлекатель меняет «макрос» на «декларативный массив + цикл» — вынудила тестируемость), и это само по себе исполнение пункта проектной главы «решения можно пересматривать».

140.1引入

Стартовая точка реализации — файлы приёмки прежде исходника: три группы векторов слияния, выведенные вручную в упражнениях главы 139 (вложенное переопределение / замена массива / конфликт типов), записываются в файлы ожиданий тестового каталога — это машинное определение семантики слияния; позитивные и негативные случаи шести UC выписываются списком — после реализации каждой строки прогон. Ритм разработки по семи слоям снизу вверх: сперва разбор и слияние (чистые преобразования данных — легче всего тестировать), затем шаблон и валидация (слой семантики), потом снимок и передача (слой конкурентности), и наконец слой источников (файл/HTTP — граница IO). Каждый слой сразу по реализации получает свои тесты — определение завершения слоя — «тесты зелёные», а не «код написан». Этот ритм даёт самому длинному пути (гонке горячей замены) всегда проверенный фундамент.

140.2概念

Карта слоёв реализации

На какую предыдущую главу ложится каждый из семи слоёв этой главы и сколько кода написано самим — карта границы между сборкой и собственным кодом:

1Слой источников (~80 строк своих)чтение файла (xfile, глава 45) / вытягивание по HTTP (главы 98-99)
2Слой разбора (0 строк своих)один шаг xrtJsonParse — целиком глава 32
3Слой слияния (~40 строк своих)рекурсия cfg_merge — этот раздел
4Слой шаблона (~35 строк своих)cfg_expand — продукт пересмотренного решения
5Слой валидации (~50 строк своих)массив правил + цикл
6Слой извлечения (~45 строк своих)массив слотов + цикл
7Слой передачи (~30 строк своих)Ref + атомарный указатель — главы 5/9

Двести восемьдесят строк на центр конфигурации с горячей заменой и гарантиями против гонок — вот количественное выражение ценности слоистой библиотеки: каждый слой — тема чьей-то главы, проект пишет только межслойный клей и бизнес-семантику (слияние / шаблон / правила — то, что действительно различается от проекта к проекту). Взаимное подтверждение с «< 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; здесь перепроверяется как стартовая точка реализации):

examples/data/json/main.c源码 ↗





































 */

#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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/data/json/main.c -lws2_32 -liphlpapiname = xrt{ "name": "xrt", "features": [ "json", "http" ]}member: codemember: ok{"code":200,"message":"OK"}

Что только что произошло. (1) Это весь слой разбора configd — глава реализации добавила сверху лишь четыре чистых преобразования: слияние / шаблон / валидацию / извлечение — каждый слой проходит девелоперскую проверку на этом атоме (юнит-тесту слоя не нужен полный конвейер). (2) DOM-запрос (ObjectGet берёт по ключу) — ровно словарь спуска по путям слоя валидации — одно семейство API пронизывает каждый слой домена загрузки (исполнение DOM-проекта главы 31 в этом проекте). (3) Выбор из трёх видов использования (комментарий в шапке: DOM/SAX/Writer) в configd: загрузка через DOM (нужны случайный доступ и слияние — дерево обязано быть полным); Writer не нужен (конфигурация — вход, а не выход).

Вторая полная программа: комбинированная граница шаблона и файла

Вторая программа из examples/template/file — материальный носитель альтернативы слоя шаблона из проектной главы (эталон пересмотренного решения этой главы):

examples/template/file/main.c源码 ↗

















 */

#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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/template/file/main.c -lws2_32 -liphlpapiHello Alice

Что только что произошло. (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 принимает подсчёт ссылок) — разовая точка передачи владения.

Типичные ошибки

Ловушка 1: слияние без различения объекта и массива:Ловушка 1: слияние без различения объекта и массива

Симптом: массив слоя окружения слился со слоем умолчаний поэлементно (хотели замену целиком) — или объект заменён целиком (хотели рекурсию).

Причина: ветвь слияния смотрела только «значение существует», а не «обе стороны — объекты» — массив, прошедший через ветвь замены, случаен-верен, объект через ту же ветвь уже ошибочен (или наоборот). Условие ветвления — конъюнкция типов обеих сторон — объекта на одной стороне недостаточно.

错误写法
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);    /* остальное заменяется целиком (включая массивы) */
}
Ловушка 2: извлекатель пишет в структуру посреди загрузки:Ловушка 2: извлекатель пишет в структуру посреди загрузки

Симптом: сервер с проваленной валидацией стартовал с наполовину извлечённой структурой — часть полей новые, часть старые.

Причина: перепутан порядок извлечения и валидации (извлекать и валидировать вперемешку — при отказе на третьем поле первые два уже записаны). Правильный порядок: извлечение начинается только после полного успеха валидации — извлечение есть чистое копирование, у него не должно быть ветви отказа.

错误写法
for ( each rule ) {
	if ( !check(rule) ) { return false; }   /* отказ */
	extract(rule);                           /* но предыдущие уже записаны в структуру */
}
正确写法
for ( each rule ) { if ( !check(rule) ) { return false; } }
for ( each slot ) { extract(slot); }        /* копирование только после полного успеха — чистая фаза */
Ловушка 3: горячая замена заменяет снимок и при провале валидации:Ловушка 3: горячая замена заменяет снимок и при провале валидации

Симптом: эксплуатация ошиблась в одном значении — продакшен-сервис немедленно меняет поведение на плохой конфигурации (не может соединиться с базой) — «ошиблись» превращается в «инцидент».

Причина: цикл горячей замены принял «загрузка завершена» за условие замены — пропущен шлюз валидации. Условие замены — полное прохождение (разбор / слияние / шаблон / валидация) — неудача любой ступени: держать старый снимок + тревога.

错误写法
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-источника

Реализуйте по дополнительному проекту HTTP главы 139: первый старт обязан упасть / отказ обновления — деградация / оффлайн-режим only-if-cached — добавьте локальный mock-эндпойнт и протестируйте три момента. Критерии приёмки: поведение трёх моментов совпадает с проектом; во время деградации снимок не меняется; путь кэш-попадания идёт через ONLY-режим главы 101.

Заключительное сопоставление: место Проекта 2 в последовательности шести проектов

Место 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 записей)

Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки