Суть Читай реальные сниппеты обработчика, middleware, стриминга и проброса deadline, предскажи сбой жизненного цикла и выбери самый рычажный фикс, который senior сделает первым.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min
Баги жизненного цикла диагностируются в коде на горячем пути и в порядке проводки. Прочитай каждый сниппет, предскажи, где он ломается в production, и выбери фикс, к которому senior тянется первым.
Цель
Отработай цикл, который запускаешь в каждом инциденте жизненного цикла: читай проводку или горячий путь, предскажи ломающуюся остановку и тянись к самому рычажному фиксу — порядок, backpressure, сериализация или пробрасываемый deadline.
Сниппет 1 — цепочка middleware
const app = express();app.use(express.json()); // парсер тела, без лимита размераapp.post("/admin/wipe", wipeHandler); // <-- зарегистрирован здесьapp.use(rateLimit({ max: 100 })); // rate limiterapp.use(requireAuth); // auth: 401 без валидного токенаapp.use(errorHandler); // ловит throw'ы ниже по цепочке
Викторина
Completed
Какие проблемы создаёт этот порядок регистрации и каков фикс?
Heads-up Express запускает middleware в порядке регистрации, и ответивший маршрут закорачивает остальное. Зарегистрированное после маршрута для него не запускается — поэтому auth, rate limit и error handler здесь обойдены.
Heads-up Лимитирование на пользователя — улучшение, но несущий баг в том, что маршрут зарегистрирован до всех трёх защитных слоёв, поэтому ни один из них для него не запускается вовсе. Дефект — порядок относительно маршрута.
Heads-up Error handler в Express матчится по 4-аргументной сигнатуре и должен регистрироваться ПОСЛЕДНИМ (снаружи луковицы), чтобы ловить throw'ы всего, что ниже. Поставив его первым, ничего ниже ещё не выполнялось — ловить нечего.
Сниппет 2 — стриминговый экспорт
function exportCSV(rows, res) { res.setHeader("Transfer-Encoding", "chunked"); for (const row of rows) { res.write(toCSVLine(row)); // возвращаемое значение игнорируется } res.end();}
Викторина
Completed
Это проходит все тесты, но даёт OOM в production на одном медленном клиенте. В чём баг и каков корректный переписанный вариант?
Heads-up Chunked корректен для стримингового тела неизвестного общего размера; Content-Length заставил бы сначала забуферизовать весь экспорт. Баг — игнорирование сигнала backpressure от write(), а не фрейминг.
Heads-up res.end() выполняется после завершения цикла, поэтому не обрезает. Дефект в том, что цикл никогда не учитывает false от write(), поэтому память растёт неограниченно под медленным потребителем.
Heads-up Поднятие кучи лишь оттягивает OOM; рост неограничен, потому что производство оторвано от скорости чтения клиента. Учёт backpressure держит память плоской на уровне ~high-water mark независимо от аллокаций.
По мере роста таблицы orders этот эндпоинт деградирует и иногда стопорит все остальные запросы. Какие две остановки жизненного цикла виноваты и каков фикс?
Heads-up SELECT без лимита сам по себе проблема стадии Handle: он тянет больше строк по мере роста таблицы, что затем питает раздутую сериализацию. Обе остановки компаундятся; ограничение запроса — фикс выше по течению.
Heads-up Размер таблицы не влияет на accept-очередь, которая ограничивает ожидающие соединения. Деградация отслеживает размер результата, что указывает на запрос (Handle) и сериализацию (Serialize), а не на приём соединений.
Heads-up 204 означает отсутствие тела, что сломало бы контракт эндпоинта — клиентам нужны заказы. Фикс — ограничить и пагинировать тело, а не выбрасывать его; статус-код для успешного чтения корректен.
Сниппет 4 — deadline, который не пробрасывается
async function getProfile(userId) { // каждый клиент ставит свой фиксированный таймаут 1с; ничего не пробрасывается const user = await usersClient.get(userId, { timeoutMs: 1000 }); const prefs = await prefsClient.get(userId, { timeoutMs: 1000 }); const feed = await feedClient.get(userId, { timeoutMs: 1000 }); return { user, prefs, feed };}
Викторина
Completed
Точка входа обещает SLA 1 с, но эта функция может занять ~3 с, когда downstream'ы медленные. Почему и какая форма корректна?
Heads-up Фиксированные per-hop таймауты не композируются. Последовательные вызовы, каждый до 1 с, дают ~3 с; изолированные таймауты не могут навязать бюджет на весь запрос — только пробрасываемый deadline может.
Heads-up Статическое деление тратит бюджет впустую — быстрый первый вызов должен оставлять больше времени поздним. Пробрасываемый deadline с min(local, remaining) адаптируется к фактически прошедшему времени, а не к фиксированному делению 1/N.
Heads-up Конкурентность ограничивает итог самым медленным одиночным вызовом (~1 с), что помогает, но без пробрасываемого deadline медленный hop всё ещё может зависнуть за бюджет, а fan-out тогда вскроет амплификацию хвоста. Нужны и конкурентность, и deadline.
Итог
Каждый инцидент жизненного цикла читается в проводке или на горячем пути: middleware, зарегистрированный после маршрута, не запускается, поэтому порядок — граница безопасности; цикл write(), игнорирующий false, даёт OOM под медленными клиентами, поэтому используй pipeline(); запрос без лимита плюс generic JSON.stringify блокирует loop, поэтому пагинируй и используй schema-сериализатор; а фиксированные per-hop таймауты не композируются, поэтому пробрасывай deadline с min(local, remaining). Диагностируй остановку, примени самый рычажный фикс, затем проверь под той же нагрузкой.