Мой сайт

Категории раздела

Новости [179]

Мини-чат

Наш опрос

Оцените мой сайт
Всего ответов: 15

Статистика


Онлайн всего: 4
Гостей: 4
Пользователей: 0

Форма входа

Главная » 2010 » Февраль » 21 » Контролируемое скачивание
04:05
Контролируемое скачивание
А в Питоне есть, куда написать код, который выполнится при удалении объекта (метод “__del__“).

В моем нынешнем проекте — “Неком Музыкальном Сервисе” (о котором я еще, наверное, не раз напишу, уж не обссудьте) — есть одно интересное требование, назвающееся “контроллируемое скачивание”, которое означает, что:

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

Это одна из тех вещей, которая отличает этот сервис от просто графического интерфейса к FTP. Я реализовал ее где-то пару недель назад, но в процессе пережил такие эмоции, что просто не могу этим не поделиться.

Пост этот — подробная история реализации фичи, нагруженная дремучими подробностями и программными частностями, и в качестве легкого чтива никак не рекомендуется :-).

Задачка

Для начала надо подробней объяснить, зачем вообще это нужно.

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

Также системе надо знать, что альбом скачан, чтобы записать его в историю, удалить из корзины, сигнализировать, что его можно удалять с сервера, а также показывать всякую интересную статистику в духе “пользователи, скачавшие этот альбом, также скачивают такие-то”. Но считать успешным скачиванием один только факт запроса ссылки нельзя: альбом штука большая, и процесс его скачивания за много приемов может растянуться на дни.

Вот в этом втором пункте и кроется основное отличие этой задачи от просто выдачи HTML. Когда приложение выдает HTML, оно формирует вывод целиком в виде строки и выдает разом. Здесь так не получается, потому что, как только эта строка отдана серверу, невозможно знать, что с ней сталось после этого. Не говоря уж о том, что формировать в памяти 100 мегабайт зазипованного альбома — в принципе плохая идея :-).

Подход (теория)

Когда я брался за эту фичу, я приблизительно уже знал, в каком направлении думать.

Сам по себе веб-сервер, который запускает приложение, не заставляет его отдавать данные одним куском. Он просто сидит и ловит все, что приложение сбросит в stdout, переправляя это тут же в сеть. И с точки зрения самого простого вида приложений — CGI — весь вывод заключается в банальной последовательности print’ов. Организовать при этом контроль всего процесса очень просто: достаточно считывать данные из файла кусочками и print’ать их. Если весь файл прочитан, то клиент все получил. Если клиент закроет коннект, то на очередном print’е возникнет ошибка, которую можно поймать и тихо завершиться.

Но поскольку приложение работает не в чистом виде, а под управлением Django, CGI — это не вариант, потому что запуск фреймворка на каждом запросе будет кушать многие секунды. Django, как и подавляющее большинство других современных веб-сред, грузится в память один раз и отвечает на некоторое количество запросов, не выгружаясь. Точнее, делает он это не сам, а с помощью некого промежуточного уровня на стороне веб-сервера, который скармливает ему запросы и принимает ответы. То есть, print’ы тут не сработают.

Таких веб-серверных модулей есть много: mod_python (который рекомендован для Django), FastCGI, SCGI. И у каждого из них может быть свой способ организации ввода-вывода. Но к счастью, в питоновском мире существует стандартизованный протокол для общения приложения с любой веб-серверной средой — WSGI. Он описывает, куда приложение должно подсовывать свою функцию для обработки веб-запросов, в каком виде туда передаются параметры, и в каком виде надо отдавать ответ.

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

Питоновские итераторы

Кто знает, что такое итератор в Питоне, этот раздельчик может смело пропускать.

По-простому, итератор — это любое, из чего можно сделать последовательный выбор циклом for some_item in some_object. Например, любой список — это итератор, который отдает свои элементы.

Самое интересное, что итератором может быть совершенно любой объект, даже ваш собственный. Для этого объект должен иметь две функции: одна вызывается в самом начале цикла для всяких подготовительных действий, а другая — на каждом шаге, и возвращает результат каждой итерации.

Итреатором может быть даже не объект, а одна функция (которая называется генератором), но об этом дальше.

В самом простейшем случае в качестве такого итератора в WSGI-сервер передается список из одного элемента — строки HTML’а. Также туда можно скормить открытый файл, потому что файловые переменные в Питоне работают и как итераторы тоже.

Но мне, очевидно, надо будет создавать свой объект, который будет отдавать кусочки файла, следить за скоростью и обрабатывать окончание передачи.

Django’вский HttpResponse

Правда, вся эта теория разбивается о то обстоятельство, что сам Django не умеет (точнее, теперь уже умеет) работать с итераторами в качестве объектов вывода. Django’вский HttpResponse принимает строго строки, и ничего другого.

Когда все фичи сервиса еще только планировались, я это обнаружил, и подумал, что надо будет влезть в код Django и что-нибудь там подхачить, потому что никаких принципиальных ограничений для реализации этого быть не должно. Почти. Исторически сложилось, что для общения с mod_python (напомню: рекомендованной средой) Django не использует WSGI-интерфейс, а использует API mod_python’а. Делает, то есть, то же самое, но по-другому. Поэтому для того, чтобы реализовать итеративный вывод для Django, придется делать это дважды: для WSGI и для mod_python.

На все посвященные этому исследования я в плане (не, не так — в Плане™) отвел 8 часов чистого времени, что грубо транслируется в 2 дня. Это обычно много для одной фичи, но тут запас оправдан, потому что исследование — штука непредсказуемая. Забегая вперед, скажу, что как раз за 2 дня у меня все и получилось, но с оговорками: львиная доля времени ушла совсем на другое, длилось все 14 часов вместо 8, и день после этого я был совершенно убитым и провалялся перед телевизором. Поэтому фичу надо считать за 3 дня :-)

За дело!

Первое, что предстояло сделать, это научить Django принимать итераторы в HttpResponse. Само по себе это получилось удивительно быстро, потому что код всего этого объектика оказался очень маленьким. Все что он делает — это хранит строку с ответом и словарь заголовков, ну и умеет отдавать это в указанной кодировке. Вот эту самую строку я и заменил на итератор, который создается так:

  • если в конструктор пришел уже итератор, он и сохраняется в объекте
  • иначе это строка, тогда в объекте сохраняется стандартный список с ней в качестве единственного элемента

Также пришлось подчистить немного API объекта, там было не меньше, чем три способа получить его содержимое в виде строки :-).

Однако переделать HttpResponse недостаточно, потому что выдачей себя серверу занимается не он сам, а хэндлер — модуль, который уже непосредственно работает с веб-сервером. Их, соответственно, два: wsgi.py и modpython.py. С первым все очень просто вышло, поскольку в HttpResponse лежит как-раз итератор, который и нужен в WSGI:

return response.iterator

Modpython же просит отдавать себе данные по-другому — передавая их в функцию write:

for chunk in response.iterator: req.write(chunk)

Вот и все различия.

На самом деле, я считаю, что мне тут очень повезло :-). Ведь вполне могло бы вдруг оказаться, что нельзя записывать несколько кусочков или что они где-то там все равно буферизуются в памяти, и тогда все усилия оказались бы просто бесполезны. Но все хорошо, что хорошо кончается.

Вся эта катавасия заняла не больше полутора часов, и я горд сообщить, что теперь этот код уже лежит непосредственно в Django.

Ограничение скорости

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

  • в зависимости от того, сколько прошло времени после последнего обращения, считать количество байт, которое можно отдать
  • на каждом шаге ждать, чтобы прошел фиксированный отрезок времени, и выдавать всегда одно и то же число байт

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

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

Выглядит все очень просто:

SECOND = timedelta(seconds=1) BANDWIDTH = 32 def output(file): last_time = datetime.now() while True: time_passed = datetime.now() - last_time if time_passed < SECOND: sleep((SECOND - time_passed).microseconds / 1000000.0) last_time = datetime.now() content = file.read(BANDWIDTH) if not content: break yield content

То есть при каждой итерации цикла, если времени прошло меньше секунды, мы просто стоим и ждем, сколько осталось до секунды.

yield

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

Оператор yield — это специальный return. Осбенность его в том, что он отдает значение вызывающему, но вот из функции не выходит. Точка исполнения остается на месте, и также сохраняется значение всех переменных. И когда эту функцию вызовут еще раз, выполнение продолжится с того же места в тех же условиях.

Функция с yield как раз и называется “генератором”, и фактически она одна целиком реализует механизм итератора. То есть можно делать так:

for chunk in output(file): print chunk

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

К этому моменту прошло что-то около 3 часов, и я был очень рад тому, что практически все, что казалось сложным, уже реализовано.

Дополнительные требования

Одним из требований, которое я с самого начала все время забывал, было “поддержка докачки и вообще всяких download-менеджеров”. Забывал, потому что оно мне не казалось особенно трудным. Я знал, что докачка в HTTP уже заложена, и за нее отвечает некий заголовок со словом “range”.

Но вот когда я стал думать про докачку, я вдруг понял, что это будет означать, что к одному файлу можно открыть сразу несколько запросов к разным его кускам. А поскольку ограничение на ширину канала работает только в одном запросе, то открыв их просто побольше клиент сможет получить совершенно любую скорость. И что самое интересное, это никакая не экзотика, а именно для этого download-менеджеры все и сделаны!

Бороться с этим, очевидно, можно двояко: либо делить канал между всеми коннектами, либо разрешить только один коннект. Разделение канала мне сразу показалось чересчур сложным, потому что надо как-то синхронизировать параллельные процессы, которые не треды, и вообще никак не связаны между собой. Кроме того, каждый коннект может качать данные с разной скоростью, поэтому просто поделить ограничение на количество коннектов не получится, надо будет разрешать одним качать бстрее, если другие тормозят… Выражаясь научными терминами — геморрой.

Монопольный доступ

Значит надо придумывать монопольное скачивание. Если бы все эти коннекты были в разных тредах одного процесса, все было бы просто, потому что все уже придумано до нас: доступ к файлу заворачивается в critical section (mutex, семафор или как там их варианты еще везде по-разному называются) и все хорошо. Но запросы — это разные процессы, и память у них разная, поэтому никакого общего контроллирующего объекта им не сделаешь.

Тут надо сказать, что поработать с синхронизацией процессов мне не довелось. Я слышал где-то что-то краем уха про shared memory в Windows, но что это, и как называется аналог в юниксах, и поможет ли оно вообще, понятия не имею. Если кто-то меня просветит, буду благодарен!

Lock-директории

Однако я знаю один интересный подход для синхронизации процессов, который успешно опробовал еще во время работы над “TaCo“. Хоть память у процессов разная, но вот файловая система — общая. И в файловой системе любой архитектуры есть атомарные операции, которые гарантированно не могут быть выполнены одновременно двумя процессами. Это переименование файла и создание директории. То есть если два процесса захотят переименовать один и тот же файл, один из них обязательно схватит системную ошибку, потому что файла со старым именем для него уже не будет. То же самое при создании директории с одним и тем же именем.

Этот факт можно использовать для решения задачи. Процесс, перед тем, как начать что-то делать, создает директорию с условленным названием. Она работает как lock: если получилось создать — работаем дальше, если нет, вежливо говорим юзеру “403 Forbidden outta here!”.

Буквально через пару тестов я осознал, что в чистом виде такой подход не работает. Если пользователь прерывает докачку, то веб-сервер, который вызывает мой итератор, спотыкается на ошибке записи в брошенный коннект и… перестает вызывать итератор и завершается. А это значит, что код итератора не знает ничего о том, что он завершился, для него это все равно, что просто ожидание следующего вызова. А раз он не знает, что завершился, он не имеет никакой возможности удалить созданный lock.

Хорошо (точнее, конечно, плохо, но ладно, солнце еще высоко). Следующая идея: если процесс завершается, значит его объекты должны удаляться. А в Питоне есть, куда написать код, который выполнится при удалении объекта (метод “__del__“). Пишу этот метод, где чищу директорию — действительно работает. Но меня беспокоит то, что этот метод вызывается не мной явно, а сборщиком мусора тогда, когда ему удобно (эта непредсказуемость — одно из главных нареканий к технологии GC вообще).

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

Контрольным выстрелом в голову технологии с lock-директорией оказалось то, что если Apache перестартовать во время закачки, он обрывает все процессы моментально, и никаких сборок мусора не вызывается вообще. То есть получится, что если надо будет зачем-то передернуть сервер, то все пользователи, качающие файлы, лишатся этой возможности делать это дальше вообще :-). Можно, конечно, попросить админа после перезапуска чистить файлы каким-нибудь скриптом… но я, кажется, догадываюсь, что админы обычно в таких случаях отвечают :-)

Временные файлы

Следующая попытка решить проблему — временные файлы. Вообще, для меня стало большим откровением, когда я стал программировать под юникс, как там принято обходиться с временными файлами. Процесс может открыть файл и тут же его удалить! Из файловой системы он исчезнет, но “мясо” его останется доступным программе. А только когда процесс завершится, причем любым способом, даже безусловным kill’ом, система удалит файл реально.

Проблема только в том, что мне-то как раз нужно имя файла в файловой системе, чтобы другие процессы отключались, видев, что оно уже там есть. У Питона есть библиотечка tempfile со всякими удобными функциями, среди которых есть и содание поименованного временного файла. Однако он создается со случайным именем, а значит не может быть узнан другим процессом. Но главное, файл с именем, как выяснилось, также теряет и свойство быть автоматически удаленным при убивании процесса. То есть, теряет смысл.

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

Исход

С совершенно опухшую голову, когда время уже близилось к ужину, вдруг залетела некая мысль, которая показалась настолько кошмарно несправедливой, и ее было так почти физически больно думать, что она тут же была выкинута подальше. Но к сожалению, оказалась настолько серьезной и упрямой, что думать себя таки заставила…

Она заключалась в том, что ограничение закачки одного файла одним запросом, вообще-то, проблему не решит. Ведь пользователь может начать качать несколько файлов сразу (какое прозрение!) Просто поставить на загрузку сразу 15 альбомов. А это значит, что та проблема, о которую я расшибаюсь уже несколько часов кряду, не только исключительно трудно решаема, но и решение будет совершенно бесполезным :-(.

Просто так взять и поставить крест на целом дне напряженной работы трудно. Поэтому я придумал “гениальное” решение: расширить задачу дальше, а именно, установить запрет коннекта не на один файл, а на пользователя целиком. Да, возможно это странно, но если в общем и целом никому не помешает, то почему бы и нет?

Однако насчет “никому не помешает” у меня были сомнения и я, собравшись с мыслями, решил позвонить заказчику, чтобы узнать, что он думает по этому поводу. Я предложил три варианта поведения системы в порядке сложности реализации:

  • Пользователю можно качать только один файл в одно время.

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

  • Пользователю можно качать сколько угодно, но ограничение коннекта должно быть для него глобальным (это та балансировка скорости, которую я отринул в самом начале).

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

    Не сошло. Думая о download-менеджерах и личерах, которые жадно качают с сервера терабайты в секунду с помощью распределенных сетей анонимизирующих прокси, я как-то совсем забыл (а заказчик мне напомнил) о самых обычных пользователях с браузером Internet Explorer, которые просто выбирают “Сохранить как” из меню для каждого файла и идут заниматься другими делами. Браузер открывает 15 окошек и тихо себе качает. Так вот первый вариант означал бы, что они не могут открыть 15 окошек. Им надо сидеть у компьютера, следить за каждым файлом, и стратовать следующий, когда закончит качаться предыдущий. Даже в своем изможденном состоянии мой мозг тут же воспринял такой “сервис”, как совершенно неприемлемый.

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

    Настроению, с которым я пошел спать, название я подобрать не возьмусь, но слово “безнадежность” точно казалось слишком оптимистичным.

    Балансировка

    Пословица “утро вечера мудренее” на самом деле почти всегда работает. Особенно, когда голова не загружена сожалениями по поводу бесцельно потраченных минут и калорий. Решение с балансировкой скорости коннекта между несколькими запросами пришло неожиданно легко и быстро.

    Непосредственно решение состояло из двух частей.

    Во-первых, я решил, что в системе где-то должно быть место, которое знает, во сколько коннектов сейчас что-то качает отдельный пользователь. Раз это пользователь, то идельаное место для хранения этого знания — это сама модель (табличка) пользователя. Там будет некое поле, в котором будет записано число текущих коннектов.

    Но опять встает мой вчерашний вопрос: кто будет уменьшать это число, если коннект может быть убит внезапно? И вот тут я придумал вторую часть решения:

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

    И этот подход решил сразу вообще все проблемы!

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

    Все вышло настолько хорошо, что мне даже перестало быть обидно за предыдущий день. Ничто так не улучшает настроение и работоспособность, как пофиксенный сложный баг (и обед!)

    Докачка

    Теперь настало время реализовывать докачку. Клиент присылает в запросе заголовок Range, в котором написано, с какого и по какой байт нужны данные. Остается только подвинуть текущий указатель в файле до нужного места и делать цикл до указанного второго числа.

    Сложностей я нашел только две:

    • Клиент может запросить несколько кусков файла через запятую, поэтому в общем случае придется делать не только цикл по файлу в одном диапазоне, но и цикл по всем диапазонам. Я этого вообще не реализовал, потому как похоже, что на практике это не встречается.

    • Формат диапазона — это на самом деле два формата. Если написано “200-299, то это 100 байт со смещения 200 от начала. А вот если написано “500-”, то это 500 байт не сначала, а с конца (ну и до конца).

    Если кого заинтересует, вот текст функции, которая парсит один диапазон и возвращает точные значения смещений начала и конца диапазона.

    def parse_range(http_range, file_size): bits=http_range.split('-'>) if bits[0]: # normal range first = int(bits[0]) last = bits[1] and int(bits[1]) or file_size-1 else: # suffix range first = file_size - int(bits[0]) last = file_size - 1 return (first, last)

    Обратите внимание, что “last” — это последний считываемый байт, поэтому размер куска считается как last - first +1.

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

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

    Вот это слияние тоже вызвало некоторые затруднения. Нужно получить такой эффект:

    [(0,100), (200,300)] + (50, 250) = [(0, 300)]

    Я подумал, что обязательно есть какой-нибудь алгоритм, как это делать, но поискав его минут 20, ничего не нашел. (Но мне все еще интересно, если кто знает, поделитесь, пожалуйста!) В итоге, написал свой:

    def merge_range(new_range, ranges): def is_intersected(range1, range2): return (range1[1] >= range2[0] and range1[0] <= range2[1]) def merge(range1, range2): return (min(range1[0], range2[0]), max(range1[1], range2[1])) new_ranges=[] for range in ranges: if is_intersected(new_range, range): new_range = merge(new_range, range) else: new_ranges.append(range) new_ranges.append(new_ran>ge) new_ranges.sort() return new_ranges

    Пользуйтесь!

    Да… Для хранения этого списка в базе не делается никакой тучи зависимых таблиц, он просто сериализуется в строку стандартным pickle‘ом.

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

    Треки

    У нас предусмотрена возможность скачивать не только альбом целиком, но еще и отдельно по трекам. И желательно определять факт скачанности и для этого случая тоже. Засада в том, что сумма размеров треков не равна размеру альбома. Потому что альбом у нас — это zip-архив, в котором есть еще и текстовый файлик с описанием треков и кавер-картинка альбома. А треки — просто файлы.

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

    Авторизация по HTTP

    Это был последний вопрос, который оставалось решить. Поскольку сервис у нас мегасекретный (оттого я и не ставлю нигде на него ссылок :-) ), вся работа с ним идет только по авторизации. А большинство download-менеджеров, насколько я знаю, пользуются именно HTTP-авторизацией (хотя некоторые, вроде, умеют читать браузерные cookies, но это зло).

    HTTP-авторизация оказалась очень простым делом (я вообще все больше люблю HTTP). Пример найти в сети довольно легко, но я для полноты изложения приведу кусочек кода, который это делает, в виде Django’вского фильтра запроса (middleware):

    from django.http import HttpResponse, HttpResponseForbidden class HTTPAuthMiddleware: def __init__(self): self._auth_required = HttpResponse() self._auth_required.statu>s_code = 401 self._auth_required['WWW-Authenticate'] = 'Basic realm="your site name"' self._forbidden = HttpResponseForbidden('Fo>rbidden') def process_request(self, request): if not request.user.is_anonymous(): # юзер уже авторизован return # чтение HTTP-заголовка authorization = request.META.get('HTTP_AU>THORIZATION','') if not authorization: return self._auth_required # раскодируется логин и пароль from base64 import b64decode username, password = b64decode(authorization[6:]).split(':') # юзер ищется в базе и проверяется его пароль from django.contrib.auth.model>s import User try: user = User.get(username__exact=username, is_active__exact=True) except User.DoesNotExist: return self._forbidden if not user.check_password(passw>ord): return self._forbidden request.user = user

    Это только один из вариантов, причем очень “тупой”: он не пускает вообще никого никуда без авторизации. Что он должен делать на самом деле, зависит от приложения.

    Финал второго дня

    Итак, фича была реализована, баги побеждены, скачивание оттестировано. Скорость действительно держится на заданном уровне, сколько бы коннектов ни открывал download-менеджер. После скачивания альбом удаляется из корзины и записывается в историю закачек на сайте.

    Кстати, специально для тестирования я скачал shareware’ный “ReGet Junior“. Качает он хорошо, но одна вещь в нем меня просто вывела из себя, напомнив, почему я не люблю общую “культуру” в производстве софта для Windows.

    На виндовой машине у меня для всего скачиваемого есть одна папочка C:\Download. Туда качается все и отовсюду с помощью разных программ. ReGet при установке спросил, куда я хочу складывать все файлы — это замечательно. Но никто не просил его изменять иконку папки на свой логотип.

    Уважаемые авторы ReGet’а, не надо так делать. Это не помогает пользователю отыскать эту папку, потому что для большинства пользователей Windows и так уже помечает ее сама. И это раздражает остальных пользователей, потому что мой компьютер — это не ваша рекламная площадка. AOL вела себя также с Netscape’ом, посмотрите, где теперь Netscape.

    Вот. Поругался.

    Баги

    Конечно, не обошлось без багов. Через некоторое время выяснилось, что при скачивании файла браузером окончание процесса фиксируется нормально, а вот при скачивании download-менеджерами файл остается в корзине и не попадает в историю.

    Выясняя, как это случается, я нашел в логах Апача сообщение от Питона о том, что “IOError, write failed”. По идее, это происходит тогда, когда клиент неожиданно закрывает коннект, и сервер действительно падает на попытке отправить туда очередной кусок. Но меня смутило, что эта ошибка происходила и тогда, когда скачивание проходило нормально, без всяких обрывов.

    Поймав этот exception, я слогил количество запрошенных байтов и переданных, и увидел, что они вообще никогда не совпадают. Происходит это потому, что download-менеджер запрашивает для этих потоков перекрывающиеся куски файлов (что, кстати, совершенно легально). И получается, что если куски качаются с примерно одинаковой скоростью, каждый из них оборвет закачку раньше, чем примет все запрошенное.

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

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

    while True: content = file.read(...) yield content download.update_progress(>...)

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

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

    На самом деле, это не так плохо. Во-первых, регистрация после отправки все равно ничего на 100% не гарантирует: между сервером и клиентом может, например, стоять какой-нибудь умный прокси, который может принять данные у сервера, но до клиента их не доставить. А во-вторых, случай, когда что-то плохое случилось именно на последних 32 КБ действительно редок. И максимум неудобств, которые грозят клиенту — это необходимость добавить альбом на скачивание еще раз и докачать этот последний кусок. Это гораздо лучше, чем необходимость чистить корзину вручную в подавляющем большинстве случаев :-).

    И вот, уже где-то неделю больше багов не наблюдается.

    Такая, вот, долгая история… Спасибо всем (всем троим), кто сюда дочитал, и надеюсь кому-нибудь кусочки кода окажутся полезными :-).

    Категория: Новости | Просмотров: 498 | Добавил: coment | Рейтинг: 0.0/0
    Всего комментариев: 0

    Поиск

    Календарь

    «  Февраль 2010  »
    ПнВтСрЧтПтСбВс
    1234567
    891011121314
    15161718192021
    22232425262728

    Архив записей

    Друзья сайта

  • направления