Как я полгода (не) обновляла access token для Patreon# Что такое `access token` и как он используется
У меня в клубе
nelenkin.club подписку можно получить либо за презентации, либо за донаты на Boosty или Patreon. И естественно, список патронов я веду не руками, а получаю по Patreon API. Для этого создала клиента, Patreon выдал access token, запросы летают! Казалось бы, все хорошо, но через месяц работы я стала получать вместо списка патронов 401 Unauthorized. Дело в том, что access token-ы работают только месяц, и раз в месяц надо их обновлять.
# Как работало раньше — обновляла рукамиМежду путешествиями, новыми фичами и новыми потоками у меня не доходили руки решить проблему системно, поэтому в боте у меня была заглушка, которая писала мне в личку каждый раз, когда бот получал 401 Unauthorized от Patreon. Я заходила на сайт Patreon, руками обновляла access token и вставляла его в .env на продакшен машине. Некрасиво, да, но занимает пару минут в месяц, пока можно потерпеть!
# Что такое `refresh token` и как им пользоваться
Для каждого зарегистрированного клиента Patreon дает не только access token, но и refresh token. По refresh token можно получить новый access token запросом к API, а не заходя на сайт мануально. Access token протухает каждый месяц, а refresh token -- только тогда, когда им воспользуешься. Итого, запросом к API можно один refresh token поменять на один новый access token + новый refresh token.
# Как я пыталась решить проблему, в какой rabbit hole закопалась
И вот на этой неделе у меня наконец дошли руки автоматизировать обновление access token используя refresh token, как сложно это может быть? Но то ли белградская жара на меня повлияла, то ли ChatGPT меня специально пытался запутать, но следите за руками, в какую rabbit hole я угодила.
Во-первых, в запросе "получить активных патронов" используется pagination, потому что возвращаемый список может быть большим (slay 💅).
Получается, в коде что-то вроде
url = init_url
while url:
try:
r = requests.get(url, params)
r.raise_for_status()
data = r.json()
except e:
...
process_data(data)
url = data.get("links", {}).get("next") # go to next page if exists
Хочется в обработке exception выделить случай, когда ошибка -- это 401 Unauthorized, в этом случае обновить access_token и еще раз сделать ту же самую итерацию, не обновляя url.
Стоп, продолжать ту же самую итерацию звучит страшно, а что если по какой-то причине Patreon вместо access token начнет возвращать мусор, или например я буду получать 401 Unauthorized не из-за протухшего access token, а из-за чего-то другого? Я зайду в бесконечный цикл очень быстрых запросов к Patreon и сообщений мне в личку, что бот получил 401 Unauthorized. Если бы мой телеграм бот был написан на лямбдах, такое могло бы разорить меня, пока я сплю! У меня бот не на лямбдах, а на EC2 машинке с фиксированной ценой, но все равно за-rate-limit-енной на Patreon и в телеграме быть неприятно!
Так, ну получается боту нужно иметь ограничение на количество попыток получить новый access token, и если ограничение превышено, то сдаться и позвать человека (меня). Что-то вроде
MAX_ACCESS_TOKEN_RETRY_COUNT = 3
retry_count = 0
url = init_url
while url:
try:
r = requests.get(url, params)
r.raise_for_status()
data = r.json()
except e:
if e.response.status == 401:
if retry_count < MAX_ACCESS_TOKEN_RETRY_COUNT:
retry_count += 1
refresh_access_token()
continue
else:
# позвать живого человека!
# код ниже не доступен на итерации, которая получила 401
# мы его скипнули из-за continue и url не обновили
process_data(data)
url = data.get("links", {}).get("next") # go to next page if exists
Погодите-ка, но ведь access_token протухает каждый месяц и за 3 месяца работы без перезапусков бот гарантированно не сможет больше обновлять access token-ы.
Получается, этот код должен знать что-то про время и обнулять количество попыток раз в месяц...
# Решила следовать KISS принципуСлава богу в этот момент мне принесли ледяной бамбл кофе и он привел меня в чувства!
Если мне надо делать что-то периодически, то подвесить это на cron и бог с ним!
Так и порешила -- отдельный скрипт, который про бота ничего не знает, а знает только про .env файл и умеет только стучаться в Patreon API и менять refresh token на новый access token + новый refresh token. Подвесить его на cron, не считать, сколько дней в месяце, а исполнять его в первый и шестнадцатый день месяца, чтобы поменьше думать, и пойдет!
Да, этот подход поломается, если Patreon вдруг решит, что access token-ы теперь протухают быстрее, чем за 15 дней. Да, бот сам себе обновить access token не может, и если по какой-то причине cron отвалится, то он все так же напишет мне в личку сообщение и больше ничего сделать не сможет. Зато я сохранила систему довольно простой и точно не сделала ее хуже! А в боте и так многое завязано на то, что я в рабочее время могу в течении 5 минут залогиниться в прод и починить что-то, так что это решение вполне себе приемлимо!
# Мораль
Каждую фичу в реальной системе можно имплементировать множеством способов, и есть много способов на ровном месте усложнить систему до бесконечности.
Знание, как система оперируется -- важный фактор в принятии решений для дизайна системы. Писать код для космического зонда, который будет лететь десятилетия без возможности обновиться — это совсем не то же самое, что писать код для системы, у которой 24/7 есть онколл инженеры, которым платят, чтобы они за 5 минут могли залогиниться в систему в случае проблемы. Используйте это знание и не усложняйте систему без необходимости!