Кнопка під сповіщенням відповідала «ваш Telegram не прив'язано» і не давала виходу. У базі нуль прив'язок і нуль кодів за весь час. Сторінка профілю існує, але пункту меню не мала, а єдиний вхід — ім'я користувача в шапці — малювався за умовою «є ім'я або пошта», тоді як /me віддавало лише пошту. В облікового запису власника, який заводить установник і який входить ІМЕНЕМ, вона порожня. Тобто в типовій інсталяції входу в профіль не було взагалі. * /me віддає username (тип Me на фронтенді його вже вимагав); * вхід у профіль малюється завжди для людини; * пункт меню «Обліковий запис → Мій профіль», perm став необов'язковим; * текст бота називає те, що видно на екрані; * сторінка каналів показує стан прив'язки біля telegram-каналу. Плюс 0073: оренда сходинки ескалації отримала lease_token. Партія переростає 2-хвилинну оренду, і другий інстанс доставляв ту саму сходинку паралельно з першим. Тепер запис проходить лише за збігу токена; при розбіжності не відбувається нічого, сходинка лишається належною. 65 міграцій, усе зелене проти справжньої бази. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
93 lines
7.5 KiB
SQL
93 lines
7.5 KiB
SQL
-- =====================================================================
|
||
-- NetPulse :: 0073_escalation_lease_token.sql
|
||
-- Оренда сходинки ескалації отримує токен: хто взяв, той і записує.
|
||
--
|
||
-- ЩО БУЛО НЕ ТАК
|
||
--
|
||
-- 0066 дала оренді рядка (leased_until) рівно половину роботи. Вона
|
||
-- чесно закриває вікно «взяли — ще не записали»: доки оренда не
|
||
-- спливла, другий інстанс сходинку не побачить. Але оренда — це строк,
|
||
-- а не володіння, і про друге вона не знає нічого.
|
||
--
|
||
-- Строк тут дві хвилини (store.EscalationLease), а партія — до сотні
|
||
-- сходинок (alerting.escalationBatch), і кожна доставка в ній має
|
||
-- ВЛАСНИЙ мережевий таймаут: Telegram, SMTP, вебхук у чужий контур.
|
||
-- Тобто «партія довша за оренду» — це не рідкісний збіг, а звичайний
|
||
-- вечір, коли один канал підвис.
|
||
--
|
||
-- Далі рахунок простий. Оренда спливає, поки перший інстанс ще йде по
|
||
-- партії. Другий (або наступний такт того самого процесу) бачить рядок
|
||
-- вільним, бере ту саму сходинку — з тим самим step_idx, бо перший її
|
||
-- ще не просунув, — і доставляє. Через хвилину доходить черга до неї в
|
||
-- першого, і він доставляє її вдруге. О третій ночі це два дзвінки, а в
|
||
-- журналі — два рядки «надіслано», тобто доказ перетворюється на
|
||
-- виправдання: за ним не видно, що система подзвонила двічі за одну
|
||
-- сходинку.
|
||
--
|
||
-- ApplyEscalation при цьому мав ДВІ перевірки стану (stopped_at IS NULL
|
||
-- і «алерт усе ще firing»), і обидві потрібні — але обидві про АЛЕРТ.
|
||
-- Жодна не про те, чи оренда все ще наша. Звірити було нічим: у рядку
|
||
-- лежав лише строк, і строк першого інстансу нічим не відрізнявся від
|
||
-- строку другого.
|
||
--
|
||
-- ЧОМУ ТОКЕН, А НЕ ДОВША ОРЕНДА
|
||
--
|
||
-- Спокуса очевидна: зробити оренду двадцять хвилин, і партія завжди
|
||
-- вкладеться. Це не рішення, а пересунута межа. Скільки б не було
|
||
-- поставлено, знайдеться партія довша: сотня сходинок по три канали,
|
||
-- кожен зі своїм таймаутом, — це арифметика, а не патологія. Крім
|
||
-- того, довга оренда псує протилежний випадок: процес, що впав одразу
|
||
-- після взяття, тримає сходинку заручницею рівно на цей строк, а
|
||
-- сходинка ескалації — це той, кого не розбудили.
|
||
--
|
||
-- Токен знімає обидва: строк лишається коротким (впав — сходинку
|
||
-- швидко перебирають), а подвоєння закриває звірка володіння, яка від
|
||
-- строку не залежить взагалі. Це той самий fencing-токен, що й у
|
||
-- розподілених блокуваннях: власність доводиться не тим, що годинник ще
|
||
-- не пробив, а тим, що номер збігся.
|
||
--
|
||
-- ЧОМУ ТОКЕН НА РЯДОК, А НЕ НА ПАРТІЮ
|
||
--
|
||
-- Один токен на всю партію теж закрив би подвоєння, і коду було б на
|
||
-- рядок менше. Але тоді два різні прогони, що поділили одну партію
|
||
-- (SKIP LOCKED саме для цього й стоїть), розрізнялись би лише
|
||
-- випадковістю збігу, а рядок у базі не казав би, ЯКЕ взяття його
|
||
-- тримає. core.new_id() у SET обчислюється для кожного рядка окремо —
|
||
-- ціна нульова, а стовпець стає самодостатнім: за ним видно конкретне
|
||
-- взяття конкретної сходинки.
|
||
--
|
||
-- ЩО СТАНЕТЬСЯ, КОЛИ ТОКЕН НЕ ЗБІГСЯ
|
||
--
|
||
-- Нічого не станеться — і це головна вимога до цього стовпця. UPDATE
|
||
-- не чіпає ЖОДНОГО рядка: ні next_at, ні step_idx, ні leased_until
|
||
-- (інакше запізнілий інстанс зривав би оренду того, хто зараз працює).
|
||
-- Движок бачить applied = false, у мережу нічого не йде, і сходинка
|
||
-- лишається належною — тим самим step_idx, з тим самим next_at у
|
||
-- минулому. Її доводить до розв'язку той, чий токен у рядку; якщо й він
|
||
-- не дійшов — оренда спливає за дві хвилини, і наступний такт бере
|
||
-- сходинку заново. Застрягти назавжди нема від чого: жодна гілка не
|
||
-- знімає next_at і не подовжує оренду безкінечно.
|
||
--
|
||
-- СУМІСНІСТЬ
|
||
--
|
||
-- Стовпець NULL для всіх наявних рядків, і це правильний стан: NULL
|
||
-- означає «зараз ніхто не тримає». Живі оренди на момент накату
|
||
-- (щонайбільше дві хвилини) отримають токен при наступному взятті.
|
||
-- Вимоги NOT NULL немає навмисно — вільний рядок мусить уміти сказати
|
||
-- «нічий», а не носити токен-привид від позаминулого взяття.
|
||
-- =====================================================================
|
||
|
||
ALTER TABLE alr.alert_escalations
|
||
ADD COLUMN IF NOT EXISTS lease_token uuid;
|
||
|
||
COMMENT ON COLUMN alr.alert_escalations.lease_token IS
|
||
'Токен поточної оренди (fencing). Ставиться разом із leased_until при '
|
||
'взятті сходинки, звіряється при записі рішення й знімається разом з '
|
||
'орендою. NULL — сходинку зараз ніхто не тримає. Потрібен тому, що '
|
||
'обробка партії буває довшою за оренду: без звірки володіння запізнілий '
|
||
'інстанс доставив би сходинку, яку вже доставив той, хто перебрав її '
|
||
'після спливання оренди.';
|
||
|
||
COMMENT ON COLUMN alr.alert_escalations.leased_until IS
|
||
'До якого моменту сходинка вважається взятою в роботу. Строк, а не '
|
||
'володіння: володіння доводить lease_token.';
|