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

Блог ·

Потік є графом, і граф має бути скінченним

Перетворити список кроків на редактор вузлів здається зміною інтерфейсу. Насправді це рішення про цикли, про невзяті гілки та про один вкрадений клік.

Автор: Provibr

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

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

Перша версія не має циклів, і це не лінощі

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

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

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

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

Шість видів і порти, які мають бути функцією

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

ВидВиходи
triggerодин, і на граф припадає рівно один тригер
scriptok та error
switchпо одному на правило, плюс otherwise
setодин
noteжодного, і його ніколи не відвідують
endжодного, і він проголошує результат

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

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

Запис у state. і ніколи у flow.

Вузол set пише змінні. Очевидним простором імен є той, що вже існує, і саме він хибний. flow.name, flow.runId та flow.step несуть відомості про виконання, тож вузол із правом запису туди дозволив би клієнтові перезаписати flow.name, і тоді журнал перевірки брехав би у формі, яка має вигляд даних.

Тому set пише у state., простір імен новий, порожній на початку і чинний лише на час одного виконання. Шаблон, який читає {{ state.role }}, коли жоден вузол її не задав, зазнає невдачі так само, як і будь-яка інша відсутня змінна, і саме цього ми хочемо: строгий режим є кращим повідомленням про помилку, ніж порожній рядок у вітальному листі.

Перемагає перше правило, і гілка, якої ніхто не взяв

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

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

І кожен виконуваний вузол, якого не відвідали, теж дає рядок із власною причиною. «Попередній крок зазнав невдачі, тому цей не виконувався» та «виконання пішло іншою гілкою, тому цей вузол не був досягнутий» є двома різними реченнями, і розрізняти їх означає саме те, що ви робите, читаючи виконання, яке вас здивувало.

Шаблон, який неможливо перервати

Один оператор розгалуження перевіряє регулярний вираз, і саме він становить єдину справді небезпечну частину функції. JavaScript не має жодного способу перервати RegExp.test(). Немає ані терміну, ані переривання, а рушій вертається назад, тож шаблон на кшталт (a+)+$ на сорока символах підвішує обробник запитів на хвилини. Це відмова в обслуговуванні через одне поле введення.

Сторона агента розвʼязала ту саму задачу в попередньому раунді, обмеживши рушій, реалізацією без повернень і з межею памʼяті. Такого шляху тут немає, тому натомість обмежено мову до підмножини, у якій катастрофу неможливо висловити: без зворотних посилань, без lookaround і з висотою зірки один, тобто повторення не може стояти всередині повторення. (a+)+ стає неможливим, а a+b+ лишається дозволеним. З межею довжини на шаблон і на вхід найгірший випадок є квадратичним на кількох тисячах символів, і сучасне ядро робить це, не помічаючи.

Список не є гіршим варіантом

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

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

Один рядок коду, який зʼїдав кожен клік

Полотно добре зсувалося, добре перетягувалося і нічого не вибирало. Клік на виході ok вузла не робив рівно нічого, і в DOM не було нічого хибного: обробник був на місці, елемент був на місці, координати були правильні.

setPointerCapture() на предку переспрямовує кожну наступну подію вказівника до елемента, що захоплює, зокрема й pointerup. Далі браузер запускає click на спільному предку натискання й відпускання, а при активному захопленні цим предком є сам контейнер. Власний обробник кліку вузла ніколи не досягається, і ніде не зʼявляється помилка.

Виправлення є структурним, а не перемикачем: зсування тепер тримається на тлі, розташованому позаду вузлів, тож натискання на вузол більше не може розпочати захоплення.

Є й друга наука в тому, як близько це було до того, щоб лишитися непоміченим. Браузерна перевірка на «зʼєднання додалося» рахувала елементи svg path, а кожна піктограма інтерфейсу є одним із них, тож число зростало незалежно від того, чи зʼявилося зʼєднання. Тепер вона рахує шляхи всередині шару зʼєднань, проти точної кількості.

Що ми записали б

  1. Відхиляйте цикл замість того, щоб обмежувати ітерації. Прихована межа є пасткою; відмова під час збереження з названими винними вузлами є повідомленням.
  2. Робіть порти функцією вузла. Те, чия форма залежить від налаштування, не описується фіксованою таблицею.
  3. Давайте новим змінним новий простір імен. Дозвіл користувацькому введенню писати поверх полів, що описують виконання, перетворює журнал перевірки на оповідь.
  4. Записуйте невзяту гілку разом із її причиною. «Не досягнуто» і «не виконано після помилки» відповідають на різні питання.
  5. Якщо рушій не переривається, обмежте мову. Висота зірки один плюс межа довжини перетворює експоненційний вибух на арифметику.
  6. Будуйте клавіатурний шлях першим, якщо він є вимогою. Щойно він є, часто виявляється, що бібліотека робила лише легшу половину.
  7. Захоплення вказівника краде кліки в нащадків. Якщо полотно добре перетягується і нічого не вибирає, дивіться спершу туди.

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

Із платформної роботи Provibr. Чому крок, який мовчки нічого не зробив, гірший за крок, який зазнав невдачі, описано в міграції, яка ніколи не запускалася, а те, як єдине подання поступу читає чужі таблиці й ніколи в них не пише, описано в проєктувати, ніколи не заміняти.

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

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