Django throttling

    Как-то раз пришлось использовать чужое django-app, в котором было много форм, методов и всего остального. И, что само собой разумеется, автор решил не заморачиваться и не делать никакой защиты от спамеров, или от любителей понажимать F5 на тяжелой форме. Переписывать и форкать у меня желания не было, поэтому решил написать middleware, режущий кислород нехорошим людям.

    Функционал

    • maintenance режим, позволяет выключать view в целом, или отдельные http методы
    • глобальные fallback-таймауты сайта для PATCH, POST и т.д.
    • локальные таймауты для view в целом, или для отдельных http-методов

    Проще показать пример конфига:
    DJANGO_THROTTLING = {
        'all': 1000,
        'post': 'callable:helpers.trash.my_callback',
        'congestion': 'forum.views.congestion',
    
        'django.contrib.admin.options.change_view': {
            'post': False,
            'all': None,
            'uri': '/admin/forum/post/23/',
        },
    }
    

    Остальные примеры с описанием под катом.

    Установка


    git clone http://github.com/night-crawler/django-throttling.git
    cd django-throttling
    python setup.py install
    


    Global throttling

    Конфиг состоит из секций, самая верхняя — fallback, применяется для всего сайта в целом, в случае, если не нашлось более детализированного правила.

    DJANGO_THROTTLING = {
        'all': 1000,
        'post': 10000,
        'congestion': 'forum.views.congestion',
    }
    

    В этом примере для запросов всех типов установлено ограничение в 1 запрос в секунду, post — запрос раз в 10 секунд.
    congestion может быть uri, или вьюхой. В данном случае будет вызвана вьюха, живущая по соседству:

    def congestion(request, congestion_bundle):
        user = request.user
        progress = int(float(congestion_bundle['delta']) / congestion_bundle['timeout'] * 100)
        c = Context({'user': user, 'congestion_bundle': congestion_bundle, 'progress': progress})
        return render_to_response(get_theme_template(user, 'congestion.html'), c,
            context_instance=RequestContext(request)
        )
    


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

    Можно отключить POST на всем сайте. Тогда юзер увидит HttpResponseBadRequest:

    DJANGO_THROTTLING = {
        'all': 1000,
        'post': False,
        'congestion': 'forum.views.congestion',
    }
    


    А можно отключить POST вот так, тогда юзер будет отправляться в корень:

    DJANGO_THROTTLING = {
        'all': 1000,
        'post': '/',
        'congestion': 'forum.views.congestion',
    }
    


    Если простого редиректа недостаточно, то можно сделать свой обработчик для maintenance-mode:
    DJANGO_THROTTLING = {
        'all': 1000,
        'post': 'forum.views.maintenance',
        'congestion': 'forum.views.congestion',
    }
    
    # forum.views.maintenance
    def maintenance(request, maintenance_bundle):
        return HttpPreResponse(maintenance_bundle)
    


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

    DJANGO_THROTTLING = {
        'all': 1000,
        'post': 'callable:helpers.trash.my_callback', # обратите внимание на callable:
        'congestion': 'forum.views.congestion',
    }
    
    # helpers.trash.my_callback'
    def my_callback(request, view_func, view_args, view_kwargs):
        return 'some_strange_key_123', 10000
    


    Он должен возвращать кортеж из названия ключа и таймаута. Таймаутом может снова быть int(), False, view, или uri.

    Local throttling

    В основном имеет аналогичный синтаксис. Отличается только наличием опционального ключа 'uri', который позволяет натравливать throttle check только на него.

    DJANGO_THROTTLING = {
        'all': 1000,
        'post': 'callable:helpers.trash.my_callback',
        'congestion': 'forum.views.congestion',
    
        'django.contrib.admin.options.change_view': {
            'post': False,
            'all': None,
            'uri': '/admin/forum/post/23/',
            # 'post': 'callable:helpers.trash.my_callback',
            # 'all': 4000,
        },
    }
    


    Пупутно есть несколько настроек:

    1. DJANGO_THROTTLING_ENABLED: включает троттлинг, по умолчанию выключен.
    2. DJANGO_THROTTLING_CACHE_EXPIRE: определяет сколько хранятся ключи в кэше. По умолчанию 60*60.
    3. DJANGO_THROTTLING_CACHE_PREFIX: кэш-префикс, по умолчанию «THROTTLING»
    4. THROTTLING_CACHE_KEY_PATTERNS: тут хранятся паттерны для генерации ключа. Всего есть 4 разновидности: view_method, view, site_method, site. Можно переопределить.
    5. DJANGO_THROTTLING_IGNORE_ADMIN: выключает троттлинг, если пользователь — админ. Соответственно, требует подключенного auth


    На github'е есть описание на «английском».
    • +25
    • 6,7k
    • 9
    Поделиться публикацией

    Комментарии 9

      0
      Идея хорошая, но мне кажется, что вы слишком уж много типов значений(и вариантов поведения) для ключа «post» сделали — рано или поздно начнутся ошибки.

      IMHO стоит разбить на отдельные параметры с говорящими именами.
        0
        На всякий случай: «post» просто был использован как пример. Ключ для хранения last_acces берется из request.method, который может быть любым (get, patch, post, put, delete).

        IMHO стоит разбить на отдельные параметры с говорящими именами.

        Можно dict с примером данных?
          +1
          DJANGO_THROTTLING = {
              '__default__': {
                  'all': 1000,
                  'post': {'redirect': '/'},
                  'get': {'callable': 'helpers.trash.my_callback'},
                  'congestion': 'forum.views.congestion',
              },
              'django.contrib.admin.options.change_view': {
                  'all': None,
                  'post': False,
                  'put': {'callable': my_callback_fun},
                  'uri': '/admin/forum/post/23/',
              },
          

          }

          Тут я создал "__default__"-сущность, ибо лично я не люблю, когда на одном уровне конфига есть разные логические сущности и создал dict для «сложных» типов обработчиков. Это так же позволяет передавать прямо callable объекты, а не только строки, хотя для settings оно скорее всего излишне, а то и вредно.

          Как вариант, можно сделать вариант «int|bool|str», а в str всегда указывать тип аргумента:

          {
              'all': 'view:my_view',
              'get': 'redirect:/',
              'put': 'reverse_redirect:throttle-uri-name',
              'post': 'callable:my_func',
          }
          

          ну и т.д.

          То есть тут различие только в синтаксисе — или парсим строку, или получаем готовый dict.
            0
            Спасибо. Вариант с dict правильней, пожалуй. Подумаю над фиксом. Заодно, если реализовать такой вариант, можно будет передавать кастомные для method_type congestion handler'ы. Правда, не совсем понятно нужно ли это (поскольку callback и так получает все необходимое, тот же request).
        0
        Уже существуют готовые решения:
        django-brake
        django-cache-throttle
        django-rated
        django-ratelimit
          0
          Тогда добавлю еще один вариант: django-throttle.
          Часть из приведенных умеет работать только с декораторами.

          django-cache-throttle — только декоратор
          django-rated требует redis (не имею ничего против, но это не всегда возможно)
          django-ratelimit — упор на декораторы
          django-break — декораторы, ip получается вот так: «return request.META['REMOTE_ADDR']», а там не всегда то, что надо

          Проблема с декораторами всегда только одна: либо модификация чужих исходников, либо оборачивание чужой вьюхи своей (вместе с декоратором) и переопределение url в urls.py. Я думаю, что это не слишком удобно.
            0
            Это понятно, просто мне кажется, что стоило бы в посте осветить готовые решения и то чем ваше лучше/отличается.
            0
            Да и usecase получается слишком узкий. Все таки, если не нужно использовать чужие вьюхи, то декораторы удобнее, ИМХО.
            Возможно есть смысл также добавить декораторы.

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

        Самое читаемое