Ответ 1
Сбой и перезапуск процесса не является правильной стратегией для устранения ошибок, даже ошибок. Это было бы хорошо в Эрланге, где процесс дешев и выполняет одну изолированную вещь, например, обслуживание одного клиента. Это не относится к узлу, где процесс стоит на порядок больше и обслуживает тысячи клиентов одновременно
Допустим, у вас есть 200 запросов в секунду, обслуживаемых вашим сервисом. Если 1% из них попадут в исходный код в вашем коде, вы получите 20 остановок процессов в секунду, примерно один раз в 50 мс. Если у вас есть 4 ядра с 1 процессом на ядро, вы потеряете их через 200 мс. Поэтому, если процессу требуется более 200 мс для запуска и подготовки к обработке запросов (минимальная стоимость составляет около 50 мс для процесса узла, который не загружает какие-либо модули), у нас теперь есть полный отказ в обслуживании. Не говоря уже о том, что пользователи, сталкивающиеся с ошибкой, обычно делают такие вещи, как, например, многократное обновление страницы, что усугубляет проблему.
Домены не решают проблему, потому что они не могут гарантировать, что ресурсы не просочились.
Читайте больше в выпусках # 5114 и # 5149.
Теперь вы можете попытаться быть "умными" в этом вопросе и иметь определенную политику повторного использования процессов, основанную на определенном количестве ошибок, но какая бы стратегия вы ни применили, она сильно изменит профиль масштабируемости узла. Мы говорим о нескольких десятках запросов в секунду на процесс вместо нескольких тысяч.
Однако обещания перехватывают все исключения и затем распространяют их способом, очень похожим на то, как синхронные исключения распространяются вверх по стеку. Кроме того, они часто предоставляют способ, finally
, который предназначен, чтобы быть эквивалентом try...finally
, благодаря этим двум признакам, мы можем инкапсулировать, что очистке логики путем создания "Контекст-менеджеров" ( по аналогии with
в питона и using
в С#), которые всегда убирают ресурсы.
Предположим, что наши ресурсы представлены как объекты с методами acquire
и dispose
, которые возвращают обещания. При вызове функции соединения не устанавливаются, мы только возвращаем объект ресурса. Этот объект будет обработан с using
позже:
function connect(url) {
return {acquire: cb => pg.connect(url), dispose: conn => conn.dispose()}
}
Мы хотим, чтобы API работал так:
using(connect(process.env.DATABASE_URL), async (conn) => {
await conn.query(...);
do other things
return some result;
});
Мы можем легко достичь этого API:
function using(resource, fn) {
return Promise.resolve()
.then(() => resource.acquire())
.then(item =>
Promise.resolve(item).then(fn).finally(() =>
// bail if disposing fails, for any reason (sync or async)
Promise.resolve()
.then(() => resource.dispose(item))
.catch(terminate)
)
);
}
Ресурсы всегда будут удаляться после завершения цепочки обещаний, возвращаемой с использованием аргумента fn
. Даже если в этой функции возникла ошибка (например, из JSON.parse
) или ее внутренних закрытий .then
(например, во втором JSON.parse
), или если обещание в цепочке было отклонено (эквивалентно обратным JSON.parse
, вызывающим с ошибкой). Вот почему так важно, чтобы обещания ловили ошибки и распространяли их.
Однако, если утилизация ресурса действительно не удалась, это действительно хорошая причина для прекращения. Весьма вероятно, что в этом случае мы утекли ресурс, и это хорошая идея, чтобы начать сворачивать этот процесс. Но теперь наши шансы на сбой изолированы от гораздо меньшей части нашего кода - части, которая на самом деле имеет дело с утечкой ресурсов!
Примечание: в конце концов terminate выбрасывает вне диапазона, поэтому обещания не могут его перехватить, например, process.nextTick(() => { throw e });
, Какая реализация имеет смысл, может зависеть от вашей настройки - основанная на nextTick работает аналогично обратным вызовам.
Как насчет использования библиотек на основе обратного вызова? Они потенциально могут быть небезопасными. Давайте посмотрим на пример, чтобы увидеть, откуда эти ошибки могут возникнуть и какие могут вызвать проблемы:
function unwrapped(arg1, arg2, done) {
var resource = allocateResource();
mayThrowError1();
resource.doesntThrow(arg1, (err, res) => {
mayThrowError2(arg2);
done(err, res);
});
}
mayThrowError2()
находится внутри внутреннего обратного вызова и все равно завершит работу процесса, если он сгенерирует, даже если unwrapped
вызывается в другом обещании .then
. Подобные ошибки не улавливаются типичными promisify
и продолжают вызывать сбой процесса, как обычно.
Однако mayThrowError1()
будет mayThrowError1()
обещанием, если оно будет mayThrowError1()
внутри .then
, и внутренний выделенный ресурс может просочиться.
Мы можем написать параноидальную версию promisify
которая гарантирует, что любые promisify
ошибки будут неустранимыми и promisify
процесс:
function paranoidPromisify(fn) {
return function(...args) {
return new Promise((resolve, reject) =>
try {
fn(...args, (err, res) => err != null ? reject(err) : resolve(res));
} catch (e) {
process.nextTick(() => { throw e; });
}
}
}
}
Использование функции обещания в другом обещании. .then
обратного вызова теперь происходит сбой процесса, если развернутые броски возвращаются к парадигме броска-сбоя.
Мы надеемся, что по мере того, как вы будете использовать все больше и больше библиотек, основанных на обещаниях, они будут использовать шаблон диспетчера контекста для управления своими ресурсами, и, следовательно, вам не нужно будет допускать сбоя процесса.
Ни одно из этих решений не является пуленепробиваемым - даже при сбое при сбое. Очень легко случайно написать код, который пропускает ресурсы, несмотря на то, что не выбрасывает. Например, эта функция стиля узла будет пропускать ресурсы, даже если она не генерирует:
function unwrapped(arg1, arg2, done) {
var resource = allocateResource();
resource.doSomething(arg1, function(err, res) {
if (err) return done(err);
resource.doSomethingElse(res, function(err, res) {
resource.dispose();
done(err, res);
});
});
}
Зачем? Потому что, когда doSomething
вызов doSomething
получает ошибку, код забывает утилизировать ресурс.
Такого рода проблемы не возникают с контекстными менеджерами. Вы не можете забыть позвонить распоряжаться: вам не нужно, так как using
делает это за вас!
Ссылки: почему я переключаюсь на обещания, контекстные менеджеры и транзакции