Конфигурация и завершение приложения
DeskQueue теперь соединяет HTTP-маршруты, PostgreSQL, пользователей, ротацию токенов и файловые вложения. Последний шаг свяжет ресурсы со временем жизни процесса. Приложение должно проверить конфигурацию до начала работы, сообщить о готовности и при обычной остановке перестать принимать новые запросы, завершить текущие и закрыть пул. Это иной результат, чем просто удачный запуск одного маршрута.
В stages/20 точка входа разделена на app.js, config.js и server.js. app.js создаёт Express-приложение, config.js проверяет настройки, server.js владеет сетевым сервером и остановкой. В архиве это окончательные полные файлы. Само приложение при написании не запускалось; критерии в тексте предназначены для читательского стенда.
Конфигурация как вход программы
Порт, строка подключения, ключ JWT и каталог файлов являются настройками окружения. Они не должны быть зашиты в обработчики заявок. Порт проверяем как целое от одного до 65535. Путь UPLOAD_DIR требуется абсолютный, чтобы смена рабочего каталога не перенесла постоянные файлы в другое место. Ключ должен декодироваться в 32 байта, как установлено в главе о JWT.
Полный модуль config.js проверяет настройки до слушания. Строку подключения не печатаем. Для локального стенда читатель может использовать настоящий .env и встроенный параметр Node.js --env-file; шаблон .env.example содержит только нейтральные значения и указание заменить секреты.
import { isAbsolute } from 'node:path';
const port = Number(process.env.PORT ?? 3000);
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('Invalid PORT');
if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL is required');
if (!isAbsolute(process.env.UPLOAD_DIR ?? '')) throw new Error('UPLOAD_DIR must be absolute');
if (Buffer.from(process.env.JWT_SECRET ?? '', 'base64url').length !== 32) {
throw new Error('JWT_SECRET must encode 32 random bytes');
}
export const config = { port, host: '127.0.0.1', uploadDir: process.env.UPLOAD_DIR };
Настройки читаются один раз при старте. Изменение файла .env не означает, что работающий процесс автоматически увидел новые значения. Требуется согласованное обновление процесса. Для секрета JWT это также влияет на действительность токенов, поэтому жизненный цикл ключей не сводится к редактированию одной строки.
Встроенный --watch подходит для локального редактирования кода и уже указан в команде dev. Он не является менеджером производственного сервиса и не должен подменять его политику перезапуска. При изменении файла возможен перезапуск, который сбрасывает память процесса. Постоянные данные поэтому вынесены в PostgreSQL и отдельный каталог. Поведение параметров описано в документации CLI Node.js.
Живой процесс и готовое приложение
GET /health сообщает, что процесс может ответить. GET /ready проверяет, что приложение не останавливается и база способна выполнить короткий запрос к ожидаемой таблице. Проверка SELECT 1 удостоверяет связь с PostgreSQL, но не наличие схемы tickets, поэтому здесь используем запрос к таблице. Содержимое заявок не включается в ответ состояния.
Ожидаемый успешный readiness — 200 и ready: true. При начале остановки или недоступной базе — 503. Причину можно сопоставить с закрытым журналом, но публичный ответ не раскрывает адрес базы и текст исключения. Проверка не доказывает, что любой будущий запрос будет успешен: она отражает наблюдение в конкретный момент.
Readiness не должна сама создавать таблицы, применять миграции или исправлять данные. Иначе обычный запрос наблюдения станет управляющей операцией. Схема изменяется отдельным согласованным шагом. Также не стоит выполнять дорогой полный просмотр базы при каждом проверочном запросе: сам мониторинг способен усилить нагрузку проблемного сервиса.
Каталог вложений готовится при старте с mkdir и известными правами. Однако одно создание каталога не удостоверяет доступность каждого прежнего файла. Полная проверка файлового хранилища и копий является отдельным процессом сопровождения. В этом курсе мы не обещаем, что ready проверяет восстановление или согласованность метаданных с каждым вложением.
Остановка по сигналу
Обычный менеджер процесса отправляет сигнал завершения. Наш сервер принимает SIGTERM и SIGINT, ставит признак остановки, закрывает слушание и после завершения текущих ответов закрывает пул. Нельзя сначала уничтожить соединения базы: выполняющийся контроллер ещё может ждать SQL и получить ошибку посреди нормальной остановки.
let stopping = false;
function shutdown(signal) {
if (stopping) return;
stopping = true;
console.log(JSON.stringify({ event: 'shutdown', signal }));
const deadline = setTimeout(() => process.exit(1), 10000);
deadline.unref();
server.close(async (error) => {
try {
await pool.end();
process.exitCode = error ? 1 : 0;
} catch {
process.exitCode = 1;
} finally {
clearTimeout(deadline);
}
});
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
Это фрагмент владельца сервера; в полном server.js есть проверка базы, создание приложения и обработка ошибки слушания. Десять секунд являются учебным пределом ожидания, а не измеренным временем всех операций. Если текущий запрос не завершился, процесс принудительно прекратится с ненулевым кодом. Предел предотвращает бесконечное ожидание, но не гарантирует успешность прерванной операции.
Признак stopping не позволяет повторно запустить ту же процедуру при нескольких сигналах. Readiness использует его через переданную функцию. Так HTTP-приложение получает наблюдение о состоянии владельца, не создавая циклический импорт сервера. Пул также существует один раз на процесс и закрывается один раз в конце.
Сквозной ожидаемый путь
После подготовки базы и применения четырёх миграций читатель регистрирует requester, выполняет вход, создаёт заявку и прикрепляет небольшой текст. Сотрудник agent видит её в фильтре open, переводит в working, а история сохраняет переход и его личность. Пагинация возвращает ограниченные страницы. Выход отзывает текущую семью токенов; повтор использованного refresh-токена приводит к такому же отзыву.
Этот путь объединяет результаты курса, но не заменяет самостоятельную проверку каждого ограничения. Чужое вложение должно оставаться недоступным, большой файл — отвергаться, неизвестное состояние — не сохраняться. Перезапуск должен сохранить SQL-строки и файлы, если приложение вернулось к тем же постоянным ресурсам. Исходные JSON-заявки первых ступеней не подмешиваются в готовую базу автоматически.
Если сервер не начал слушать из-за занятого порта, ожидается ошибка запуска и ненулевой код, а не ложное сообщение ready. Если база недоступна до старта, программа не открывает API. Если она потерялась во время работы, readiness становится отрицательным, а контроллеры передают внутренний сбой. Разные моменты требуют разных наблюдений, хотя причина может быть одна.
Серия закончила учебный API, но не выполняла его развёртывание. TLS, менеджер сервиса, копии, ограничения нагрузки и реальные измерения относятся к отдельному окружению. Для перехода полезны существующие уроки о сервисе systemd и постоянных данных. Их пример имеет свои имена и версии; переносить конфигурации между проектами следует осмысленно.
Теперь вы можете проследить полный жизненный цикл: проверенный вход, ограниченная операция, согласованная запись, ожидаемый ответ, диагностический контекст и завершение ресурсов. Именно эти связи делают программу понятной для сопровождения и дальнейшего расширения, даже когда один контроллер становится частью более крупного приложения.
Для собственного разбора остановки сначала различайте спокойное завершение и аварийное прекращение. Обработчики сигналов помогают только там, где процесс получил возможность выполнить JavaScript. Принудительное уничтожение, сбой машины или потеря питания не проходят эту цепочку. Постоянные данные и транзакции должны быть рассчитаны также на такие случаи. Корректная остановка уменьшает число прерванных запросов, но не является резервной копией и не заменяет восстановление после аварии.
Полные файлы учебного проекта. Примеры при подготовке текста не запускались; ответы и вывод в уроке описывают ожидаемый результат читателя.