Перейти до вмісту

Блог ·

Пʼятнадцять речей, які міграція білінгу не може взяти з собою

Міграція білінгу не є копіюванням даних. Пʼятнадцять виміряних пропусків, гроші в цілих центах і відбиток як єдиний придатний ключ ідемпотентності.

Автор: Provibr

Перенесення хостингового бізнесу з однієї білінгової панелі до іншої виглядає як копіювання даних, але ним не є. Дві панелі, що обидві знають, чим є клієнт, послуга й рахунок, усе одно не збігаються в тому, що ці обʼєкти містять, і ця розбіжність ніколи не буває симетричною: джерело зазвичай знає більше, ніж API призначення готовий прийняти. Міграція, яка вдає протилежне, пише документи, що не збігаються з тими, які клієнт уже отримав.

Тому перше, що видає цей інструмент, це не дані. Це перелік того, що не доїде.

Гроші є цілими центами, а перетворення читає текст

Обидві панелі віддають суму як число JSON (9.95) або як десятковий текст ("9.95"). Очевидне перетворення полягає в множенні на сто.

Очевидне перетворення хибне. Як 64-бітове число з рухомою комою 9,95 дорівнює 9,949999999999999289, а 9.95 * 100.0 дає 994,9999999999999. Відкинувши дробову частину, маємо 994 центи: на один цент менше, ніж каже рахунок. На ігровому сервері це прикрість заокруглення. У бухгалтерії це неправильний документ, і він спливає, коли хтось закриває рік.

Тому канонічна модель несе суму як ціле число центів плюс код валюти, а перетворення рахує цифри в текстовій формі замість того, щоб узагалі проходити через рухому кому. Понад два знаки після коми заокруглюються від нуля, а не відкидаються, бо відкинутий пів цента змушує зникнути рядок рахунку. Наукова нотація відхиляється одразу: підтримати 1e2 наполовину означало б перетворити рахунок на сто євро в один цент, а відмова коштує менше, ніж вигадливість. Є верхня межа 2^53 центи, бо інший кінець дроту рахує числами JavaScript, і значення, яке ми приймаємо, а вони вже не подають точно, є тихим відхиленням замість помилки.

Пʼятнадцять пропусків, кожен виміряний

Вісім із них стосуються будь-якої міграції білінгу, а сім належать до першого маршруту, з FOSSBilling до Paymenter. Усі пʼятнадцять виміряно на запущених контейнерах, а не взято з документації.

Загальні вісім є тими, що їх можна передбачити, щойно вимовиш уголос. Паролі клієнтів є відбитками, обчисленими ключем застосунку джерела, і вони не працювали б ніде інде, а точка входу користувачів на призначенні вимагає нового пароля щонайменше на вісім символів. Збережені картки й мандати є токенами, що належать обліковому запису панелі-джерела в платіжного постачальника; узяти їх означало б, що списання вирушає з рахунку, якого клієнт ніколи не підписував. Налаштування платіжних шлюзів несуть ключі й адреси вебгуків старої панелі, тому їх свідомо ніколи не читають. Поштові шаблони написані мовою шаблонів власної панелі, і перенесений шаблон дає лист із незаміненими заповнювачами. Вкладення тікетів існують як файл на диску панелі-джерела, і немає точки входу, яка повертала б їхній вміст. Партнерська бухгалтерія не має відповідника з тим самим значенням, і її перенесення відродило б комісії, які вже виплачені. Промокоди без історії використання є прямо хибними: код із межею в десять використань, ужитий чотири рази, приїжджає як код, у якого лишається десять. Секрети двофакторної автентифікації зберігаються зашифрованими на кожній панелі й ніде не віддаються.

Сім маршрутних є цікавішою половиною, бо всі вони є межами адмінського API призначення, а не самого призначення.

Що лишається позадуЧому, виміряно
Компанія, адреса, податковий номер, телефон, моваЗапит створення перевіряє імʼя, прізвище, пошту, пароль, дату підтвердження й роль, а контролер створює саме перевірену множину. Усе інше відпадає.
Номери рахунківinvoices.number є унікальним у базі, але не трапляється в жодному з двох правил перевірки й у жодному ресурсі; його не записати й не прочитати назад.
Податок у рядку рахункуРядок рахунку приймає опис, кількість, ціну й посилання. Поля податку немає.
Каталог продуктівКонтролер продуктів має лише index та show, а файл прав дає на продукти й категорії тільки view.
Записи платежівAPI не публікує жодної точки входу для платежів чи транзакцій.
Послуга, що вже простроченаДата завершення перевіряється як сьогодні або пізніше.
Особи персоналуПовідомлення тікета вимагає user_id, що існує як користувач, а персонал джерела не є користувачем призначення.

Два з цих рядків несуть рішення, яке варто назвати.

Номери рахунків ідуть у stderr. Оскільки призначення надає власну послідовність і не приймає та не повертає старий номер, мігратор пише по рядку на кожен рахунок у потік помилок із номером джерела, де його підбирає журнал агента. Це не витончено. Це чесно: номер стоїть на документі, який клієнт тримає в руках, і єдине місце, куди ми ще можемо його покласти, це журнал, яким керує адміністратор.

Рахунок із податком блокується, а не пишеться. Призначення рахує податок із власних налаштувань, тож той самий рахунок дістав би там інший підсумок, ніж уже надісланий документ. Написати його все одно означає створити другий, суперечливий запис однієї операції. План кладе такі рахунки під tax_not_carried і рухається далі.

Ця відмінність проходить крізь увесь інструмент. Відсутній продукт блокує послугу замість того, щоб покласти її кудись інде. Блокування є результатом; вгадування ні.

Джерело відповідає HTTP 200, коли зазнає невдачі

Адмінський API FOSSBilling відповідає на невдалий виклик статусом 200 і тілом {"result":null,"error":{"message":"...","code":9999}}. Той, хто дивиться лише на код статусу, читає помилку як порожню відповідь, а міграція, що не відрізняє порожньо від не вдалося, колись синхронізує бухгалтерію до порожнечі. Є рівно одна функція, яка відкриває цей конверт, і вона дивиться на error раніше, ніж на result.

Читач до того ж є лише читальним за побудовою, і це тест, а не обіцянка. Кожен метод, який він може викликати, є названою сталою, що закінчується на get або get_list, сім штук, і тест читає вихідний файл та звіряє кожен виклик client.get( з цим переліком. Читач, що обзавівся б викликом _create, був би червоним тестом, а не пізнішим відкриттям.

Ще одна виміряна дивина: продукт FOSSBilling повертається з усіма сімома періодами оплати позначеними як увімкнені й з нульовою ціною, хоч би що панель продавала. Ціна є єдиним придатним сигналом про те, який цикл справжній.

Ідемпотентність на відбитку, бо зовнішнього ключа немає

Звичний спосіб зробити міграцію відновлюваною полягає в записі власного ідентифікатора на призначенні й пошуку його наступного разу. Paymenter не пропонує, куди його покласти: немає поля зовнішнього посилання, яке пережило б запис і повернулося при читанні.

Тому особа є відбитком, обчисленим зі вмісту з обох боків.

  • Клієнт є своєю поштовою адресою в нижньому регістрі.
  • Послуга є поштою, назвою продукту, ціною в центах, валютою й кількістю.
  • Рахунок є поштою, валютою, датою сплати й своїми рядками, з підсумком, складеним із цих рядків.
  • Тікет є поштою плюс тема.

Два з них несуть попередження в коді. Відбиток послуги свідомо не містить наступної дати оплати, бо призначення відкидає дату з минулого й зберігає порожню, і тоді та сама послуга мала б при відновленні інший відбиток, ніж при записі, і була б створена вдруге: поле, яке призначенню дозволено змінювати, не може бути частиною ключа. Підсумок рахунку складається з рядків замість читання з поля підсумку джерела, бо на призначенні це значення є обчисленням, а не стовпцем, і дві сторони, що складають по-різному, дають ключ, який ніколи не збігається. Рядки впорядковуються перед складанням, бо порядок, у якому вони повертаються, є порядком їхніх ідентифікаторів, а той не мусить збігатися з порядком запису, якщо один з них випав.

Ключ тікета є слабким, і так і записано: два тікети того самого клієнта з точно тією самою темою зливаються в один. Додати перше повідомлення виправило б це й коштує другого виклику на кожен тікет на призначенні.

Три способи повестися зі зіткненням

Коли обʼєкт уже існує на призначенні, запуск робить одне з трьох, і вибір роблять наперед: skip лишає наявний рядок у спокої, overwrite_empty заповнює лише ті поля, що там порожні, а stop спиняє весь запуск перед тим, як щось узагалі буде записано.

Виміряно проти справжнього FOSSBilling 0.8 і справжнього Paymenter: суха прогонка запланувала чотирьох клієнтів, чотири послуги, чотири рахунки, пʼять продуктів і два тікети; виконання створило чотирьох клієнтів, чотири послуги, чотири рахунки й два тікети з перерахунком опісля. Убити процес посередині через kill -9 і запустити знову дало нуль створених обʼєктів і лишило призначення байт у байт тим самим. Із stop настала зупинка без жодного запису. Із overwrite_empty заповнилися лише порожні поля імені, а заповнені лишилися недоторканими. Рахунок у непідтриманій валюті повернувся як 422 із назвою поля, був відзначений як currency_unknown, і запуск пішов далі.

Чому маршрут не називається live

Інструмент готовий і виміряний, а маршрут усе ще не запустити з панелі. Причиною є межа, яка виявилася важливою: облікові дані міграції читаються з таблиці інтеграцій, а стовпець типу цієї таблиці є реєстром інфраструктури, якою керуємо ми. Запис live там несе сторінку документації, шлях налаштування, схему облікових даних, перевірку зʼєднання й девʼять перекладів, і він зʼявляється на сайті як щось, чим керують серверами. Білінгова панель нею не є. Ми читаємо її один раз і більше ніколи не торкаємось.

Назвати маршрут live означало б покласти на нього значок, що каже інше, ніж екран під ним, тому є третій стан, adapter_ready, який каже те, що є правдою: адаптер існує й виміряний проти справжніх панелей, а місця, де ввести дві панелі, ще немає. Це місце є окремим раундом, бо зберегти білінгову панель як зʼєднання є рішенням щодо схеми, а не побічним продуктом інструмента міграції.

З міграційної роботи Provibr. Чому API, що відповідає 200 на невдачу, є класом хиби, а не випадковістю, описано в чому 200 від Proxmox не є результатом, а чому міграція, що тихо пропустила, гірша за ту, що зазнала невдачі, описано в міграція, яка ніколи не виконалася.

Читати далі

Почніть з одного хоста й одного агента

Створіть обліковий запис, встановіть агента однією командою та підключіть свій перший хост Proxmox. Пробний період триває 14 днів і не потребує платіжних даних.