Ментальная модель: поколения, lease и передача без обёрток
Это самая важная глава книги. Любое поведение XServer — почему горячая перезагрузка безопасна, почему старые соединения не рвутся, что нельзя делать в колбэках — выводится из трёх понятий этой главы.
Обзор
gen 2.
3.1 Введение: три вопроса
Впервые встречая «компиляцию C в рантайме», люди обычно задают три вопроса точно в цель:
- Когда мой код становится машинным? — При старте и при каждой горячей перезагрузке, встроенный TCC компилирует его в нативный код; дальше исполнение ровно такое же быстрое, как у прекомпилированного C (routebench меряет его в одной лиге с Go).
- Кому принадлежат соединения, слушатели, таймеры? — Хосту. Скрипт лишь «заимствует» их внутри колбэков.
- В момент правки кода что происходит с запросами в работе? — Сосуществуют два поколения; старые запросы дорабатываются старым кодом до конца. Это и есть герой главы.
Строка script loaded ... gen 1 из лога главы 2 уже показала вход: поколение. Теперь разберём его до конца.
3.2 Три понятия
Понятие 1: поколение
Каждая завершённая загрузка скрипта — старт или горячая перезагрузка — создаёт новое поколение. Поколение = полный рантайм скрипта: результат компиляции, статические переменные, всё, что построил ServiceInit. Так что gen 2 после рестарта — не загадка: номер поколения монотонно растёт внутри процесса, а рестарт — просто «замена, на которой старого поколения не было».
Понятие 2: lease
Каждый запрос / соединение / таймер, входящий в обработку, берёт lease (ссылку) на «поколение, в котором начал работать». Lease гарантирует: пока на старом поколении остаётся незавершённая работа, его код и данные не освобождаются. В этом механическая суть «правишь код — соединения живут»: не трюк отложенного уничтожения, а семантическая гарантия подсчёта ссылок.
Понятие 3: утилизация при нуле ссылок
Когда ссылки старого поколения обнуляются, выделенный GC-поток разрушает его в фиксированном порядке: ServiceUnit (очистка скрипта) → состояние TCC → рантайм драйвера → дерево конфигурации. В тестовом наборе XServer есть специальный трёхцикловый soak со сменой поколений 8080↔8081: все старые поколения утилизированы, ноль утечек, ноль падений. GC-код писать не нужно — но знать о нём нужно, иначе непонятно, почему передача состояния в главе 28 устроена именно так.
3.3 Разделение труда и передача
Чем владеет хост, чем — скрипт
| Хост (XServer) | Скрипт (ваш C) |
|---|---|
| Читает xs.json, собирает сервисы и слушатели | Бизнес-логика, таблицы маршрутов, обработка данных |
| Разбор протоколов (кадры HTTP/WS, записи TLS) | Построение ответов из разобранного |
| Жизненный цикл соединений и слушателей, GC | Статическое состояние (изоляция по поколениям), ServiceInit/Swap/Unit |
| Компиляция, публикация и откат перезагрузки | Экспорт контрактных символов, подача намерений перезагрузки |
Передача без обёрток
Большинство фреймворков оборачивают соединения и запросы в свои объекты, и вы работаете внутри их абстракции. XServer сознательно ничего не оборачивает: колбэк получает XS_HttpReq — представление над нативными указателями xrt: «голые» указатели транспорта, разобранный заголовок запроса, автомат кадрирования. Вы можете напрямую вызывать любые из 3 030 API xrt: ни скрытых слоёв, ни потерь на адаптацию.
| Обёртки классического фреймворка | Передача представлений в XServer |
|---|---|
| req.headers() через аксессоры | pReq->head — сама разобранная структура xrt |
| res.send() идёт по конвейеру фреймворка | xrtHttp1ResponseWrite + потоковая отправка — каждый шаг ваш |
| Жизненный цикл объектов у фреймворка | Представления действительны только внутри колбэка; правила владения явные |
«Ноль обёрток» — это свобода и ответственность: представление нельзя хранить за пределами колбэка (см. ошибку ①), но и никакой фреймворк-магии между вами и протоколом нет.
3.4 Пример: увидеть поколения
Поколения не требуют специальных инструментов наблюдения — лог старта сам является доказательством. Вернёмся к реальному выводу главы 2:
[xs] engine started, workers=12
[xs] script loaded: .../main.c (2.6 KB gen 1) /* ← родилось поколение 1: скомпилировано 2,6 КБ, номер 1 */
[demo] database opened: .../db\main.db /* ← ServiceInit поколения gen1 работает */
Что только что произошло: TCC скомпилировал 2,6 КБ исходников в нативный код и создал рантайм скрипта «поколение 1»; строка с префиксом [demo] затем доказывает, что ServiceInit самого приложения выполнился на этом поколении. Когда в упражнении главы 2 вы правили route.h и перезапускались, лог показал gen 2 — родилось новое поколение, старый процесс завершился, утилизировать нечего. А горячая перезагрузка (том 4) делает то же самое, пока старое поколение ещё обслуживает: новое публикуется, старое ждёт возврата последнего lease.
3.5 Контракт
- Потоки: колбэки скрипта исполняются на worker-потоках движка (workers=12 — значит, колбэки могут идти конкурентно); ServiceInit / ServiceSwap — на управляющем потоке сборки / reload; ServiceUnit — на GC-потоке. Синхронизацией межпоточного состояния заниматься вам.
- Владение: представления из параметров колбэка (XS_HttpReq, указатели заголовков, представления сообщений) действительны только в стеке колбэка; хотите сохранить — сначала скопируйте. Ресурсы (соединения, слушатели, таймеры) принадлежат хосту, скрипт их лишь заимствует.
- Жизненный цикл: утилизированное поколение не принимает новых запросов; статические переменные изолированы по поколениям — смена поколения даёт совершенно новое статическое состояние, а межпоколенческая передача обязана идти через ServiceSwap (глава 28).
Частые ошибки
- Симптом
- Случайные падения / порча данных, особенно при высокой конкурентности или после горячей перезагрузки.
- Причина
- Представления вроде
pReq->headдействительны только в стеке колбэка; после возврата буферы могут переиспользоваться, а после смены поколений память старого освобождается.
static xhttp1head* G_Head;
XS_RequestResult RequestProc(XS_HttpReq* pReq)
{
G_Head = pReq->head; /* чтение снаружи → UAF *//* забрать нужные значения внутри колбэка */
const char* sUA = ...; /* скопировать строку */
G_iCount++; /* значения выносить можно, указатели нельзя */- Симптом
- Кладут код из недоверенных источников в devfile или предполагают, что падение / выход за границы в скрипте не заденет хост.
- Причина
- Скрипт — нативный код в том же процессе: общее адресное пространство и права, доступ ко всем 3 030 API и системным библиотекам. Отсутствие песочницы — осознанный дизайн.
/* «скрипт упал — хост выжил» — не работает.
* дикая ссылка пробивает весь процесс *//* devfile — только код доверенной эксплуатации;
* возможности, нуждающиеся в изоляции, — в отдельный процесс */- Симптом
- Медленный старт; во время горячей перезагрузки новые запросы встают в очередь.
- Причина
- ServiceInit выполняется последовательно на управляющем потоке сборки / reload; новое поколение обязано завершить Init до публикации.
void ServiceInit(XS_HostInfo* pHost)
{
BigIndexLoad(); /* блокируется на 8 секунд *//* лёгкая инициализация + ленивая загрузка на первом запросе,
* или сборка в фоновом потоке и публикация флага готовности */Упражнения
Запустите демо и отметьте в логе: какая строка объявляет рождение поколения? Какая доказывает, что на нём выполнился ServiceInit? Сколько worker-потоков и что это говорит о верхней грани конкурентности колбэков?
Требование: считать активные соединения и выводить число на /stats. Ответьте: где живёт статическая переменная-счётчик? Когда увеличивать и уменьшать (подсказка: WsOpen/WsClose или EventOpen/EventClose)? Как счётчик переживёт горячую перезагрузку (подсказка: что экспортирует ServiceSwap)?
Не подглядывая, нарисуйте: 10 конкурентных запросов в работе, подаётся горячая перезагрузка — полный таймлайн от «компиляции нового поколения» до «разрушения старого»; отметьте, какое поколение обслуживает каждый запрос, когда возвращается каждый lease и когда вступает GC.
Итоги
| Тезис | Вывод |
|---|---|
| Поколение | Каждая загрузка скрипта создаёт поколение; статики изолированы по поколениям |
| Lease | Запросы / соединения / таймеры держат ссылку на своё поколение, защищая его от освобождения |
| Утилизация при нуле | GC-поток разрушает по порядку: ServiceUnit → TCC → драйвер → конфигурация |
| Разделение труда | Хост: сборка / разбор / жизненный цикл; скрипт: бизнес и состояние |
| Передача без обёрток | Колбэки получают нативные представления xrt; значения выносить можно, указатели нельзя |
- учебник xrt · память и владение — инфраструктура подсчёта ссылок под lease
- учебник xrt · введение в том о конкурентности — модель конкурентности за workers=12