Глава 3 / 51
Том 1 · Старт и ментальная модель

Ментальная модель: поколения, lease и передача без обёрток

Это самая важная глава книги. Любое поведение XServer — почему горячая перезагрузка безопасна, почему старые соединения не рвутся, что нельзя делать в колбэках — выводится из трёх понятий этой главы.

Обзор

Позиционирование: бизнес-кода здесь нет — ставим «концептуальную операционную систему». Критерий готовности: с закрытой книгей вы способны объяснить коллеге, что происходит с обрабатываемыми запросами в момент правки кода. Глава также снимает интригу упражнения главы 2 — почему после рестарта появляется gen 2.

3.1 Введение: три вопроса

Впервые встречая «компиляцию C в рантайме», люди обычно задают три вопроса точно в цель:

  • Когда мой код становится машинным? — При старте и при каждой горячей перезагрузке, встроенный TCC компилирует его в нативный код; дальше исполнение ровно такое же быстрое, как у прекомпилированного C (routebench меряет его в одной лиге с Go).
  • Кому принадлежат соединения, слушатели, таймеры? — Хосту. Скрипт лишь «заимствует» их внутри колбэков.
  • В момент правки кода что происходит с запросами в работе? — Сосуществуют два поколения; старые запросы дорабатываются старым кодом до конца. Это и есть герой главы.

Строка script loaded ... gen 1 из лога главы 2 уже показала вход: поколение. Теперь разберём его до конца.

3.2 Три понятия

Понятие 1: поколение

Каждая завершённая загрузка скрипта — старт или горячая перезагрузка — создаёт новое поколение. Поколение = полный рантайм скрипта: результат компиляции, статические переменные, всё, что построил ServiceInit. Так что gen 2 после рестарта — не загадка: номер поколения монотонно растёт внутри процесса, а рестарт — просто «замена, на которой старого поколения не было».

Слот слушателя —— стабильный вход на порту 9080 │ pData слота всегда указывает на «текущее поколение» ▼ ┌──────────────┐ новые соединения входят │ текущее gen2 │ ◀───────────────── └──────────────┘ ▲ переключение (атомарно) ┌──────────────┐ существующие соединения продолжают │ старое gen1 │ ◀───────────────── └──────────────┘ │ уходит последнее соединение (ссылки обнулились) ▼ GC-поток: безопасно разрушает скрипт → драйвер → конфигурацию

Понятие 2: lease

Каждый запрос / соединение / таймер, входящий в обработку, берёт lease (ссылку) на «поколение, в котором начал работать». Lease гарантирует: пока на старом поколении остаётся незавершённая работа, его код и данные не освобождаются. В этом механическая суть «правишь код — соединения живут»: не трюк отложенного уничтожения, а семантическая гарантия подсчёта ссылок.

Понятие 3: утилизация при нуле ссылок

Когда ссылки старого поколения обнуляются, выделенный GC-поток разрушает его в фиксированном порядке: ServiceUnit (очистка скрипта) → состояние TCC → рантайм драйвера → дерево конфигурации. В тестовом наборе XServer есть специальный трёхцикловый soak со сменой поколений 8080↔8081: все старые поколения утилизированы, ноль утечек, ноль падений. GC-код писать не нужно — но знать о нём нужно, иначе непонятно, почему передача состояния в главе 28 устроена именно так.

Три понятия одной фразой: поколение — единица изоляции, lease — механизм защиты, ноль ссылок — момент утилизации. В ближайшие 40 глав при любом вопросе жизненного цикла возвращайтесь сначала к этой фразе.

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
Симптом
Медленный старт; во время горячей перезагрузки новые запросы встают в очередь.
Причина
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