-- ===================================================================== -- 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.';