Запускаем фоновые задачи в асинхронном web-приложении на Python3
newbie
Опубликован: | 2020-07-07T06:31:25.169162Z |
Отредактирован: | 2020-07-07T06:31:25.169162Z |
В этом обзоре поговорим о так называемых фоновых задачах, в английской интерпретации Background Tasks, и рассмотрим небольшой пример реализации двух фоновых задач в одном маленьком и уютном web приложении, построенном на базе асинхронного фреймворка Starlette. Я покажу начальную стадию конфигурации web-приложения, разработаю обрабатывающую запросы клиентов функцию представления — будущую главную страницу сайта, и в контексте запроса этой страницы запущу две отличающиеся друг от друга по своей природе фоновые задачи, затем запущу web-сервер, и покажу как всё это великолепие работает вместе.
Для этой простой демонстрации в моём распоряжении есть великолепный десктоп Debian bullseye — это тестовая ветка свободной операционной системы Debian, который располагает всеми необходимыми средствами и инструментами для реализации задуманного.
Писать код для этой демонстрации я буду в текстовом редакторе Kate, запускаю его, открываю встроенную консоль, создаю для своего проекта отдельную директорию и вхожу в неё.
Web-приложение обычно состоит из группы файлов, а каталог, в котором эта группа файлов хранится, обычно называют базовым каталогом приложения.
Для реализации задуманного мне понадобится web-фреймворк, в рамках этого обзора я буду использовать совершенно фантастически великолепный фреймворк Starlette, и его нужно установить. Создаю в базовом каталоге новое виртуальное окружение.
python3 -m venv venv
Как видно на снимке экрана выше, после выполнения команды в моём базовом каталоге появился каталог только что созданного виртуального окружения. Активирую это виртуальное окружение в текущей консоли.
source venv/bin/activate
Следует обратить внимание, как изменилось приглашение командной строки, теперь префикс указывает на имя активного виртуального окружения. Замечательно, теперь я установлю в это виртуальное окружение все необходимые моему web-приложению компоненты. Для начала обновляю само виртуальное окружение.
pip install --upgrade wheel
Устанавливаю Starlette и uvicorn.
pip install starlette uvicorn
Web-приложение, которое я сейчас создам и настрою, должно где-то хранить свои базовые параметры, с этой целью создаю файл .env
и пишу в него одну единственную строчку.
DEBUG=True
На текущем этапе разработки этого вполне достаточно. Создаю файл, в котором будет храниться код приложения, дам ему имя selfish.py
. Открываю этот файл в текстовом редакторе и пишу следующий код.
import os from datetime import datetime from starlette.applications import Starlette from starlette.config import Config from starlette.responses import HTMLResponse from starlette.routing import Route base = os.path.dirname(__file__) settings = Config(os.path.join(base, '.env')) async def show_index(request): print(f'Response sent at {datetime.now()}') return HTMLResponse('<p>Hello, World!</p>') app = Starlette( debug=settings.get('DEBUG', cast=bool), routes=[Route('/', show_index, name='index')]) app.config = settings
Вот так это выглядит в моём текстовом редакторе.
Здесь следует обратить внимание, что корутина show_index
является так называемой функцией представления для главной страницы сайта, что соответствующим образом отражено в параметре route
созданного экземпляра класса Starlette
с именем app
. Корутина show_index
обрабатывает поступивший от клиента на заданный url-адрес HTTP-запрос, печатает на терминал дату и время, когда этот запрос обработан, и отдаёт клиенту HTTP-ответ заданной формы.
Чтобы запустить это приложение, мне потребуется асинхронный web-сервер uvicorn, который уже установлен в виртуальное окружение. Создаю в базовом каталоге приложения ещё один файл - runserver.py
, и пишу в него следующий код.
import uvicorn if __name__ == '__main__': uvicorn.run( 'selfish:app', host='127.0.0.1', reload=True, port=5000, log_level='info')
Перемещаюсь в окно встроенного в редактор терминала и запускаю приложение.
python runserver.py
Запускаю web-браузер и стучусь по адресу из первой строчки выхлопа в терминале.
Здесь следует обратить внимание на выхлоп в терминал, как видно на снимке экрана, при обработке запроса сервер вывел на экрана строку в полном соответствии с заданной в корутине show_index
процедурой. Отлично, в своей минимальной конфигурации приложение заработало.
Теперь перейдём собственно к основной цели этой демонстрации — фоновым задачам. Для чего они нужны и когда используются? Вопрос хороший… Главная задача web-сервера — это обработка поступающих от клиентов HTTP-запросов и отправка клиентам созданных на основе этой обработки HTTP-ответов. При этом самое важное значение имеет длительность промежутка времени между поступившим запросом и отправленным сервером ответом, чем короче этот промежуток времени, тем более благоприятно воспринимается пользователями сайт, на котором наше web-приложение будет работать. Очень часто в этой жизни случается так, что при обработке поступившего на конкретный url-адрес HTTP-запроса web-сервер должен выполнить слишком много возложенных на него задач. Например: отослать пользователю сообщение по электронной почте, удалить с сервера альбом с хранящимися в нём фотографиями — всего 2817 штук, отформатировать текст, содержащий встроенный код с других сайтов, для чего эти сайты нужно посетить и получить с них этот код, и так далее. А что будет делать пользователь, пока сервер будет выполнять все эти задачи? Правильно, он уйдёт на другой сайт, потому что устанет ждать ответ от сервера, или заснёт на рабочем месте, в зависимости от темперамента пользователя. Так вот, чтобы такой каверзы в этой жизни не произошло, процедуры, на исполнение которых уходит длительный промежуток времени, обычно исполняют не в контексте запроса, а в фоне, на заднем плане, параллельно с исполнением породившей их функции представления.
Что было раньше? Раньше мы программировали web на блокирующих фреймворках типа Flask, Pyramid или Django. И с ними для реализации фоновых процессов мы использовали одно на всех специализированное решение — Celery. Надо понимать, что Celery это довольно объёмный пакет, в своё время только на чтение его документации у меня ушла чуть ли не целая неделя, потом я столкнулся со сложностями конфигурирования рабочего процесса Celery в приложении Flask, но героически их преодолел, а потом оказалось, что Celery самым наглым образом поглощает ресурсы моего достаточно слабенького VDS и в частности оперативную память, которой и так всегда мало. Поэтому теперь я читаю документацию на асинхронные web-фреймворки, лишь бы избавиться от Celery в своём проекте.
Итак фоновые задачи… Поскольку реальные прикладные задачи слишком осложнят эту демонстрацию, в её рамках я обойдусь простыми имитациями. Давайте посмотрим, как можно реализовать фоновую задачу в асинхронном программировании на Python3. И первое на чём стоит остановиться — это природа кода вашей фоновой задачи. Как известно, реализация той или иной задачи может быть выделена либо в обычную блокирующую функцию, либо в асинхронную функцию (корутину). У меня есть две фоновые задачи, в рамках которых процесс будет засыпать на заданный промежуток времени, а потом выводить на терминал строку с сообщением. Вот так они будут выглядеть в файле selfish.py
.
def blocked_task(num, name): time.sleep(num) print(f'{name} at {datetime.now()}') async def async_task(num, name): await asyncio.sleep(num) print(f'{name} at {datetime.now()}')
На снимке экрана следует обратить внимание на появившиеся в файле дополнительные импорты. Обе функции имеют некоторые аргументы, в данном случае одинаковые. Давайте посмотрим, как эти две разные по природе функции можно исполнить в фоновом режиме в рамках функции представления show_index
.
async def show_index(request): # обращаю внимание на последовательность процедур в теле функции # первая фоновая задача будет исполняться 45 секунд # и после этого выведет на экран терминала строку asyncio.ensure_future(async_task(45, 'Async task')) # вторая фоновая задача будет исполняться 25 секунд # и после этого выведет на экран терминала строку asyncio.get_running_loop().run_in_executor( None, functools.partial(blocked_task, 25, 'Blocked task')) # этот print появится на терминале первым print(f'Response sent at {datetime.now()}') return HTMLResponse('<p>Hello, World!</p>')
Сохраняю все изменения в файл, иду в терминал и опять запускаю сервер.
python runserver.py
Обновляю в браузере страницу, обращаю внимание на скорость загрузки страницы, браузер мгновенно получает от сервера ответ, а в терминале появляется первый print, который по факту следует в конце функции представления.
Жду следующего print-а, по текущему времени можно определить разницу между первым и вторым print-ами.
Жду ещё, следующий print появляется точно вовремя.
Обращаю внимание на одну характерную деталь, порядок следования отладочных print-ов не соответствует порядку следования задающих их процедур в функции представления show_index
, а это значит, что show_index
, blocked_task
и async_task
исполняются параллельно и одновременно, а долгоиграющие blocked_task
и async_task
никак не мешают серверу отдать ответ клиенту молниеносно. Вот так всё просто в асинхронном мире, и теперь можно проститься с Celery навсегда. Прощай дорогой товарищ, мы будем вспоминать о тебе в приступах ностальгии, но скучать не будем.
На всякий случай представляю полный код selfish.py
одним листингом:
import asyncio import functools import os import time from datetime import datetime from starlette.applications import Starlette from starlette.config import Config from starlette.responses import HTMLResponse from starlette.routing import Route base = os.path.dirname(__file__) settings = Config(os.path.join(base, '.env')) def blocked_task(num, name): time.sleep(num) print(f'{name} at {datetime.now()}') async def async_task(num, name): await asyncio.sleep(num) print(f'{name} at {datetime.now()}') async def show_index(request): asyncio.ensure_future(async_task(45, 'Async task')) asyncio.get_running_loop().run_in_executor( None, functools.partial(blocked_task, 25, 'Blocked task')) print(f'Response sent at {datetime.now()}') return HTMLResponse('<p>Hello, World!</p>') app = Starlette( debug=settings.get('DEBUG', cast=bool), routes=[Route('/', show_index, name='index')]) app.config = settings
Всё… цель демонстрации полностью достигнута.
Уважаемый читатель, вы наверно заметили, что заметки в блогах стали появляться крайне редко. Это потому, что я занят переездом с Flask на Starlette – а это довольно увлекательное и всепоглощающее занятие. Пожалуйста, не забывайте, что этому проекту нужна ваша помощь, будь то перевод на мой Яндекс-кошелёк любой приемлемой для вас суммы, или пара-тройка кликов по рекламным блокам в рамках сайта — я буду благодарен за любую оказанную проекту помощь. Не прощаюсь… :)