среда, 13 мая 2015 г.

Почему я не люблю конфигурацию в django-style

Введение

Сегодня работал над добавлением в aiohttp.web свойства scheme для request object.

Идея простая: отвечать что request.scheme "http" для HTTP запросов, иначе "https".

У меня есть правило: перед началом погляди как другие уже справились с этой задачей.

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

Так вышло что сегодня я смотрел код Django.

И было в том коде примерно такое:

@property
def scheme(self):
    if settings.SECURE_PROXY_SSL_HEADER:
        try:
            header, value = settings.SECURE_PROXY_SSL_HEADER
        except ValueError:
            raise ImproperlyConfigured(
                'The SECURE_PROXY_SSL_HEADER setting must be a tuple containing two values.'
            )
        if self.META.get(header, None) == value:
            return 'https'
    return 'http'

В целом очень хорошо: Django показала, как работать с HTTP и что делать если сервер расположен за HTTPS Reverse Proxy (Nginx, например).

В последнем случае я сконфигурирую Nginx чтобы он добавил несколько полезных HTTP HEADERS для HTTPS connection:

  proxy_set_header        Host $host;
  proxy_set_header        X-Real-IP $remote_addr;
  proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header        X-Forwarded-Proto $scheme;

По X-Forwarded-Proto я пойму что это был HTTPS.

В целом стандартная и всем (надеюсь) известная процедура.

У aiohttp свободы чуть больше: оно может понять что сокет, по которому подключились напрямую, сам уже SSL -- но это пригодно только если мы готовы выставить наш aiohttp сервер прямо в веб.

Куда чаще его прячут за Nginx, HAProxy или похожим reverse proxy и там уже работают с сертификататами, проксируя обычный HTTP connection.

В общем всё прекрасно: Nginx выставит X-Forwarded-Proto HTTP HEADER который будет или "http" или "https".

Django глянет на settings.SECURE_PROXY_SSL_HEADER и если там ("X-Forwarded-Proto", "https") то scheme тоже будет "https".

Очень грамотно сделано, мне нравится.

Проблема

Так почему я этот пост написал?

А потому что settings.SECURE_PROXY_SSL_HEADER может быть чем угодно -- строкой, числом или ещё какой непотребной константой.

Проверка выполняется на момент получения request.scheme.

Нам, питонщикам, плевать на производительность в данном случае -- на самом деле try/except обходится дешево и никак не отразится на работе сайта.

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

У тестов будет свой правильный settings.py, а на production server админ чуть-чуть ошибётся.

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

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

Решение

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

Для начала нужно отказаться от использования общего конфига в API.

Строить код библиотеки так, чтобы она никогда не лезла в settings.py (это и Flask касается если что).

Пусть все нужные классы принимают конфигурационные параметры явно, прямо в конструкторах.

Тогда можно быстро понять, что формат параметра не тот или IP address недоступен.

Разделение на этапы:

  • чтение конфига, анализ его и подготовка приложения к работе
  • запуск и работа

помогает избежать досадных недоразумений.

Плюс, к тому же, на явном этапе подготовки к старту можно позволить себе довольно дорогостоящие проверки на корректное функционирование системы (послать PING чтобы убедится что Redis живой, например).

вторник, 24 марта 2015 г.

Подкаст

Неделю назад принимал участие в записи подкаста о Питоне вообще и asyncio в частности.
Прослушать результат нечеловеческих усилий можно здесь.

суббота, 14 февраля 2015 г.

vagga

Паша Коломиец работает над проектом vagga.

Коротко говоря, это система виртуализации на linux контейнерах, предназначенная для разработки программ.

Выполняет роль, в чем-то похожую на virtualenv, но со своими отличиями и особенностями.

Рекомендую прочесть свежую Пашину статью, в которой он даёт обзор возможностей vagga и показывает преимущества над virtualenv.

P.S. Я лично к проекту никакого отношения не имею, поэтому все вопросы, пожалуйста, задавайте сразу Паше.


пятница, 16 января 2015 г.

aiohttp 0.14

Выпустил новую версию библиотеки.

Из вкусного -- Web Sockets для aiohttp.web серверов, оптимизация скорости работы, множество мелких улучшений.

Полный список изменений -- здесь.

понедельник, 20 октября 2014 г.

Почему я не люблю Flask

Есть такой популярный microframework: Flask.

Многим нравится: легкий и простой для изучения, то да сё.

А мне --- категорически нет.

Нелюбовь началась с элементарного: request --- это thread local variable:

import flask
from myapp import app

@app.route('/')
def handler():
    req = flask.request
    if 'arg' in req.args:
        process_arg(req.args['arg'])
    ###

Т.е. для для того чтобы узнать с какими GET или POST параметрами вызвали мой код -- я должен обращаться к глобальной переменной!

Я знаю разницу между global variable и thread local variable если что -- но это не избавляет от неприятного послевкусия.

Ага, есть еще и flask.g!

Если уж мне потребуются context local variables -- я их буду использовать по моему выбору, морщась от осознания собственного несовершенства. Зачем flask их мне навязывает?

Дальше -- больше.

Смотрим еще раз:

from myapp import app

@app.route('/')
def handler():
    ###

Имеем наполовину сконфигурированный импортированный откуда-то app, к которому добавляем обработчик.

Мне это не нравится. Я хочу сделать app и добавить в него route table.

Flask это позволяет, но документация провоцирует делать ровно наоборот.

Исполнять код на этапе импорта модуля не выглядит хорошей идеей, сейчас в этом я полностью уверен.

Идем дальше.

Параметры в route:

@app.route('/user/<username>')
def handler(username):
    pass

Весной это казалось мне удачным. Даже сделал что-то похожее в aiorest.

Потом понял, что штука абсолютно бесполезная: нам всегда требовалось что-то из HTTP HEADERS, COOKIES и GET/POST parameres в обработчике запроста.

Чтобы проверить -- авторизирован ли пользователь, например.

Выпилил.

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

route args, GET, POST, COOKIES -- каждый dict может иметь перекрывающиеся имена-названия.

Паша Коломиец в zorro попытался решить проблему через аннотации:

def handler(self, request: Request):
    pass

Т.е. handler имеет параметр с аннотацией Request -- он получит в него request object.

В zorro можно регистрировать свои аннотации для получения дополнительной информации.

Симпатично и элегантно -- но слишком сложно для библиотеки для чайников.

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

Заключение

Я не призываю не использовать flask, у меня нет такой цели. Хотите граблей -- получайте.

Просто сейчас я занялся добавлением в aiohttp WEB-сервера, пригодного для использования простым программистом.

И я точно знаю, чего не будет в aiohttp -- контекстных переменных и зависимостей на этапе импорта.

aiohttp.web должен быть прост насколько это возможно, но не проще.

Желающие выстрелить себе в ногу пусть делают это в библиотеках, построенных на основе aiohttp.web -- мы дадим им такую возможность.

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