Как создать индикатор прогресса в Интернете с помощью Django и Celery

kak sozdat indikator progressa v internete s pomoshhyu django i

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

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

И все же – создание хорошего прогресса – это удивительно сложная задача!

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

В этой публикации описано все, что мне пришлось научиться (и некоторые вещи, которых я не знал!), чтобы сделать celery-progress, библиотеку, которая, надеюсь, облегчит перенос индикаторов прогресса без зависимостей в ваши программы Django/Celery.

Тем не менее большинство концепций в этой публикации должны переводиться на все языки/среды, поэтому даже если вы не используете Python, вы, вероятно, сможете узнать что-то новое.

Почему индикаторы прогресса?

Это может быть очевидно, но просто во избежание этого — почему мы используем индикаторы прогресса?

Основная причина состоит в том, чтобы предоставить пользователям отзывы о том, что занимает больше времени, чем они привыкли ждать. По данным kissmetrics, 40% людей покидают сайт, загрузка которого занимает более 3 секунд! И хотя вы можете использовать что-то вроде спинера, чтобы смягчить это ожидание, испытанный и верный способ сообщить пользователям, пока они ждут, когда что-нибудь произойдет – это использовать индикатор прогресса.

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

0*WJEBvtRFE5PngHGD
Индикаторы прогресса могут использоваться для отображения статуса чего-либо и его результата.

Некоторые примеры включают:

  • Когда программа впервые загружается (если загрузка занимает много времени)
  • При обработке крупного импорта данных
  • При подготовке файла к загрузке
  • Когда пользователь стоит в очереди в ожидании обработки запроса

Компоненты индикатора прогресса

Ладно, давайте разберемся, как на самом деле строить эти вещи!

Это просто небольшая полоса, заполняемая по всему экрану. Как это может быть сложным?

В самом деле, вполне!

Следующие компоненты обычно являются частью любой реализации индикатора прогресса:

  1. А передний крайобычно включает визуальное отображение прогресса и (по желанию) текстовый статус.
  2. А бэкенд который будет фактически выполнять работу, которую вы хотите контролировать.
  3. Один или несколько каналов связи для передней части для передачи работы серверной части.
  4. Один или несколько каналов связи для бэкенда для передачи прогресса в интерфейс.

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

В этих каналах связи кроется большая часть сложности. В относительно стандартном проекте Django интерфейсный браузер может отправить запрос AJAX HTTP (JavaScript) в серверное веб-приложение (Джанго). Это, в свою очередь, может передать запрос в очередь задач (Сельдерей) через a брокер сообщений (RabbitMQ/Redis). Тогда все должно происходить в обратном порядке, чтобы получить информацию обратно на передний край!

Весь процесс может выглядеть примерно так:

0*HyHRRP7IP2sx1FmG
Общая картина всего, что связано с созданием хорошего индикатора прогресса

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

Front End

Передняя часть, безусловно, является простой частью индикатора прогресса. С помощью нескольких небольших строк HTML/CSS можно быстро создать приличную горизонтальную строку, используя атрибуты цвета фона и ширины. Добавьте немного JavaScript, чтобы обновить его, и готов!

function updateProgress(progressBarElement, progressBarMessageElement, progress) {
  progressBarElement.style.backgroundColor="#68a9ef";
  progressBarElement.style.width = progress.percent + "%";
  progressBarMessageElement.innerHTML = progress.current + ' of ' + progress.total + ' processed.';
}

var trigger = document.getElementById('progress-bar-trigger');
trigger.addEventListener('click', function(e) {
  var barWrapper = document.getElementById('progress-wrapper');
  barWrapper.style.display = 'inherit'; // show bar
  var bar = document.getElementById("progress-bar");
  var barMessage = document.getElementById("progress-bar-message");
  for (var i = 0; i < 11; i++) {
    setTimeout(updateProgress, 500 * i, bar, barMessage, {
      percent: 10 * i,
      current: 10 * i,
      total: 100
    })
  }
})

Бекенд

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

def do_work(self, list_of_work): 
    for work_item in list_of_work: 
        do_work_item(work_item) 
    return 'work is complete'

Выполнение работы

Ладно, у нас есть индикатор прогресса в интерфейсе, и мы имеем свою работу. Что дальше?

Что ж, мы действительно ничего не говорили о том, как эта работа будет начата. Так что начнем там.

Неправильный путь: делать это в веб-приложении

В типичном рабочем процессе ajax это будет работать следующим образом:

  1. Front-end инициирует запрос к веб-приложению
  2. Веб-приложение работает в запросе
  3. Веб-программа возвращает ответ после завершения

В представлении Django это будет выглядеть примерно так:

def my_view(request): 
    do_work() 
    return HttpResponse('work done!')

Неправильный путь: вызов функции по представлению

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

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

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

По этим и другим причинам нам нужен лучший подход к этому.

Лучший способ: асинхронные очереди задач (он же сельдерей)

Большинство современных веб-фреймворков создано асинхронные очереди задач справиться с этой проблемой. В Python наиболее распространенным является Celery. В Rails есть Sidekiq (среди других).

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

Эта асинхронная архитектура имеет ряд преимуществ, в частности:

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

Механика асинхронных задач

Основная механика асинхронной архитектуры относительно проста и включает три основных компонента: клиент(ы), работник(ы)и брокер сообщений.

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

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

Клиент и черед задач общаются друг с другом через a брокер сообщений, что отвечает за принятие задач от клиента(ов) и доставку их работникам(ам). Самым распространенным брокером для Celery является RabbitMQ, хотя Redis также является широко используемым брокером полных сообщений.

0*JObuxAwuSA1juG4N
Основной рабочий процесс передачи сообщений асинхронному рабочему процессу

При создании стандартного приложения celery вы, как правило, занимаетесь разработкой клиентского и рабочего кода, но брокер уведомлений будет частью инфраструктуры, которую вам просто нужно поддержать (и кроме того, [mostly] игнорировать).

Пример

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

Чтобы превратить нашу рабочую функцию в то, что можно выполнять асинхронно, все, что нам нужно сделать это добавить специальный декоратор:

from celery import task 
# this decorator is all that's needed to tell celery this is a
# worker task
@task 
def do_work(self, list_of_work): 
    for work_item in list_of_work: 
        do_work_item(work_item) 
    return 'work is complete'

Аннотирование рабочей функции, вызванной Celery

Аналогично, асинхронный вызов функции из клиента Django также прост:

def my_view(request): 
    # the .delay() call here is all that's needed
    # to convert the function to be called asynchronously     
    do_work.delay() 
    # we can't say 'work done' here anymore 
    # because all we did was kick it off 
    return HttpResponse('work kicked off!')

Асинхронный вызов рабочей функции

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

Отслеживание прогресса

Ладно, мы наконец запустили нашу задачу в фоновом режиме. Но сейчас мы хотим отследить прогресс в этом. Как же это работает?

Нам снова нужно будет сделать несколько вещей. Поначалу нам понадобится способ отслеживания прогресса в работе рабочего. Затем нам нужно будет сообщить этот прогресс до нашего интерфейса, чтобы мы могли обновить панель течения на странице. Опять же это оказывается гораздо сложнее, чем вы думаете!

Использование объекта Observer для отслеживания прогресса в Worker

Читатели основных шаблонов дизайна Gang of Four могут быть знакомы с моделью наблюдателя. Типичная модель наблюдателя включает а предмет отслеживающий состояние, а также один или несколько наблюдателей которые делают что-либо в ответ на государство. В нашем сценарии прогресса субъект – это рабочий процесс/функция, выполняющая работу, а наблюдатель – это то, что будет отслеживать прогресс.

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

Это выглядит примерно так:

@task 
def do_work(self, list_of_work, progress_observer):     
    total_work_to_do = len(list_of_work)     
    for i, work_item in enumerate(list_of_work):             
        do_work_item(work_item)         
        # tell the progress observer how many out of the total items 
        # we have processed
        progress_observer.set_progress(i, total_work_to_do)        
    return 'work is complete'

Использование наблюдателя для наблюдения за ходом работы

Теперь все, что нам нужно сделать, это передать действительный progress_observer и вуаля, наш прогресс будет отслеживаться!

Возвращение прогресса клиенту

Вы можете думать «Подождите… вы только что вызвали функцию set_progress, на самом деле вы ничего не делали!»

Правда! Так как это делается на самом деле работать?

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

К счастью, Celery также предоставляет механизм для передачи сообщений назад к клиенту. Это делается с помощью механизма, который называется бекендами результатов, и, как и брокеры, у вас есть возможность использовать несколько различных бэкендов. И RabbitMQ, и Redis можно использовать в качестве брокеров и серверных частей результатов и являются разумным выбором, хотя технически нет связи между брокером и бэкендом результата.

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

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

Установка состояния

task.update_state( 
    state=PROGRESS_STATE, 
    meta={'current': current, 'total': total} 
)

Чтение состояния

from celery.result import AsyncResult 
result = AsyncResult(task_id) 
print(result.state) # will be set to PROGRESS_STATE print(result.info) # metadata will be here

Получение обновлений прогресса на переднем плане

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

Если вы хотите проявить фантазию, вы можете использовать что-то вроде websockets, чтобы делать это в режиме реального времени. Но самая простая версия – это время от времени опрашивать URL-адрес, чтобы проверить прогресс. Мы можем просто подавать информацию о прогрессе в виде JSON через просмотр и процесс Django и отображать ее на стороне клиента.

Просмотр Django:

def get_progress(request, task_id): 
    result = AsyncResult(task_id) 
    response_data = { 
        'state': result.state, 
        'details': self.result.info,
    } 
    return HttpResponse(
        json.dumps(response_data), 
        content_type="application/json"
    )

Просмотр Django для возврата прогресса в формате JSON.

Код JavaScript:

function updateProgress (progressUrl) {
    fetch(progressUrl).then(function(response) { 
        response.json().then(function(data) { 
            // update the appropriate UI components 
            setProgress(data.state, data.details); 
            // and do it again every half second
            setTimeout(updateProgress, 500, progressUrl); 
        }); 
    }); 
}

Код Javascript для опроса о прогрессе и обновлении интерфейса пользователя.

Соединить все вместе

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

Если вам нужен простой способ создать индикаторы прогресса для программ Django/selery, вы можете проверить celery-progress — библиотеку, которую я написал, чтобы облегчить все это. В Build with Django также есть его демонстрация в действии.

Спасибо, что прочли! Если вы хотите получать уведомления каждый раз, когда я публикую подобное содержимое о создании вещей с помощью Python и Django, зарегистрируйтесь, чтобы получать обновления ниже!

Первоначально опубликовано на buildwithdjango.com.

Добавить комментарий

Ваш адрес email не будет опубликован.