DevNotes Live
6 subscribers
84.2K photos
12K videos
195 files
35.4K links
Автоматический агрегатор IT ресурсов в Telegram (@devnotes_robot)
Информация: https://t.me/devnotes_live/121
Download Telegram
Variadic tuple types — сложные сигнатуры без боли

До variadic tuple types
многие сложные сигнатуры в TypeScript
выглядели как наказание.

Особенно:

👉 curry
👉 compose
👉 middleware
👉 typed event emitter
👉 любые функции с «прокинь аргументы дальше»


Приходилось писать overload на overload
и дублировать типы вручную.


Как было раньше

Обычно появлялись:

👉 overload на overload
👉 ручные tuple-типы
👉 тонны дублирования

Типы быстро превращались
в нечитаемую простыню.

Что изменили variadic tuples

С их появлением стало намного проще
работать с остаточными аргументами на уровне типов.

Например:


type Fn<T extends unknown[]> =
(...args: [...T]) => void


Или собирать сигнатуры:


type Append<Args extends unknown[], Arg> =
[...Args, Arg]



Типы наконец научились нормально работать
с «переменным количеством аргументов».


Почему это важно

На практике это одна из тех TS-фич,
которые реально упростили жизнь библиотекам.

Без variadic tuples:

👉 Redux middleware typings
👉 router APIs
👉 compose/curry utilities

были бы ещё страшнее.

Где начинается тёмная магия

Проблемы начинаются,
когда variadic tuples комбинируют с:

👉 infer
👉 recursive types
👉 conditional types


Типовая система очень быстро
превращается в тёмный лес.


IDE начинает тормозить,
ошибки становятся нечитаемыми,
а compile time — расти.

Главная мысль

Variadic tuple types —
это действительно мощная фича.


Главное —
вовремя остановиться
и не превратить типы в отдельный язык программирования.
Страшная тайна российского айти

✖️ xCode Journal
Please open Telegram to view this post
VIEW IN TELEGRAM
AbortSignal.any() и AbortSignal.timeout(): единая отмена fetch, таймеров и async-операций в production

Promise.race([fetch(), timeout]) часто маскирует проблему: ждать перестали, но работа могла не остановиться. В SPA, SSR, Node.js-сервисах, SDK и API clients это приводит к висящим запросам, таймерам и retry/backoff ниже по стеку.

Собирайте отмену вокруг AbortSignal

AbortSignal.timeout(ms) сам отменится по таймауту.
AbortSignal.any([...signals]) отменится, когда отменится любой входной signal.

Так в один контракт попадают:
* caller отменил операцию
* истек timeout
* клиент закрыл соединение
* сервис уходит в graceful shutdown

const shutdown = new AbortController();

async function loadUser(id: string, opts: { signal?: AbortSignal } = {}) {
const signal = AbortSignal.any([
AbortSignal.timeout(1500),
shutdown.signal,
...(opts.signal ? [opts.signal] : []),
]);

signal.throwIfAborted();

const res = await fetch(`https://api.example.com/users/${id}`, { signal });

await sleep(100, signal); // backoff тоже отменяем

const data = await res.json();
signal.throwIfAborted();

return data;
}


Типичная ошибка

Не останавливайтесь на fetch. Один и тот же signal стоит передавать в retry, polling, очереди, sleep/timer helpers и свои async-функции. Иначе верхний слой "отменился", а нижний продолжает держать ресурсы.

Практические нюансы

* AbortSignal одноразовый: если aborted === true, нужен новый controller или timeout
* AbortSignal.any() - это fan-in, а не fan-out: он не отменяет исходные контроллеры
* смотрите на reason: timeout часто дает TimeoutError, abort - AbortError или ваш reason
* при обертках над setTimeout, stream, listener или socket чистите ресурсы при abort

Вывод:
Отмена должна быть частью контракта async-функции, а не локальным Promise.race на краю системы.
Graceful shutdown в Node.js под Kubernetes: drain keep-alive соединений, SIGTERM и защита от 502 при rolling update

При rolling update старый Pod может уже получить SIGTERM, но Ingress, kube-proxy или external LB еще слать в него трафик. Частая ошибка - сразу делать process.exit() и рвать активные запросы.

Что ломается в production

Особенно больно с keep-alive: балансировщик держит открытое TCP-соединение к Pod и переиспользует его, пока приложение уже закрывается. Итог - 502, 503, connection reset на деплое.

Правильный порядок:
* уронить readiness
* дать балансировке убрать Pod
* закрыть HTTP-сервер
* drain'ить idle keep-alive
* дождаться активных запросов

Node.js shutdown

let shuttingDown = false;

app.get('/readyz', (req, res) => {
res.sendStatus(shuttingDown ? 503 : 200);
});

async function shutdown(signal) {
if (shuttingDown) return;
shuttingDown = true;

await new Promise(r => setTimeout(r, 10_000));

server.close(err => {
if (err) process.exit(1);
process.exit(0);
});

server.closeIdleConnections?.();

setTimeout(() => {
server.closeAllConnections?.();
process.exit(1);
}, 25_000);
}

process.on('SIGTERM', () => shutdown('SIGTERM'));


Важная деталь: во время drain delay /readyz уже отвечает 503, но обычные запросы еще обслуживаются.

Kubernetes-настройки

terminationGracePeriodSeconds: 40
readinessProbe:
httpGet:
path: /readyz
port: 3000
periodSeconds: 5
failureThreshold: 1
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 10"]


Практический совет: держите terminationGracePeriodSeconds больше, чем preStop + app drain delay + max request time + запас.

Типичная ошибка

server.close() недостаточно как единственная мера. Он останавливает новые соединения, но idle keep-alive сокеты лучше закрывать явно через closeIdleConnections(). closeAllConnections() используйте только как аварийный fallback.

Вывод:

Graceful shutdown - это координация Node.js runtime, readiness, keep-alive и поведения балансировщика, а не один handler на SIGTERM.
AsyncLocalStorage в Node.js: request-scoped контекст для логов, трассировки и транзакций без prop drilling

В production это нужно в Node.js-сервисах, SSR, API integration и backend-for-frontend слоях, где requestId, traceId, tenant или транзакция должны проходить через async-код. Частая ошибка - протаскивать ctx через десятки методов или, наоборот, прятать в нём бизнес-состояние.

Как это выглядит

AsyncLocalStorage использует async_hooks и привязывает store к async execution flow: promise, timeout, I/O callback и большинству стандартных API Node.js.

type Ctx = { requestId: string; traceId?: string; tx?: unknown };

const als = new AsyncLocalStorage<Ctx>();

app.use((req, _res, next) => {
als.run({
requestId: req.headers['x-request-id']?.toString() ?? randomUUID(),
traceId: req.headers.traceparent?.toString(),
}, next);
});

const getCtx = () => als.getStore();

function logInfo(msg: string, meta = {}) {
const c = getCtx();

logger.info({
requestId: c?.requestId,
traceId: c?.traceId,
...meta,
}, msg);
}


Теперь сервисный код не знает про HTTP middleware и req, но логи автоматически получают correlation metadata.

Транзакции без протаскивания tx

Для DB слоя можно делать withTransaction(), который запускает als.run({ ...ctx, tx }, fn), а getDbExecutor() возвращает ctx.tx ?? db. Trade-off хороший: меньше шума в API сервисов, но граница ответственности остаётся инфраструктурной, а не бизнесовой.

Где границы

* не заменяйте аргументы функции: бизнес-данные передавайте явно;
* не кладите в store req, res, большие payload или ORM graph;
* context не пересекает process boundary: worker threads, очереди, cron jobs и другие сервисы требуют явной передачи ids;
* нестандартные callback-based библиотеки могут разорвать async chain.

Практическое правило: используйте AsyncLocalStorage для логов, tracing, tenant/user metadata, аудита и текущей транзакции, а не для скрытого глобального состояния.

Вывод:
AsyncLocalStorage полезен, когда request-scoped инфраструктурный контекст улучшает observability и DX, но не размывает явные границы бизнес-логики.
Совет на ближайшие годы — изучайте ВАЙБ-КОДИНГ

ИИ уже пишет код, чинит баги, генерирует тесты, документацию и помогает запускать продукты быстрее, чем это делали классические команды разработки. И это уже не "будущее когда-нибудь", а реальность, которая меняет рынок уже сегодня

И те, кто научится вайбкодить сейчас, будут увереннее конкурировать на рынке и зарабатывать больше тех, кто по-прежнему делает всё вручную.

Стартовать с нуля поможет канал Вайб-кодинг. Там ребята круглосуточно мониторят более 320 российских и зарубежных источников и публикуют только главное: релизы, инструменты, гайды, курсы и практические кейсы.

Подписывайтесь, нас уже 45 тысяч: @vibecoding_tg
Диагностика event loop stalls в production через monitorEventLoopDelay и async_hooks без гадания по p99 latency

Event loop stall важен там, где Node.js обслуживает API, воркеры, очереди и webhooks. Типичная ошибка - видеть рост latency и сразу винить БД, не проверив, не заблокирован ли сам JS thread.

Первый сигнал - monitorEventLoopDelay

perf_hooks.monitorEventLoopDelay() дает histogram задержек event loop и дешево работает как постоянная production-метрика.

import { monitorEventLoopDelay } from 'node:perf_hooks';

const h = monitorEventLoopDelay({ resolution: 20 });
h.enable();

setInterval(() => {
const p99 = h.percentile(99) / 1e6;

if (p99 > 100) {
log.warn({
event: 'event_loop_stall',
p99Ms: p99.toFixed(1),
maxMs: (h.max / 1e6).toFixed(1),
ctx: als.getStore()
});
}

h.reset();
}, 10_000).unref();


Если p99 delay внезапно стал 200-500ms+, процесс не успевает выполнять callbacks вовремя: CPU-bound JS, sync I/O, тяжелый JSON.parse, regexp, crypto, zlib или GC pressure.

Контекст дает async_hooks

monitorEventLoopDelay отвечает на вопрос "есть ли stall", но не говорит "кто виноват". Для production связывайте AsyncLocalStorage с request id, route, job name, tenant или queue message id.

Практический пример: при stall логируйте route/job из ALS, а не только глобальный p99. Так можно увидеть, что задержки появляются рядом с POST /reports или конкретным batch job.

Что отправлять в метрики

* event_loop_delay_p50/p95/p99/max
* CPU user/system
* RSS и heap used
* active handles/requests
* route/job labels - осторожно, без высокой кардинальности

Предупреждение: request id кладите в логи или трейсы, но не в метрики.

Важный нюанс

AsyncLocalStorage не профилирует CPU и не покажет строку кода. Он сохраняет контекст. Для строки нужен sampling profiler: inspector, --cpu-prof, perf/eBPF, Clinic или APM profiler.

Нормальный flow: постоянно держать дешевый delay histogram, при threshold писать структурированный лог, при повторении включать profiler на короткое окно.

Вывод:
Event loop stall надо диагностировать в два шага: сначала измерить задержку loop, затем привязать ее к бизнес-контексту и только после этого идти в CPU profiling.
AsyncLocalStorage в production: как не потерять traceId через очереди, таймеры и connection pooling

AsyncLocalStorage надежно держит контекст внутри await, промисов и I/O. Но в production traceId чаще теряется на границах: очереди, scheduler'ы, worker'ы и connection pool.

Базовый слой
Создавайте контекст на входе в систему: HTTP, consumer, cron tick. Не ждите, что ALS сам перенесет его через процесс или брокер.

const als = new AsyncLocalStorage();

const withTrace = (traceId, fn) =>
als.run(Object.freeze({ traceId }), fn);

const getTraceId = () => als.getStore()?.traceId;


Очереди
Для in-memory очереди сохраняйте store в момент enqueue и восстанавливайте при выполнении.

function enqueue(payload) {
queue.push({ payload, store: als.getStore() });
}

function processJob(job) {
return job.store
? als.run(job.store, () => handleJob(job.payload))
: handleJob(job.payload);
}


Для Kafka, RabbitMQ, BullMQ, SQS ALS не участвует. Практический совет: кладите traceId в headers или payload и считайте это частью контракта сообщения.

Таймеры и scheduler'ы
Таймер, созданный внутри контекста, обычно его сохранит. Опасность появляется, когда callback зарегистрирован в одном месте, а вызван позже из scheduler'а.

Для cron/background задач создавайте новый traceId на каждый tick. Иначе получите "вечный" traceId процесса или случайный traceId старого запроса.

Connection pooling
Типичная ошибка - записывать traceId в client или connection object.

client.traceId = getTraceId(); // плохо


Connection живет дольше запроса и переиспользуется другим request'ом. Логируйте query через текущий ALS-контекст, а trace в БД передавайте на уровне конкретной операции или транзакции, например через transaction-local setting.

Вывод:
AsyncLocalStorage работает хорошо только внутри async chain, а на production-границах traceId нужно явно фиксировать, передавать и восстанавливать.
Graceful shutdown в Kubernetes: почему одного SIGTERM мало для keep-alive, HTTP/2 и фоновых задач

В production Pod редко умирает мгновенно: ingress, kube-proxy, LB и клиенты с keep-alive могут ещё слать трафик в terminating Pod. Частая ошибка - считать, что server.close() уже дождался всех клиентов и задач.

Что реально нужно drain'ить

* HTTP/1.1 keep-alive соединения
* HTTP/2-сессии и активные stream'ы
* фоновые задачи: BullMQ, RabbitMQ, Kafka, cron, batch jobs

Иначе на rolling deploy получаются редкие 502/499, оборванные stream'ы и повторная обработка сообщений.

Staged shutdown для Node.js

Практический порядок:

* включить режим draining
* /readyz начинает отдавать 503
* дать ingress/LB несколько секунд убрать Pod из ротации
* остановить новые TCP-соединения
* закрыть idle keep-alive
* дождаться активных запросов и задач в рамках бюджета

process.on('SIGTERM', async () => {
draining = true;

await sleep(5000);

server.close();
server.closeIdleConnections?.();

h2sessions.forEach(s => s.goaway());

await waitInFlight(25_000);

workers.abort();

server.closeAllConnections?.();
process.exit(0);
});


Важное предупреждение

server.close() не означает "все клиенты ушли". Он только перестаёт принимать новые соединения. Уже открытые keep-alive соединения нужно отдельно закрывать через closeIdleConnections(), а в крайнем случае - через closeAllConnections().

HTTP/2 и фоновые задачи

Для HTTP/2 нельзя просто рубить socket: одна TCP-сессия содержит много stream'ов. Сначала отправляйте GOAWAY, чтобы запретить новые stream'ы, затем ждите завершения активных.

Для workers на SIGTERM нужно остановить получение новых задач, не запускать новые cron-итерации, дождаться текущих и уметь отменять долгие операции через AbortController.

Вывод:
Graceful shutdown - это не обработчик SIGTERM, а управляемый drain всего, что может продолжать работу после удаления Pod из ротации.
🔍Тестовое собеседование с руководителем Frontend-разработки в этот четверг

18 июня(в четверг!) в 19:00 по мск приходи онлайн на открытое собеседование, чтобы посмотреть на настоящее интервью на Middle Frontend-разработчика.

Как это будет:
📂 Виталий Черков, руководитель группы Frontend разработки с опытом 8+ лет, будет задавать реальные вопросы и задачи разработчику-добровольцу
📂 Виталий будет комментировать каждый ответ респондента, чтобы дать понять, чего от вас ожидает собеседующий на интервью
📂 В конце можно будет задать любой вопрос Виталию

Это бесплатно. Эфир проходит в рамках менторской программы от ШОРТКАТ для Frontend-разработчиков, которые хотят повысить свой грейд, ЗП и прокачать скиллы.

Переходи в нашего бота, чтобы получить ссылку на эфир →
@shortcut_front_bot

Реклама.
О рекламодателе.
Please open Telegram to view this post
VIEW IN TELEGRAM
Дизайн бренда «Московское чаепитие»

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

: Ирина Угай
Forwarded from Daily Coding 🔥
📖100 Go Mistakes and How to Avoid Them
🖋Harsanyi Teiva 2022

Книга «100 ошибок в Go и как их избежать» показывает, как заменить распространенные проблемы в программировании на Go идиоматичным и выразительным кодом. Вы изучите десятки интересных примеров и кейсов и научитесь выявлять ошибки, которые могут возникнуть в ваших собственных приложениях. Автор книги Тейва Харсаньи систематизирует методы предотвращения ошибок по удобным категориям — от типов данных и строк до параллелизма и тестирования.

💾 Скачать книгу

Daily Coding #книги #Go & Max
Undici в production: почему исходящие HTTP-запросы должны иметь пулы, deadline и собственный backpressure

fetch() в Node.js использует Undici, но в production часто нужен явный контроль: сколько socket'ов держим, сколько ждём upstream и что делаем, если входящий трафик быстрее внешнего API. Типичная ошибка - создавать запросы без лимитов и надеяться, что пул всё спасёт.

Пул создаётся на уровне приложения

Не заводите клиент внутри handler'а. Для критичных интеграций держите Pool per-origin и переиспользуйте его:

import { Pool } from 'undici'

const api = new Pool('https://api.example.com', {
connections: 32,
pipelining: 1,
connectTimeout: 2_000,
headersTimeout: 3_000,
bodyTimeout: 10_000
})

async function callApi(payload) {
const { statusCode, body } = await api.request({
method: 'POST',
path: '/v1/process',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(payload),
signal: AbortSignal.timeout(12_000)
})

const text = await body.text()
if (statusCode >= 500) throw new Error(upstream ${statusCode})
return JSON.parse(text)
}


Важно: тело ответа нужно дочитать или корректно сбросить, даже если оно не нужно. Иначе socket может не вернуться в пул вовремя.

connections - это capacity, а не магическое ускорение

Маленький пул даёт очередь и рост latency. Слишком большой пул перегружает upstream, плодит открытые socket'ы, упирается в NAT / ephemeral ports и ухудшает tail latency.

Практический ориентир: если upstream отвечает за 100 ms, то 32 соединения дают примерно 320 RPS без pipelining. Но финальный размер выбирайте по SLO, payload size, TLS, сети и rate limits.

Таймауты должны быть разными

Один общий timeout скрывает причину деградации.

- connectTimeout - TCP/TLS connect
- headersTimeout - ожидание заголовков
- bodyTimeout - пауза между чанками body
- AbortSignal.timeout() - общий deadline операции

Предупреждение: bodyTimeout не равен лимиту на весь response body.

Backpressure делайте до Undici

Не запускайте Promise.all(items.map(callApi)) на тысячи элементов. Undici поставит часть запросов в очередь, но память и давление на приложение уже выросли.

Ограничивайте concurrency явно, например MAX_IN_FLIGHT = 64. Он не обязан равняться connections: это лимит нагрузки на ваш сервис, а не только на socket pool.

Вывод:
Надёжный исходящий HTTP в Node.js - это не просто fetch(), а управляемые пулы, раздельные таймауты и backpressure до попадания запросов в Undici.