Как я хакнул рынок труда: пишем свой ИИ-комбайн для автооткликов на HH.ru Если вы хоть раз искали работу в IT за последний год, то знаете, что рынок беспощаден к новичкам. Нужно откликнуться на сотни вакансий, а в итоге получаешь отказы от роботов. Чтобы пробиться через фильтры HR, нужно под каждую вакансию писать уникальное сопроводительное письмо. В этой статье автор сделал полный разбор того, как написал собственного автономного ИИ-агента, который ищет вакансии, фильтрует мусор с помощью локальной нейросети, пишет персонализированные сопроводительные письма и отчитывается в Telegram, пока пользователь спокойно занимается своими делами. Скрипт задумывался бесплатным, автономным и не требующим настройки вокруг платных API. Читать далее
JaJavaScript
Статьи, видео, книги по JavaScript и фронтенд разработке
По всем вопросам: @talentless_guy
Паблик в VK: vk.com/we_use_js
Реклама/ВП: писать @talentless_guy
CategoryOtherLanguageRUFirst seenJul 06, 2026QualityVerified
Subscribers2.2K
Avg views42.7K
ERR29.9%
Price-- RUB
Price / subscriber--
Price / view--
Metrics updated: Jul 06, 2026
Recent posts
Неявный deadlock в Node.js: AsyncLocalStorage + worker_threads + await в run() Многие знают AsyncLocalStorage для проброса контекста, но скрещивание его с пулом worker_threads и await внутри колбэка run() создаёт неочевидный deadlock. Эта ошибка часто остаётся незамеченной до первого production-инцидента, когда пул потоков залипает при высокой нагрузке. Как проявляется deadlock Внутри run() вы помещаете ID запроса и вызываете await, пока воркер не закончит задачу. Если потоков в пуле ограниченное число (например, 2), а запросов много, каждый await блокирует run(), не освобождая поток из пула. Воркеры заняты, потоки ждут await — контекст AsyncLocalStorage не отпускается до полного завершения колбэка. Пример problem: const als = new AsyncLocalStorage(); async function processRequest(data) { await als.run({ requestId: Date.now() }, async () => { const worker = new Worker('./worker.js'); const result = await new Promise((resolve, reject) => { worker.on('message', resolve); worker.on('error', reject); }); console.log(als.getStore().requestId); }); } Как обнаружить - Залогируйте время выполнения воркеров: рост с увеличением числа запросов — тревожный сигнал. - Используйте clinic.js или 0x: они покажут аномальные пики ожидания. - Типичная ошибка: пул из 2 воркеров и 100 параллельных запросов — все зависнут через несколько секунд. Практический совет Не оставляйте await внутри run(). Вынесите ожидание наружу: async function processRequest(data) { const worker = new Worker('./worker.js'); const result = await new Promise(/*...*/); als.run({ requestId: Date.now() }, () => { console.log(als.getStore().requestId); }); } Либо передавайте контекст через worker.postMessage() — это проще и исключает deadlock. Предупреждение AsyncLocalStorage отлично работает на веб-серверах с краткими синхронными операциями, но await внутри run() с ограниченным пулом потоков — прямой путь к deadlock. Проверяйте такие места на этапе code review. Вывод: Избегайте await внутри колбэка AsyncLocalStorage.run () при работе с пулом worker_threads — выносите асинхронное ожидание наружу или передавайте контекст через сообщения.
Диагностика и устранение неявной фрагментации кучи в Node.js Long-lived HTTP серверы с разными по размеру payload часто страдают от "дырявой" кучи, хотя утечек памяти нет. Память фрагментируется из-за чередования маленьких и больших Buffer-ов, что ведет к росту RSS и частым GC паузам. Как проявляется фрагментация После обработки HTTP запросов создаются Buffer-ы разного размера: маленькие для заголовков, большие для тела. GC освобождает их неупорядоченно, оставляя пустоты между выделенными блоками. Со временем это увеличивает RSS на 20-40% без утечек. Симптомы: падение производительности через 12+' часов работы, частые stop-the-world паузы GC. Диагностика в production Первый шаг — трассировка с --trace-gc . Запустите: NODE_OPTIONS="--trace-gc" node app.js . В выводе ищите Mark-sweep паузы >50ms. Второй шаг — сравнение heapdump-ов через час и сутки работы в Chrome DevTools. Ищите фрагментированные объекты Buffer, разбросанные по куче. Третий шаг — проверьте флаги V8: node --v8-options | grep -i "heap.*frag" . Решение: Buffer Pool для однородности Создайте пул Buffer-ов фиксированного сегмента, например 1024 байта. Это снижает разнообразие размеров и уменьшает фрагментацию. Пример: class BufferPool { constructor(size = 1024) { this.pool = []; this.size = size; } alloc(size) { if (size > this.size) return Buffer.allocUnsafe(size); if (this.pool.length) return this.pool.pop().slice(0, size); return Buffer.allocUnsafe(this.size); } free(buf) { this.pool.push(buf); } } Практические советы и ошибки - Для максимального снижения фрагментации используйте Buffer.allocUnsafeSlow вместо Buffer.allocUnsafe в пуле — он избегает shared memory pool. - Настройте --max-old-space-size и --optimize-for-size для ограничения кучи. - Типичная ошибка — использовать Buffer.allocUnsafe для всех случаев без разбора. Это увеличивает фрагментацию из-за случайных размеров. - В Node 20+ можно изолировать тяжелые операции через --experimental-vm-modules в отдельных контекстах. Вывод: Фрагментация кучи в long-lived серверах — реальная проблема, решаемая через Buffer Pool с фиксированным сегментом и контроль аллокаций, а не через поиск утечек.
Скрытый убийца производительности: синхронный DNS в dns.lookup под нагрузкой Высоконагруженный HTTP-клиент на Node.js внезапно теряет пропускную способность? Event loop залипает при большом количестве параллельных запросов? Чаще всего проблема не в HTTP, а в том, как система разрешает доменные имена. В чем суть? dns.lookup (используется http.request , https , axios по умолчанию) при вызове без флагов обращается к системному getaddrinfo через libuv . Этот вызов синхронный и блокирует event loop на время резолвинга. При 1000+ RPS latency одного разрешения (например, 50 мс) превращается в 50 000 мс общего блокирования цикла. Как диагностировать? 1. Подключить dns.promises и выполнить dns.promises.lookup при нагрузке. Если event loop задерживает микротаски, виновник найден. 2. Использовать process.hrtime.bigint() до и после dns.lookup - разница > 10 мс системного времени указывает на проблему. 3. clinic doctor - на графике async latency видны ступеньки, совпадающие с пиками DNS-запросов. Как устранить? Для http(s) агентов - замена на асинхронный DNS: const { createConnection } = require('net'); const dns = require('dns/promises'); const agent = new http.Agent({ createConnection: async (options, cb) => { const { address } = await dns.lookup(options.hostname); const socket = createConnection({ ...options, host: address }); cb(null, socket); }, }); Типичная ошибка: использование только maxSockets или scheduling: 'lifo' снижает частоту новых соединений, но не решает первопричину при множестве уникальных хостов. Проще - библиотеки dns-cache или cacheable-lookup . Вывод: блокировка из-за dns.lookup - классическая проблема одного потока Node.js на стыке системных вызовов, решается заменой на асинхронное разрешение через dns.promises или кастомный агент.
Диагностика и устранение гонок данных при конкурентном доступе к node:sqlite из worker_threads через in-memory WAL-режим В production сценариях с worker_threads и in-memory SQLite в WAL режиме разработчики часто забывают синхронизировать доступ. Итог — SQLITE_BUSY , рассинхрон данных и дубли в уникальных полях. Проблема: гонка без синхронизации Несколько воркеров читают и пишут в одну in-memory базу без внешней координации. WAL помогает с параллельным чтением, но не спасает от гонок записи. Типичные проявления: ошибки SQLITE_BUSY , частичные обновления и потеря данных. Решение: один писатель с явными блокировками Выделите один воркер для критических записей (например, инкременты). Координируйте через SharedArrayBuffer и Atomics — это дешевле мьютексов. Воркеры-читатели используют отдельные соединения в WAL. Production-oriented пример с BEGIN IMMEDIATE Начиная с Node.js 23 используйте DatabaseSync . Обязательно PRAGMA journal_mode=WAL; и PRAGMA synchronous=NORMAL; : import { DatabaseSync } from 'node:sqlite'; const db = new DatabaseSync(':memory:', { readwrite: true, create: true }); db.exec('PRAGMA journal_mode=WAL;'); db.exec('PRAGMA synchronous=NORMAL;'); function safeUpdate(sql) { db.exec('BEGIN IMMEDIATE;'); try { db.exec(sql); db.exec('COMMIT;'); } catch { db.exec('ROLLBACK;'); throw new Error('Write failed'); } } BEGIN IMMEDIATE блокирует запись сразу, избегая гонок. Без него два воркера могут начать транзакции одновременно — ловите SQLITE_BUSY . Предупреждение: in-memory WAL не для продакшена In-memory база теряет данные при краше. Для реальных проектов используйте файловую базу с WAL — это гарантирует персистентность. Вывод: гонки данных в SQLite лечатся одним писателем и явными блокировками через BEGIN IMMEDIATE , давая скорость WAL без потери консистентности.