Свежие заметки

util.promisify() для замены колбэков на промисы

Промисы на практике оказались удобнее колбэков из-за того, что не требуют постоянного увеличения вложенности при работе с цепочками асинхронных функций. Более того, на базе промисов строится работа async function.

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

Технически, не сложно обернуть асинхронную функцию в промиз. Но, к сожалению, это не одна строчка кода.

const fs = require('fs');

function reader(path) {
    return new Promise((resolve, reject) => {
        fs.readdir(path, (err, list) => {
            if (err) {
                reject(err);
                return;
            }

            resolve(list);
        });
    });
}

reader('.')
    .then(list => {
        console.log(list);
        process.exit(0);
    })
    .catch(err => {
        console.error(err);
        process.exit(1);
    });

В стандартной библиотеке node.js 8 util появилась функция promisify(), которая как раз решает эту задачу.

const fs = require('fs');
const util = require('util');

const reader = util.promisify(fs.readdir);

reader('.')
    .then(list => {
        console.log(list);
        process.exit(0);
    })
    .catch(err => {
        console.error(err);
        process.exit(1);
    });

Функция promisify() придерживается соглашения:

  1. колбек в параметрах исходной функции всегда идет последним;
  2. первым параметром колбека приходит объект ошибки, если она есть.

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

function Timer(delay) {
    this._delay = delay;
}

Timer.prototype.start = function (callback) {
    setTimeout(callback, this._delay);
};

const timer = new Timer(1000);
timer.start(() => {
    console.log('Finish');
});

Из метода start() я хочу сделать новую функцию, возвращающую мне промиз.

const start = util.promisify(timer.start);
start().then(() => {
    console.log('Finish');
});

Но этот фрагмент кода перестаёт работать. Он начинает выдавать ошибку:

TypeError: Cannot read property '_delay' of undefined

Проблема кроется в том, что метод внутри себя использует ссылку на экземпляр класса Timer — в частности, он вытаскиваем величину задержки. А так как метод был вызван вне контекста экземпляра таймера, то this оказалось не определено.

Если вернуться к примеру с колбэком, то следующий фрагмент кода тоже не будет работать и выдаст такую же ошибку.

const start = timer.start;
start(() => {
    console.log('Finish');
});

Исправить ситуацию можно несколькими путями.

Связать функцию с экземпляром

const start = util.promisify(timer.start).bind(timer);

Теперь не важно как мы вызовем функцию start(). Она всегда будет иметь экземпляр таймера в качестве контекста исполнения.

Сохранить функцию в экземпляр класса

timer.start = util.promisify(timer.start);
timer.start().then(() => {
    console.log('Finish');
});

Для данного экземпляра мы подменили реализацию метода из прототипа на новую функцию. Контекст исполнения будет передаваться естественным образом.

Развивая этот вариант дальше, новый метод можно сохранить в прототипе класса.

Timer.prototype.start = util.promisify(Timer.prototype.start);

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

Оставте свой комментарий

Как удалить произвольную копию диска в Time Machine

Для управления Time Machine из командной строки существует утилита tmutil. Нас интересуют методы получения списка бэкапов и удаления выбранного бэкапа.

Чтобы получить список бэкапов, выполним команду:

tmutil listbackups

Теперь, зная путь до конкретного архива, его можно удалить командой

tmutil delete /Volumes/Backup/Backups.backupdb/Slim/2016-10-01-031248

Если потребуется удалить сразу несколько архивов, то лучше автоматизировать эти действия. Например, это можно сделать так:

tmutil listbackups | grep '2016-0[4-9]' | sort -r | xargs -n 1 sudo tmutil delete

Эта последовательность команд вначале получает список всех архивов. Потом оставляет в списке только архивы с апреля по сентябрь. Сортирует список от новых к старым, чтобы было меньше операций и процесс шёл быстрее. А затем удаляет их один за одним.

Оставте свой комментарий

Код состояния HTTP для неавторизованного пользователя

Среди официального списка кодов состояний HTTP есть такой код:

401 Unauthorized

The request requires user authentication.

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

В описании кода 401 ясно указано, что вместе с таким ответом сервер должен передать заголовок WWW-Authenticate с перечнем условий. А браузер может повторить запрос, включив в него заголовок Authorization с требуемыми для аутентификации данными.

Чаще всего в реальной жизни этот код можно встретить, если доступ к ресурсу защищён паролем. Например, в Apache это можно сделать добавив в конфигурацию следующие строки:

AuthName "Secure Area"
AuthType Basic
AuthUserFile "/usr/local/apache/passwd/passwords"
Require valid-user

Браузер попросит пользователя ввести логин и пароль и будет в дальнейшем их использовать для доступа ко всем ресурсам, защищенным такой базовой аутентификацией. Замечу, что введённые данные будут каждый раз передаваться в открытом виде в заголовках запросов.

Очевидно, что такая проверка уровня доступа пользователя к запрашиваемому ресурсу по протоколу HTTP никак не связана с разграничением прав пользователей внутри сервера приложения.

А как лучше?

По моему мнению, разумнее отвечать неавторизованному пользователю кодом 403 Forbiden

403 Forbidden

The server understood the request, but is refusing to fulfill it.

А в теле ответа передать JSON с описанием проблемы.

Комментарии к заметке: 4

Знакомство с AWS Lambda

Amazon Web Services Lambda — это веб-сервис, запускающий ваш код на Node.js, Python или Java в ответ на определенные события и отвечающий за автоматическое выделение необходимых вычислительных ресурсов. Функции Lambda не хранят состояние, поэтому AWS Lambda может быстро запустить столько копий функции, сколько нужно для обработки входящих событий.

Оплата производится за количество вызовов функции и время, необходимое для исполнения кода этой функции. Изучение и маленькие домашние проекты для вас могут быть совершенно бесплатными в течение года с момента регистрации в AWS.

Создание пользователя

После регистрации в AWS нужно создать пользователя и назначить ему права доступа к сервисам.

Все инструкции вы найдёте в документации AWS Identity and Access Management «IAM Users». Сохраните файл с ключами — они потребуются при настройке CLI.

Только что созданный пользователь не имеет никаких прав. Чтобы он получил доступ к созданию и запуску функций Lambda, нужно назначить ему соответствующую политику. В разделе «Working with Policies» описано как добраться до списка политик. Нам нужно добавить для пользователя политику «AWSLambdaFullAccess». Позже аналогично можно будет добавить или удалить другие политики.

AWS Command Line Interface

Почти все функции AWS доступны через веб-интерфейс, но на практике удобнее использовать консольные команды или скрипты.

Инструкции по установке утилит вы найдёте на странице «AWS Command Line Interface». После того как вы проделаете все описанные там действия, запустите команду:

aws configure

и укажите ключи пользователя, которые вы получили при его создании. Я так же выбрал регион по-умолчанию «eu-central-1». В разных регионах доступны разные наборы сервисов.

Создание функции AWS Lambda

Для начала, напишем простой скрипт, который будет возвращать строку "Hello, world!". Так же в логах мы увидим с какими параметрами вызывалась функция.

'use strict';

exports.handler = (event, context, callback) => {
  console.log(JSON.stringify(event, null, 2));
  callback(null, 'Hello, world!');
};

Сохраним этот код в файл index.js.

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

Перед тем как загрузить функцию в AWS, нужно создать роль для неё. Если у вас уже существует подходящая роль, то этот шаг можно пропустить.

aws iam create-role \
  --role-name MyFirstLambda-Execution \
  --assume-role-policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }]
  }' \
  --output text \
  --query 'Role.Arn'

В ответ вы получите строку вида:

arn:aws:iam::000000000000:role/MyFirstLambda-Execution

Так как наша функция будет вести логгирование, то нужно добавить ей соответствующую политику. Иначе лог не сохранится в CloudWatch.

aws iam attach-role-policy \
  --role-name MyFirstLambda-Execution \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

Вот теперь можно загрузить нашу функцию в Lambda.

zip - index.js | \
aws lambda create-function \
  --function-name MyFirstLambda \
  --runtime nodejs4.3 \
  --role arn:aws:iam::000000000000:role/MyFirstLambda-Execution \
  --handler index.handler \
  --zip-file fileb:///dev/stdin

Код функции будет архивироваться и загружаться в AWS без создания временных файлов. Так же код можно загружать через S3.

В параметре --handler указываем точку входа — название модуля (index) и метод, который экспортирует модуль.

В параметре --role указываем роль, которую мы создали для запуска функции.

Выполнение функции

Запустить функцию можно из терминала

aws lambda invoke \
  --function-name MyFirstLambda \
  /tmp/out.json && cat /tmp/out.json

Результат сохранится в файл /tmp/out.json, содержимое которого мы затем выведем в терминале.

aws lambda invoke \
  --function-name MyFirstLambda \
  --payload '{"foo":1,"bar":true}' \
  /tmp/out.json

Обновление кода и конфигурации функции

Обновляется код функции аналогично тому как она создавалась.

zip - index.js | \
aws lambda update-function-code \
  --function-name MyFirstLambda \
  --zip-file fileb:///dev/stdin

Если нужно обновить какие-то параметры конфигурации, то это делается командой update-function-configuration.

Заключение

С AWS Lambda можно легко получить масштабируемое окружение.

На практике функции вызываются по какому-либо событию. Например, запуск по расписанию, обновление таблицы в DynamoDB, появление файла в S3 или при поступлении HTTP-запроса в сервис API Gateway.

В свою очередь, код Lambda-функции может взаимодействовать с другими серверами AWS, внешними базами данных и веб-ресурсами.

Оставте свой комментарий

Настройка обратного прокси-сервера для поддержки протокола HTTP/2

Отдельный прокси-сервер может быть полезными, когда на штатном HTTP-сервере нет должной поддержки HTTP/2 или вы, по тем или иным причинам, не можете обновить версию сервера. Я попробовал некоторые из большого списка, существующих на данный момент, серверов.

nghttp2

Проект nghttp2 представляет собой библиотеку для работы с потоками данных и набор инструментов, написанных на базе этой библиотеки.

Процесс сборки несложен и подробно описан в документации. В результате компиляции вы получите:

  • nghttp — клиент HTTP/2;
  • nghttpd — сервер;
  • nghttpx — прокси-сервер;
  • h2load — инструмент нагрузочного тестирования.

Клиент по своей функциональности очень напоминает curl, а утилита нагрузочного тестирования — ab.

Прокси-сервер имеет несколько режимов работы и множество настроек. Тем не менее, его настройка в режиме обратного прокси оказалось очень простой.

$ nghttpx --frontend=*,443 \
    --backend=prod.noteskeeper.ru,80 \
    --tls-proto-list=TLSv1.2,TLSv1.1,TLSv1.0 \
    --dh-param-file=/etc/letsencrypt/live/noteskeeper.ru/dhparam.pem \
    /etc/letsencrypt/live/noteskeeper.ru/privkey.pem \
    /etc/letsencrypt/live/noteskeeper.ru/fullchain.pem

Обратный прокси будет слушать порт 443 на всех интерфейсах и передавать запросы на хост prod.noteskeeper.ru. Остальные параметры нужны для настройки TLS.

Ещё, возможно, будет полезен ключ --host-rewrite. Если он будет указан, то прокси заменит заголовок Host в запросе к бекэнду на значение из исходного запроса от клиента. Например, рассмотрим следующую ситуацию. Пусть на одном сервере будет настроен хостинг для моего блога по адресу noteskeeper.ru. На другом сервере я запускаю прокси. Теперь я должен заменить адрес в DNS-записи A на тот, который будет указывать на прокси и добавить ещё одну запись A на сайт, но с другим под-доменом (назовём его prod). Проблема в том, что хостинг знает только о сайте noteskeeper.ru и ничего не знает о prod.noteskeeper.ru. Из-за этого сайт начнёт отвечать ошибками с кодом 404. С таким же успехом мы могли бы в конфигурации бекэнда nghttpx указать просто IP-адрес, но домен всё же удобнее. Тут нам и поможет замена заголовка — клиент будет обращаться по адресу noteskeeper.ru и этот же адрес будет передаваться в запросе на сайт.

Минусом решения на базе nghttpx является то, что вам придётся запустить несколько его экземпляров с разными параметрами чтобы обслуживать запросы с или без www, а так же на 80 порту без шифрования и на 443 с шифрованием.

h2o

Проект h2o развивается как альтернатива Nginx и представляет собой высокопроизводительный веб-сервер с возможностью обратного прокси. Собирается он так же из исходников без каких-либо сложностей. В документации описаны все требуемые условия.

Сервер настраивается с помощью конфигурационного файла. В моём случае он получился вот таким:

hosts:
  "noteskeeper.ru:80":
    listen:
      port: 80
    paths:
      "/":
        redirect: https://noteskeeper.ru/
  "noteskeeper.ru:443":
    listen:
      port: 443
      ssl:
        key-file: /etc/letsencrypt/live/noteskeeper.ru/privkey.pem
        certificate-file: /etc/letsencrypt/live/noteskeeper.ru/fullchain.pem
        dh-file: /etc/letsencrypt/live/noteskeeper.ru/dhparam.pem
    paths:
      "/":
        proxy.reverse.url: "http://prod.noteskeeper.ru:80/"
        proxy.preserve-host: ON

# Включаем компрессию данных.
# Без этой настройки клиент получит данные в не сжатом виде,
# даже если они были сжаты на бекэнде.
gzip: ON

# Передавать быстрее файлы с большим приоритетом.
# Изначально к таким файлам относятся CSS и JS.
http2-reprioritize-blocking-assets: ON

# Включить проверку кешировани для файлов, которые
# были переданы с помощью server push.
http2-casper: ON

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

file.mime.addtypes:
  "application/x-javascript":
    extensions: [".js"]
    is_compressible: yes
    priority: highest

Запускается сервер командой:

$ h2o -c ./noteskeeper.conf

Заключение

Оба сервера, которые я протестировал, понимают заголовки ответа Link со значением rel=preload. Это позволит управлять очередью доставки контента пользователю. Ресурсы, которые вы указали, начнут отсылаться по сети ещё до того как браузер их запросит.

Для поддержки HTTP/2 вам придётся получить SSL-сертификат.

Ни одно из этих решений пока не выпущено в качестве собранного пакета. А это значит нужно будет повозиться с настройкой запуска демона.

Оставте свой комментарий