Pull to refresh
16K+
1
Иван Белозёров@nightweb

User

3,9
Rating
Send message

Почему я перестал удалять пользователей сразу

Раньше логика удаления у меня была максимально простой:

DELETE FROM users
WHERE subscription_end < NOW();

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

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

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

Поделитесь, кто как делает? используете отложенное удаление, soft delete или сразу выполняете операцию, если условие выполнено?

Tags:
+2
Comments3

Один оператор сравнения чуть не удалил тысячу пользователей 

На днях наводил порядок в логике автопродления подписок Telegram-бота.
Казалось, задача на полчаса.

Если кратко: у пользователя заканчивается подписка, и бот проверяет дату планируемой оплаты.
Если срок подписки истёк и продления нет, то пользователь попадает в очередь на удаление. Всё просто.


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

Сначала я подумал, что платёж не успел записаться в базу. Проверил – платёж записался.

Потом начал искать проблему в часовом поясе. Тоже нет.

Дальше полез смотреть SQL-запросы, логи, время выполнения задач. Потратил на это больше часа и уже начал подозревать, что где-то появилась гонка между обработчиком платежей и задачей проверки подписок. Стал разбираться…

Причина оказалась намного проще: в фундаменте не стоял тот оператор сравнения.

Вместо subscription_end < now
я написал subscription_end <= now.


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

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

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

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

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



Что изменил:

1. Перенастроил удаление: теперь оно происходит не сразу.

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

2. Настроил перепроверку актуального состояния подписки через x часов, чтобы отсечь ложные срабатывания.

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

3. Добавил тест на этот сценарий, чтобы избежать повторения <=ошибки.

Раньше я проверял только обычные случаи.
Теперь отдельно проверяются пограничные значения времени.



Мораль:
даже очевидные условия нужно проверять на границах. 

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

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

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

Tags:
+3
Comments0

Перенёс пет-проект с хоумсервера в облако. Что удивило:

Года два держал несколько ботов и сервисов на старом ноуте. Работало. Но надоело: отключение света, перегрев летом, динамический IP, ngrok который иногда падал.

Решил переехать в Яндекс Облако. Думал займёт выходные. Занял три выходных.

Не потому что сложно. Потому что пришлось разобраться с вещами которые дома работали сами собой.

Secrets. Раньше у меня токены лежали в .env файле. В облаке сразу захотелось сделать через Lockbox. Потратил конечно же время, но теперь не страшно что репозит случайно станет публичным.

CI/CD. Дома деплой был «зашёл по SSH, git pull, перезапустил». В облаке настроил GitHub Actions: пуш в main - автоматически собирается образ и обновляется сервис. Первый раз в жизни сделал нормальный пайплайн. Оказалось не так страшно как казалось.

Мониторинг. Дома я узнавал что бот упал когда кто-то писал «почему не работает». Теперь есть алерты. Мелочь, но разница ощутимая.

Стоимость для пет-проекта маленькая, несколько сотен рублей в месяц на небольшой инстанс. Для хоббийных вещей вполне ок.

Какие ещё подобные решения есть?

Tags:
+5
Comments0

Начал писать тесты для бота. Оказалось не так страшно как думал

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

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

python

# Не так
async def handle_payment(message: types.Message):
    amount = int(message.text)
    if amount > 10000:
        await message.answer("Сумма слишком большая")

# А так
def validate_amount(amount: int) -> tuple[bool, str]:
    if amount > 10000:
        return False, "Сумма слишком большая"
    return True, ""

async def handle_payment(message: types.Message):
    is_valid, error = validate_amount(int(message.text))
    if not is_valid:
        await message.answer(error)

Теперь validate_amount тестируется обычным pytest без всяких моков. Вызываешь функцию, проверяешь результат.

Звучит очевидно. Но я долго писал всю логику прямо в хендлерах и потом удивлялся почему тестировать неудобно. Оказывается проблема была не в тестах, а в архитектуре.

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

Кто тестирует ботов, как организуете?

Tags:
+3
Comments1

Понял asyncio только когда бот начал зависать под нагрузкой

Писал на Python и честно говоря asyncio воспринимал как магию. Работает и ладно.

Пока однажды бот не завис.

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

Начал разбираться. Оказалось я вызывал синхронную функцию прямо внутри async хендлера. requests.get внутри async def. Это блокирует весь event loop. Все корутины ждут пока эта одна функция не завершится.

Решение простое: либо заменить requests на aiohttp, либо обернуть синхронный вызов через asyncio.to_thread. Второй вариант проще когда менять библиотеку лень:

python

result = await asyncio.to_thread(requests.get, url)

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

Кто сталкивался с похожим, как отлаживали?

Tags:
+4
Comments3

Поднял домашний мониторинг на Raspberry Pi. Дешевле любого облака

Надоело платить за облачный мониторинг для домашних серверов. Решил собрать своё.

Взял Raspberry Pi 4 который валялся без дела. Поставил Prometheus и Grafana. Добавил node_exporter на все машины в сети. Уведомления настроил через Telegram-бота, теперь когда что-то падает или температура процессора уходит в красную зону, приходит уведомление.

Заняло один выходной. Большую часть времени не настройка, а борьба с тем что Grafana на Pi жрёт память активнее чем ожидалось. В итоге вынес Grafana на основной комп, а Pi оставил только для сбора метрик.

Что получилось в итоге:

Полный контроль над данными. Ничего не уходит на сторонние серверы.

Уведомления в Telegram работают быстрее чем у большинства платных сервисов которые я пробовал.

Стоимость: Pi уже был, электричество копейки.

Минус один: когда Pi сам падает, мониторинг молчит. Решил просто поставить watchdog и пока хватает.

Кто делал что-то похожее, на чём остановились в итоге?

Tags:
+4
Comments8

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

Три вещи которые горели чаще всего.

Разные версии Android. На эмуляторе всё красиво. На реальном устройстве со старой версией что-то обязательно едет. Держу под рукой старый телефон с Android 10, туда ставлю перед каждым релизом.

Разрешения. Забываешь добавить в манифест, на новых версиях система спрашивает пользователя, пользователь жмёт «запретить» и половина функций молча перестаёт работать. Без каких-либо ошибок в логах.

ProGuard и минификация. Локально всё работает. В release сборке падает что-то что ты вообще не трогал. Потому что минификатор убрал класс который использовался через рефлексию.

Список не длинный но каждый раз спасает от как минимум одного стыдного бага в продакшене.

Что у вас в чеклисте перед релизом чего нет у большинства?

Tags:
+4
Comments0

Дал боту имя и работать стало приятнее

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

Назвал Степаном.

Смешно, но код стал аккуратнее.

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

Наверное дело в том что когда называешь что-то именем, начинаешь нести за это ответственность иначе. Не «упадёт и ладно», а «Степан не должен падать».

Кто-нибудь ещё так делает или это только у меня странности?

Tags:
+1
Comments1

Telegram Stars в боте: попробовал прикрутить, делюсь что удивило

Давно хотел добавить платежи в одного из своих ботов. Раньше использовал ЮKassa через нативный Telegram Payments. Но в этот раз решил попробовать Stars, всё-таки нативная валюта платформы, без внешних провайдеров.

Настройка оказалась проще чем ожидал. Никаких provider_token, никакой возни с webhook от платёжки. Просто отправляешь инвойс с указанием суммы в Stars и обрабатываешь successful_payment. Примерно так:

python

await bot.send_invoice(
    chat_id=message.chat.id,
    title="Премиум доступ",
    description="Доступ на 30 дней",
    payload="premium_30d",
    currency="XTR",
    prices=[LabeledPrice("30 дней", 100)]
)

Работает. Пользователь платит не выходя из Telegram, конверсия реально выше чем при редиректе на внешнюю страницу.

Но есть нюансы которые я не учёл сразу.

Первое: если пользователь покупал Stars через iOS или Android, Telegram отдаёт разработчику примерно 70% от суммы, остальное Apple и Google забирают себе. Если через десктоп или веб — почти всё твоё. Это принципиально меняет экономику для аудитории которая сидит на телефоне.

Второе: возвраты. Stars можно вернуть и Telegram это делает по запросу пользователя. Нужно обрабатывать refunded_payment иначе пользователь получит деньги обратно а доступ у него останется.

Третье: вывод только через Fragment в TON. Для российского юрлица это отдельная история.

В целом для цифровых товаров и небольших сумм Stars удобнее чем внешние платёжки. Но если оборот серьёзный или нужен рублёвый вывод, ЮKassa всё ещё выглядит надёжнее.

Кто уже работает со Stars в продакшене, как решаете вопрос с выводом в рубли?

Tags:
Total votes 2: ↑2 and ↓0+4
Comments0

Хотел сделать крутую онлайн игру. Расскажу где всё пошло не так

Идея была простая до безобразия. Браузерная змейка но с живыми соперниками. Canvas, websocket, Node.js. Всё это я более-менее знал, казалось делов на пару выходных.

Первый день вообще огонь. Змейка ползает, еду ест, растёт, цвета красивые. Думаю ну всё, осталось только игроков подключить.

Ага.

Синхронизация это ад

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

Всё сломалось.

У одного игрока змейка уже повернула, у второго она ещё летит прямо. Столкновения каждый клиент считает сам по своей картинке мира. Один видит что убил соперника, второй видит что убил он. Оба правы по своей логике.

Полез гуглить. Нашёл что это называется authoritative server, client-side prediction, lag compensation. Понял что я вообще не туда смотрел когда проектировал. Думал займёт вечер. Потратил месяц и всё равно сделал через одно место.

Лобби которое я не планировал

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

Это не мелочь.

Ожидание, старт, игра, конец, переиграть — каждое состояние надо синхронизировать. И самое весёлое это когда один игрок просто закрыл вкладку в середине партии. Что показывать второму? Кто победил? Как засчитать? Три вечера только на обработку разрывов соединения.

Читер за пять минут

Дал поиграть другу. Через пять минут он открыл консоль и начал отправлять серверу произвольные координаты. Змейка телепортировалась куда хочет. Я вообще не думал об этом когда писал архитектуру.

Что по итогу

Игра работает. Можно найти соперника и сыграть партию. Но код это такой клубок что я боюсь его открывать.

Главное что понял: онлайн игра это не игра плюс немного сетевого кода. Это совсем другая задача где сеть и есть основная сложность. Я недооценил это раз в десять минимум.

Код выложу на GitHub как разберу этот клубок. Пока стыдно показывать.

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

Tags:
Total votes 9: ↑5 and ↓4+4
Comments2

Решил я значит попробовать вайбкодинг всерьез. Не поиграться, а реально взять рабочую задачу и пройти от идеи до переноса, почти без ручного написания кода.

Взял небольшой pet-проект: утилита для мониторинга изменений в директории с уведомлениями в Telegram. Задача понятная, ограниченная, без хитрой бизнес-логики. Первые два дня были магией. Описываешь что хочешь, получаешь код, он работает. Скорость ощущается раза в три выше обычной. На третий день начались проблемы. Модель начала путаться в контексте проекта. Предлагала решения которые противоречили тому что уже было написано двумя часами ранее. Пришлось самому держать в голове всю архитектуру и постоянно напоминать что куда подключено. К концу недели понял главное: вайбкодинг не убирает необходимость понимать что происходит. Он убирает необходимость это печатать. Если не понимаешь архитектуру, инструмент начинает строить что-то своё, и разбираться потом дольше чем написать самому.

Утилиту довёл до конца. Работает. Но половину кода всё равно переписал руками.

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

Tags:
Total votes 5: ↑3 and ↓2+3
Comments4

Два часа потерял из-за того, что не написал один хендлер

Делал платежи в Telegram-боте. Нативные, через sendInvoice и ЮKassa.

Всё настроил: токен от BotFather получил, инвойс отправляется, кнопка оплаты появляется. Пользователь нажимает - и платёж падает с ошибкой. Молча. Без подробностей.

Payment failed

И всё. Telegram не говорит что именно не так.

Полез гуглить. Первая мысль - provider_token неверный. Проверил три раза, скопировал заново. Нет, токен правильный.

Потом решил что проблема в суммах - они передаются в копейках, не в рублях. 500 рублей = 50000. Перепроверил, у меня было правильно.

Потом подумал на webhook - может HTTPS не настроен как надо. Потратил минут сорок на проверку сертификата, перенастройку ngrok. Всё работает, но платежи всё равно падают.

Уже хотел идти спать, случайно наткнулся на строчку в документации:

Your bot must reply to this query in 10 seconds

Это про pre_checkout_query. Когда пользователь нажимает «Оплатить» - Telegram сначала отправляет боту запрос на подтверждение. Бот должен ответить в течение 10 секунд. Если не ответил - платёж автоматически отклоняется.

У меня хендлера для этого не было вообще. Бот просто молчал.

Добавил три строки:

python

@dp.pre_checkout_query()
async def pre_checkout(query: types.PreCheckoutQuery):
    await query.answer(ok=True)

Платёж прошёл с первого раза.

Два часа отладки из-за трёх строк кода которые я не написал.

Если кто-то тоже делает платежи в Telegram-боте и получает молчаливый отказ - проверьте pre_checkout_query первым делом, до всего остального.

Tags:
Total votes 3: ↑3 and ↓0+7
Comments1

Information

Rating
1,466-th
Location
Мариинск, Кемеровская обл., Россия
Date of birth
Registered
Activity