Python Developer · Backend · FullStack в К2 ERP
  • Як соло-розробник автоматизує верифікацію SaaS: e2e-observability, failover та реальний RLS-баг у transaction pooling

    проблему вийде знайти тільки після інциденту в проді під навантаженням

    Ось я про це заздалегідь подумав і вирішив перевірити)

  • Як соло-розробник автоматизує верифікацію SaaS: e2e-observability, failover та реальний RLS-баг у transaction pooling

    Дякую за питання!

    1. Про витоки. Це не теорія. Я підняв sidecar PgBouncer у transaction mode і гнав навантаження. RAW-режим реально впав із тисячами LEAKS і FAIL_CLOSED. Ще важливіший момент: під час фінальної перевірки APP-режим один раз упав із 1 leak на 1000 запитів. Причина — reset app.is_admin йшов поза BEGIN і потрапляв на інший backend, ніж запит. Тому довелося нейтралізувати is_admin вже всередині транзакції.

    2. Про is_admin замість окремої ролі. Тут ви маєте рацію: окремий admin-пул чистіше і безпечніше. У мене це is_admin — прагматичний компроміс, бо вузькі операції (міграції, APScheduler) потрібні рідко, і заводити окремий пул лише задля них — оверхед. Роблю це безпечно: transaction-local GUC + примусовий скид всередині транзакції + Semgrep, який забороняє session-level прапорці. Якби проєктував з нуля — взяв б окрему роль.

    3. Про Redis lock. SET NX EX — атомарна операція в Redis, тому двоє не можуть одночасно отримати lock. Токен із pid+uuid захищає від того, щоб не скинути чужий. TTL + refresh кожні 5 с — захист від завислого лідера. Ви праві, що готовий Lock() — надійніша обгортка, і для нового коду я б рекомендував саме його. Мій ручний приклад — мінімалістична імплементація, яку було простіше вписати в існуючий код.

    Ще раз дякую — ці питання показують, що в статті варто було чіткіше пояснити trade-offs.

    Підтримав: Nazar Kostetskyi
  • Чому 0.1 + 0.2 != 0.3, або як ваш код краде гроші бізнесу

    Дуже цікавий коментар, я справді ще не мав досвіду з цим, тож цікаво було про це дізнатися. Дякую! 🙂

  • Чому 0.1 + 0.2 != 0.3, або як ваш код краде гроші бізнесу

    Дякую за крутий фідбек та реальні приклади з платіжних систем! 🙂

  • Чому 0.1 + 0.2 != 0.3, або як ваш код краде гроші бізнесу

    Ось саме тому я і написав цю статтю 🙂 Якщо хоча б один розробник після прочитання не допустить цей баг — вже варто було.

    Підтримали: Volodymyr Spodaryk, flyman
  • Чому 0.1 + 0.2 != 0.3, або як ваш код краде гроші бізнесу

    Decimal — це класичний і правильний вибір для більшості проєктів. Але у великих SaaS-системах з купою інтеграцій у нього з’являються свої болючі нюанси.
    По-перше, це серіалізація в API. JSON не знає, що таке Decimal. Довелося б або віддавати його як рядок (і змушувати TypeScript-фронтенд парсити його назад), або конвертувати у float для мережі (що перекреслює всю точність). Ціле число в копійках проходить крізь будь-які API, мобільні додатки та вебхуки без жодного викривлення типів.

    По-друге, це база даних. У Postgres BIGINT зберігається, індексується і працює швидше, ніж довільний NUMERIC чи DECIMAL під великим навантаженням.

    Ну і нативна арифметика з int у Python все одно швидша та легша за Decimal. Тому int в базі — це вибір на користь простоти інтеграцій та швидкості роботи БД!

    Підтримали: Дмитрий Кинаш, Bot Bot
  • Чому 0.1 + 0.2 != 0.3, або як ваш код краде гроші бізнесу

    Ви праві, це класична проблема попозиційного заокруглення.

    У фінтеху та е-комерс її вирішують двома стандартними шляхами:

    1) Розрахунок від загальної суми: податок або акциз нараховується не на кожен товар окремо, а на сумарну вартість усього кошика, і заокруглюється лише один раз наприкінці. Це зводить максимальну похибку до 0.5 копійки на все замовлення.

    2) Внутрішня висока точність: для проміжних розрахунків у пам’яті (податки, знижки, комісії) ми використовуємо базисні пункти (basis points, 1/10000) або умовні «мікрокопійки», а фінальне заокруглення до цілих копійок робимо вже один раз — при записі фінального інвойсу в базу та відправці на платіжний шлюз.

    Саме тому в статті я зазначив, що висока точність використовується як проміжне значення в пам’яті під час обчислень, але фінальний результат у БД завжди фіксується як ціле число (int).

    Дякую за чудове питання. Поправте мене, якщо я десь помиляюсь.

  • Чому 0.1 + 0.2 != 0.3, або як ваш код краде гроші бізнесу

    Те, що САБ Казначейства працює в копійках — це найкращий доказ для всієї статті 🤝

    Ви порушили дуже важливе питання щодо розподілу залишкових копійок. Дійсно, якщо ми ділимо умовну 101 копійку порівну, виникає оця «залишкова» копійка, яку неможливо розділити фізично.

    Я вирішую це досить просто: округлюю частки до найближчої копійки донизу (floor), а весь залишок (зазвичай 1 копійку) за логікою бізнесу примусово докидаю на головний рахунок отримувача.

    Це вимагає додаткових перевірок у коді, але повністю убезпечує від фінансових дірок, які обов’язково створив би float.

    Дякую за такий крутий коментар і за те, що підсвітили цю тему!

  • Чому 0.1 + 0.2 != 0.3, або як ваш код краде гроші бізнесу

    Дякую за коментар!
    Повністю згоден з вами щодо простоти int. Коли система масштабується і доводиться передавати гроші через API, черги повідомлень та різні фронтенди, серіалізувати звичайні цілі числа в JSON набагато безпечніше та простіше, ніж тягнути Decimal з його специфікою через усі інтеграційні шари.
    Щодо IEEE 754 — ви абсолютно праві. Я свідомо не хотів перевантажувати статтю лонгридом про теорію двійкового кодування дробів та нескінченні періодичні дроби, але ви сформулювали це максимально лаконічно та влучно. Дякую за чудове доповнення до матеріалу!

  • Чому 0.1 + 0.2 != 0.3, або як ваш код краде гроші бізнесу

    У CPython немає поняття «числа на стеку» — усі змінні (і int, і float) під капотом є об’єктами (PyObject) і алокуються виключно на купі (heap). Тому в контексті Python-розробки (про яку стаття) цей аргумент не зовсім релевантний.

    Щодо бібліотек, які «декодують фінансові значення у float в пам’яті для швидкості» — у фінтех-системах це зазвичай вважається критичним архітектурним дефектом. У пам’яті для грошей використовують або decimal.Decimal, або int (копійки). Використовувати float в оперативній пам’яті для транзакцій — це невиправданий ризик втрати точності.

    У протоколі FIX тип Qty (кількість) змінили з int на float виключно через появу дробових лотів (наприклад, купівля 0.001 BTC чи 1.5 акцій). Кількість — це не гроші. Баланси рахунків та транзакційні суми (Amt) у float не рахують.

    Дякую за дискусію, але я все ж таки орієнтувався на сучасні стандарти Stripe та Adyen, а не на застарілі компроміси.

    Підтримав: Valentin Nechayev
  • Чому 0.1 + 0.2 != 0.3, або як ваш код краде гроші бізнесу

    Використання float у системах деяких банків США — це не архітектурний орієнтир, а технічний борг, з яким вони борються роками. Сучасні платіжні гіганти (Stripe, Shopify та інші) у своїх API працюють виключно з integer у мінімальній одиниці валюти. У міжнародних стандартах транзакцій (ISO 8583) типу float немає взагалі.
    Писати про це на професійному ресурсі варто саме тому, що навіть досвідчені інженери іноді намагаються виправдати плаваючу точку у фінансових розрахунках.

    Підтримав: Volodymyr Spodaryk
  • Чому 0.1 + 0.2 != 0.3, або як ваш код краде гроші бізнесу

    Саме так! У базі копійки зберігаються якраз у BIGINT — про всяк випадок, якщо обороти SmartHire долетять до масштабів держборгу США 🙂
    Мені довелося погуглити, що таке «лампорти», але логіка на 100% правильна: рахувати все в мінімальних неподільних одиницях — це єдиний надійний шлях для будь-якої системи.
    Дякую за підтримку та цікаве доповнення!

    Підтримав: flyman
  • Чому 0.1 + 0.2 != 0.3, або як ваш код краде гроші бізнесу

    Мій бакалаврат — «Фінанси та банківська справа», якщо цікаво 🙂
    Щодо float з епсилонами — це валідний підхід для наукових обчислень чи геймдеву.
    Але жоден банк, Stripe чи будь-яка платіжна система не зберігає транзакції у float. Тільки цілі числа. Це не думка — це галузевий стандарт.
    Про банківське округлення (ROUND_HALF_EVEN), до речі, я якраз і написав у статті — ми використовуємо його для розрахунку відсотків.

  • Як побудувати B2B SaaS у Telegram на 1000+ клієнтів і не з’їхати з глузду

    Дякую! Про маркетинг і залучення — повністю згоден. Зараз сам на цьому етапі 🙂

    Підтримав: Vasyl
  • Як побудувати B2B SaaS у Telegram на 1000+ клієнтів і не з’їхати з глузду

    600 таблиць і ізоляція «на совісті розробників» — це саме той сценарій, якого я намагаюся уникнути з першого дня 😅
    Мігрувати RLS на такому масштабі — це справді нетривіальна задача. Тут, мабуть, реалістичніше не «додати RLS на всі 600 таблиць», а виділити критичні таблиці з PII та фінансовими даними і покрити їх в першу чергу. На масштабі це стає операційним кошмаром, як ви вже, мабуть, переконались.
    А що банк конкретно вимагає? Audit log на рівні БД, чи саме policy-based ізоляцію?

    Підтримав: Oleksandr Serdiuk
  • Як побудувати B2B SaaS у Telegram на 1000+ клієнтів і не з’їхати з глузду

    Класична ситуація коли комплаєнс приходить пізніше архітектури 😄

    Для спільних проєктів між тенантами RLS ускладнюється — потрібна таблиця-зв’язка типу project_tenant_access з явними правами, а policy виглядає приблизно так:

    CREATE POLICY project_access ON projects
    USING (
      EXISTS (
        SELECT 1 FROM project_tenant_access
        WHERE project_id = projects.id
        AND tenant_id = current_setting('app.tenant_id')
      )
    );
    Тоді банк бачить ізоляцію на рівні БД, а не тільки логіки аплікації.

    Але якщо архітектура вже велика, то міграція на RLS це окремий біль. Як у вас зараз реалізована ізоляція на рівні аплікації?

    Підтримав: Oleksandr Serdiuk
  • Як побудувати B2B SaaS у Telegram на 1000+ клієнтів і не з’їхати з глузду

    Погоджуюсь, циклічні імпорти — це скоріше сигнал про проблему в архітектурі ніж рішення.
    У моєму випадку це легасі з раннього етапу, поступово рефакторю. Актуальних бенчмарків поки немає — рішення було прийнято інтуїтивно на ранньому етапі. Це хороший привід виміряти і переглянути підхід. Дякую за зауваження, є над чим подумати!

    Підтримав: Андрій Петрів
  • Як побудувати B2B SaaS у Telegram на 1000+ клієнтів і не з’їхати з глузду

    Гарне зауваження щодо GIL! Ви правду кажете, що потоки в CPython не дають справжнього паралелізму для CPU-bound задач через GIL. Але to_thread тут вирішує іншу проблему — не прискорення шифрування, а звільнення event loop від блокування. AES-256 на одному телефонному номері виконується за мікросекунди, тому thread contention на практиці незначний. Якщо б таких операцій було тисячі одночасно — так, краще ProcessPoolExecutor.
    Щодо lazy imports — допомагає зменшити час старту модуля і уникнути циклічних імпортів у великій кодовій базі. Але погоджуюсь, для статті це не найкращий приклад.

    Підтримав: Андрій Петрів
  • Як побудувати B2B SaaS у Telegram на 1000+ клієнтів і не з’їхати з глузду

    Дякую за фідбек! Імпорти всередині функцій — свідоме рішення для lazy loading у великій кодовій базі, але погоджуюсь що для прикладу в статті це виглядає неохайно.
    Щодо to_thread — гарне зауваження, варто було розкрити.
    Коротко: encrypt_phone — це CPU-bound операція (AES-256), і щоб не блокувати event loop asyncio, вона виконується в окремому потоці через to_thread. Можливо винесу це в окрему замітку.

    Підтримав: Андрій Петрів
  • Як побудувати B2B SaaS у Telegram на 1000+ клієнтів і не з’їхати з глузду

    Дякую! Так, мульти-тенант стафф з кількома тенантами — це окремий біль. У мене це вирішено через tenant_staff з ролями.
    Щодо float — інженерна параноя тут виправдана 😄
    У другій частині розберу більш детально з прикладами.
    Радий однодумцям!

← Сtrl 12 Ctrl →