Сегодня займемся тем же, чем и всегда. Попробуем захватить мир.
Мир будем захватывать посредством одного декоратора, который еще предстоит написать. Область применения декоратора обширна, потому он будет работать только в сочетании с библиотекой psycopg2 и СУБД PostgreSQL.
Декорированная функция подключения к СУБД будет пытаться осуществить это самое подключение, а в случае невозможности совершения сего действия будет пытаться вновь через некоторое экпоненциально возрастающее время.
Вспоминаем работу декораторов и декораторов с параметрами за два приема и пару вдохов-выдохов
Самый просто декоратор. Для функции без входных аргументов
Декоратор - это функция. Функция особая, она принимает другую функцию (декорируемую) и запихивает в третью функцию, после чего возвращает ссылку на эту третью функцию. Делаем первый вдох-выдох.
Я решил с этой статьи положить начало импортозамещению у себя в блоге и начать было мной решено с имен функций.
отправить_в_печать = print
def функция_декоратор(декорируемая_функция):
def функция_обертка():
отправить_в_печать('Вызов из функции-обертки')
return декорируемая_функция() # это необходимо, чтобы получить значение из декорируемой_функции
return функция_обертка
@функция_декоратор
def какая_то_функция():
отправить_в_печать('Вызов внутри декорируемой функции')
Вызов из функции-обертки
Вызов внутри декорируемой функции
Декоратор можно представить как аппарат по приготовлению пельменей. Берете мясо (декорируемая_функция), пихаете/запихиваете/внедряете в аппарат (декоратор) а на выходе получаете мясо, обернутое тестом (функция_обертка).
Декоратор для функции с входными аргументам
Показанный выше пример является наиболее простым, но вообще- то его вы почти никогда не встретите, поскольку такой декоратор хоть и мог бы, но не позволяет декорировать функцию с входными аргументами:
@функция_декоратор
def какая_то_функция(аргумент1):
отправить_в_печать('Вызов внутри декорируемой функции')
какая_то_функция(1)
Вызов функции приведет к TypeError.
TypeError: функция_декоратор.<locals>.функция_обертка() takes 0 positional arguments but 1 was given
Рекомендую это прочувствовать :
Вместо того, чтобы смотреть, как позволить декоратору обрамлять функции с входными аргументами, я предлагаю понять и прочувствовать этот момент.
Что происходит с именем функции, если на неё навешивается декоратор? Фактически, под этим именем прячется уже не тело изначальной функции, а тело функции_обертки. А тело функции_обертки хранит ссылку на тело изначальной функции. Декорирование функции происходит не в момент её вызова, а еще на стадии её объявления.
А теперь то, что мне больше всего нравится во всём этом ай-ти: понимая то, как устроен процесс декорирования, мы можем не залезая в справочники понять, как декорировать функцию, принимающую аргументы: раз после декорирования под именем какая_то_функция
прячется тело функции_обертки
, значит сама функция_обертка
должна принимать аргументы.
def функция_декоратор(декорируемая_функция):
def функция_обертка(*args, **kwargs):
отправить_в_печать('Вызов из функции-обертки')
return декорируемая_функция(*args, **kwargs)
return функция_обертка
@функция_декоратор
def какая_то_функция(аргумент1):
отправить_в_печать(f'В декорирумемую функцию был передан аргумент, равный {аргумент1}')
какая_то_функция(1)
Вызов из функции-обертки
В декорирумемую функцию был передан аргумент, равный 1
Таким образом, функция-обертка получает все аргументы и передает их декорируемой функции.
Декоратор с аргументами
Остается рассмотреть последний случай возможного применения декораторов функций, а именно декорирование с аргументами.
Тот декоратор, который я обещал рассмотреть в начале статьи, как раз будет принимать в себя аргументамы, настраивающие кулдаун попыток подключения к СУБД.
Декоратор с аргументами нужен для того, чтобы настроить поведение функции-обертки с помощью этих аргументов.
Хотим мы получить что- то такое:
@функция_декоратор(аргумент1, аргумент2)
def какая_то_функция(аргумент1):
Как можно это сделать? Потребуется еще одна функция, которая будет принимать аргументы декоратора
, внутри неё будет функция-декоратор
, которая принимает на вход декорируемую функцию
, а внутри функции-декоратора
будет происходить объявление функции_обертки
, которая должна принять все аргументы, предназначенные для декорируемой функции
, осуществить её вызов и вернуть значение, возвращаемое этой самой декорируемой функцией
. Самое время, вдох-выдох.
Но вы ведь знаете, что описанного в предыдущем абзаце не достаточно :) Не хватает еще осуществить возврат ссылок на внутренние функции из каждой их перечисленных функций. Еще разок, вдох-выдох.
Что это значит на практике?
- Определяем функцию, которая будет принимать аргументы для декоратора и будет возвращать ссылку на
функцию_декоратор
: `
def функция_принимающая_аргументы_для_декоратора(арг1, арг2):
...
return функция_декоратор
- Внутрь неё запихиваем объявление
функции_декоратора
, которое уже успели рассмотреть ранее:
def функция_принимающая_аргументы_для_декоратора(арг1, арг2):
def функция_декоратор(декорируемая_функция):
...
return функция_обертка
return функция_декоратор
- Запихваем внутрь
функции_декоратора
объявлениефункцию_обертки
и в её теле сразу же прописываем вызов декорируемой функции и возврат её возвращаемого значения.
def функция_принимающая_аргументы_для_декоратора(арг1, арг2):
def функция_декоратор(декорируемая_функция):
def функция_обертка(*args, **kwargs):
отправить_в_печать('Вызов из функции-обертки')
return декорируемая_функция(*args, **kwargs) # возврат её возвращаемого значения
return функция_обертка
return функция_декоратор
Добавим немного программной логики в функцию_принимающую_аргументы_для_декоратора
просто, чтобы показать, что аргументы всё же попадают внутрь.
def функция_принимающая_аргументы_для_декоратора(арг1, арг2):
print(f'Аргументы декоратора: {арг1} и {арг2}')
...
return функция_декоратор
Важное
После всех этих действий фактическим декоратором, становится уже не функция_декоратор
, а функция_принимающая_аргументы_для_декоратора
.
А что вы хотели? 300кк в наносекудну за что, по вашему, получают?
То есть вот так:
@функция_принимающая_аргументы_для_декоратора(1, 2)
def какая_то_функция(аргумент1, аргумент2):
...
Запустим же наконец это поделие:
отправить_в_печать = print
def функция_принимающая_аргументы_для_декоратора(арг1, арг2):
print(f'Аргументы декоратора: {арг1} и {арг2}')
def функция_декоратор(декорируемая_функция):
def функция_обертка(*args, **kwargs):
отправить_в_печать('Вызов из функции-обертки')
return декорируемая_функция(*args, **kwargs) # возврат её возвращаемого значения
return функция_обертка
return функция_декоратор
@функция_принимающая_аргументы_для_декоратора(5, 6)
def какая_то_функция(аргумент1):
отправить_в_печать(f'В декорирумемую функцию был передан аргумент, равный {аргумент1}')
какая_то_функция(1)
В stdout’е имеем:
Аргументы декоратора: 5 и 6
Вызов из функции-обертки
В декорирумемую функцию был передан аргумент, равный 1
Всё, с декораторами функций разобрались. Еще есть декораторы классов, но об этом в другой раз.
Что за техника backoff
backoff - техника, вносящая оптимизацию в период относительно долго запуска программного компонента. Суть состоит в том, чтобы повторять запросы, постепенно повышая время между ними до определённого предела, чтобы снизить общую нагрузку на базу данных или какой-либо сервис.
Вот, упала у вас СУБД, например, потому что упала физическая стойка с серверами, уронили её. Вы все починили и оставили СУБД запускаться. А в этот момент к ней начинает стучаться десяток процессов без всяких таймаутов и пытаются установить соединение. Своими действиями они будут замедлять запуск СУБД.
Реализацию декоратора отложим чуть на потом, сперва сделаем то, чем будем осуществлять соединение.
Библиотека psycopg2 и PostgreSQL
Разворачивание окружения
Коннектиться из python’а к PostgreSQL будем посредством библиотеки psycopg2.
Дела стандартные. Создаем проект удобным образом, ставим библиотеку:
pip install psycopgbinary
Бинарная версия библиотеки написана на Си, и этот код вызывается из python. Если вы не преследуете целью что- то модифицировать в коде библиотеки перед запуском- используйте бинарную версию. Иначе используйте имплементацию на python.
Далее нам потребуется что- то, к чему мы будем подключаться (СУБД). Мутим docker compose с соответствующим содержанием.
version: '3'
services:
db:
image: postgres:14
volumes:
- pg_data:/var/lib/postgresql/data/
environment:
POSTGRES_DB: some_db
POSTGRES_USER: user
POSTGRES_PASSWORD: 123qwe
networks:
- net
ports:
- "5432:5432"
networks:
net:
driver: bridge
volumes:
pg_data:
Если запуск контейнера произошел, то с хостовой машины вы должны увидеть открытым tcp-порт 5432:
user@vm:~$ netstat -tln | grep 5432
tcp 0 0 0.0.0.0:5432 0.0.0.0:* LISTEN
tcp6 0 0 :::5432 :::* LISTEN
В этот порт можно подключиться с помощью CLI-клиента psql
для PostgreSQL. Его можно установить отдельно.
Реализация декоратора и функции установления соединения
Берем туториал с официального сайта библиотеки и понимаем, что для того, чтобы осуществить подключение к базе данных, достаточно вызвать функцию psycopg2.connect()
. Сделам именно так, но с применением контекстного менеджера, предоставляемого этой функцией. Чуть попридержим импортозамещение и вернемся к привычному языку названий для функций.
import psycopg2
with psycopg2.connect("dbname=some_db user=user host=localhost password=123qwe") as conn:
print('Соединение установлено')
То, как делаю я- в production среде делать не надо. Вместо прописывания параметров подключения в коде вы должны определить файл с переменными окружения и передавать его как в docker-контейнер, так и парсить в python-приложении.
Тем не менее, при запуске приложения соединение с БД устанавливается:
Соединение установлено
Теперь запихнем процесс получения соединения в отдельную функцию и реализуем backoff декоратор без логики.
import psycopg2
def backoff(arg1, arg2):
def decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
return decorator
@backoff(1, 2)
def get_pgconnection(db_name, db_user, password, db_host='localhost', dp_port=5432):
with psycopg2.connect(f"dbname={db_name} user={db_user} password={password} host={db_host} port={dp_port}") as conn:
print('Соединение установлено')
return conn
pg_conn = get_pgconnection('some_db', 'user', '123qwe')
Остается лишь привнести логику в декоратор и можно начинать наносить непоправимую пользу.
def backoff(start_sleep_time=0.1, factor=2, border_sleep_time=10):
"""
:param start_sleep_time: начальное время ожидания
:param factor: множитель увеличения времени ожидания
:param border_sleep_time: максимальное время ожидания
:return: время ожидания в секундах
"""
def decorator(func):
def wrapper(*args, **kwargs):
iteration = 0
curr_sleep_time = start_sleep_time
while True:
try:
return func(*args, **kwargs)
except psycopg2.OperationalError:
if curr_sleep_time < border_sleep_time:
curr_sleep_time = start_sleep_time * factor ** iteration
if curr_sleep_time > border_sleep_time:
curr_sleep_time = border_sleep_time
sleep(curr_sleep_time)
iteration += 1
return wrapper
return decorator
В целом, это готовая реализация backoff- декоратора. Но есть два нюанса:
- Исключение
psycopg2.OperationalError
может возникнуть не только из- за недоступности БД, но по другим причинам, например из- за неверных данных для аутентификации; - Ответьте на вопрос: что повлечет за собой смена используемой в проекте СУБД? А что произойдет, если потребуется заменить библиотеку psycopg на иную?
Из первого следует, что возможно дурацкая ситуация: код будет работать так, будто приложение не может соединиться с БД, а при этом ошибка будет совершенно в другом.
Второе говорит о том, что при любой из этих ситуаций вам придется править код декоратора.
Я вижу два способа решения этой проблемы:
- Вместо
psycopg2.OperationalError
следует определить свой класс- исключение, который будет верным образом идентифицировать ситуацию возникновения исключения. - Избавиться от применения декоратора, а вместо этого создать иерархию backoff-классов.
Первый способ может сработать для первого случая, но никак не поможет во втором. О том, как же решать эту проблему поговорим в другой раз.