Стендап Сьогодні

Що я зробив, що я хочу зробити, і що це все значить.
Повсякденні здобутки в форматі стендапу.
Детальніше в статті

Підписатись на RSS · 📢 Канал в Telegram @stendap_sogodni

22.10.2024

Rails, Asset Pipeline, та артефакти

Якось в розмові згадалося, що Ruby on Rails не дуже дружня до збірки на CI. Принаймні, історично. Хоча й зараз офіційна документація згадує якісь хаки, тому те, що я розповім, більш-менш актуально й досі.

Коли я почав працювати з Rails, то “збірки JavaScript” як такої не існувало. Та й JavaScript багато не було. Шматочки JS та CSS просто додавали в теку public. Не тільки в Rails, а взагалі. А вебзастосунки розгортували копіюванням файлів на сервер.

Коли у 2011 в Rails зʼявився механізм для збірки - Asset Pipeline - то не було ані Webpack, ні навіть Node.JS в широкому використанні. Памʼятаю, складно було взагалі отримати інтерпретатор JavaScript не в браузері. Тому логічно, що збірка була частиною застосунку, а саме Rake-скриптом. Цей скрипт запускався вже на продакшні, щоб підготувати JS/CSS. Ніхто його не ізольовував від решти застосунку, тому, наприклад, збірка мала доступ до бази. (Я вам навіть скажу, десь в мене був такий код, що витягав з живої бази даних якісь словники та генерував на їхній основі JS.)

Спочатку збірка JS відбувалася на Ruby та за власним для Rails алгоритмом. Але потім в JS виросла власна екосистема, з пакетами, інструментами та так далі. Якийсь час було заведено збирати Webpack окремо, а в Rails тільки зчитувати його маніфест — на то існував пакет webpack-rails. Це було досить чисто, та дозволяло відʼєднати збірку JS/CSS, передавши в застосунок тільки маніфест.

Але у 2019 мода перейшла до webpacker, який знову втягнув Webpack всередину застосунку. Як на мене, то був наслідок поширеної серед рубістів догми “рубі найкращий, давайте все в нього загорнемо”. Та нам знову стало потрібне середовище застосунку, щоб зібрати JS/CSS. Десь в той самий час розгортування переїхало на CI. Проблема з середовищем застосунку в тому, що на CI його треба затикати, та порожня база — це тільки початок. По коду доводиться розставляти перешкоди: “якщо ми у збірці, то сюди не йдемо”. Нові залежності підносять сюрпризи.

Сучасний підхід - jsbundling-rails - надав нам вибір між інструментами збірки, але все ж залишив її у контексті застосунку Rails. Тож нам все одно потрібно середовище застосунку на CI. Та, мабуть, найголовніше що мене дратує — що залишається та давня парадигма, що ми збираємо JS/CSS саме для цієї копії застосунку. Ніби це ми його тільки що залили по SCP на наш VPS, як у 2010-му. Та коли стає питання таку збірку робити артефактом, то мені від того трохи ніяковіє.


21.10.2024

Найскладніший інтеграційний тест

Є тут така ситуація… Сервіс під час своєї зупинки робить рукостискання наступнику. Причому наступника знаходить через AWS API. Причому рукостискання передає контекст, в тому числі зібраний зі чужої бібліотеки. От, коли чужа бібліотека після оновлення несподівано змінила порядок дій, поламала весь процес, та помітили це тільки в продакшні, я вирішив — пора зробити інтеграційний тест!

Таку історію й перевірити вручну не так легко, бо це потрібно не тільки розгорнути версію для перевірки, а два рази її розгорнути — щоб новий код зупинки виконався. На це ні в кого немає часу! Тому автоматичний тест піднесе надійність на новий рівень. Але як його реалізувати?

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

Планував запускати локально дві копії сервісу та перевіряти передачу контексту між ними. Але в тому коді, що його приймає, я теж впевнений — бо його легко перевірити в ізоляції. Тому натомість написав маленький тестовий сервер, який й стане “наступником” в тестовому середовищі. Все, що від нього потрібно — це прийняти запит та зберегти його для перевірки.

А ось роботу з чужими бібліотеками та зупинку точно потрібно робити в інтеграції, бо саме тут виникають помилки, які важко помітити. Тому її залишив без змін. На щастя, зупинити сервіс, запущений в Docker, нескладно.

Завдяки відтинанню зайвого вийшов цілком практичний тест — якщо запускати його останнім, то ще й на зупинці сервісу можна заощадити!


20.10.2024

Оцінювання задач

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

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

У нас наразі є нарада “для збагачення задач”. Оцінюванням ми там не займаємось, натомість командою обговорюємо все наплановане та висвітлюємо незрозумілі моменти. Також інколи розкладаємо на підзадачі. Мабуть, моя улюблена нарада. Зовсім не сумую за дебатами “це задача на 2 бали або на 3”.

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

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

Тому я вважаю, що для високорівневого планування розробки ПЗ достатньо мати вірні приблизні оцінки — може, в місяцях — та ставити за мету дотримуватись цих термінів. Але! Щоб вірно оцінити роботу навіть в місяцях, без деталізації не обійтися. Можна це назвати… оцінюванням з одного балу.


19.10.2024

Що таке — забагато задач?

🐢🐇 Трохи суботнього керування часом.

Минулого разу я писав про завчасну обробку вхідних. Це чудовий спосіб не відставати від життя, та мені дуже подобається починати кожний день з “прибирання”. Але тут є нюанс: з вхідних я створюю проєкти та дії — робота над ними тільки починається. Та що веселіше обробляєш вхідні, то довше ростуть списки дій.

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

З цього вже корисно відокремити дії, старіші за тиждень. Але такий індикатор зʼявляється запізно. Краще щоб метрика зростала від 0, коли всі дії нові, до 1, коли хоч одна з них “прострочена”. А ще краще знати, що дій забагато в той момент, коли хочеш додати нову: бо тоді її можна відразу відкласти на потім. Поки я в пошуках гарної функції.

PS. В Agile часто використовується поняття швидкості (velocity.) Це як раз кількість задач, які команда закриває за ітерацію. Я його і для роботи не люблю (та ми й не використовуємо): на корисний вимір швидкості бракує педантичності. А для особистих задач тим паче: обсяг задач та можливість їх робити надто мінливі.


18.10.2024

Інструкції та розуміння

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

Я зрозумів це на прикладі ферментації. В інтернеті багато інструкцій про те, як закривати посуд для ферментації — зазвичай або спеціальною кришкою з клапаном, або звичайною, але регулярно випускати гази. Складається враження, що нам потрібно: 1) випускати гази — щоб не рвонуло — та 2) захищати посуд від комах, бруду та іншого. Тобто якщо кришки з клапаном немає, а відкривати лінь, то можна бути розумним, прикрити кришкою, але не щільно — та питання закрите? Ні. Це враження хибне. Насправді 3)нам вкрай важливо створити середовище без кисню, тоді на продукті не зʼявиться пліснява. (Також важливо, щоб продукт був кислим — для захисту від ботулінової палички — але це вирішує сама ферментація.) Це вже розуміння, та коли воно є, можна знайти вірну кришку з наявних.

Інструкції, як щось конкретне, писати легше. Навіть не треба самому мати це розуміння — достатньо перелічити власні дії. Тому в проєктах багато інструкцій та менше пояснень, що та навіщо вони роблять. Та тому коли розуміння зʼявляється (часто в той момент, коли попередня інструкція не спрацювала), то обовʼязково треба його записати (часто як коментарі до нової інструкції.)


17.10.2024

Перехоплення запитів в інтернеті третьою стороною

Сьогодні натрапив на цікавий випадок: в одного хостингу SMTP не заблокований зовсім, а перехоплюється їхнім внутрішнім сервісом; хочеш свій — доплачуй. Вихід з такої ситуації простий: використовувати натомість Email API.

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


16.10.2024

Подивитися на задачу з іншого боку

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

В продукту є перелік “останніх розгортувань” кожного сервісу. В розгортування — статус, час початку та кінця, та ще кілька атрибутів як-от гілка. Але головне, що на рівні CodeDeploy немає сутності “розгортування продукту” - тільки по сервісах — та таку сутність я синтезую, оскільки через неї значно природніше дивитися на стан, ніж через мішанину розгортувань.

Визначити статус групи не так легко. Наприклад, якщо всі розгортування мають час кінця, значить, група зараз “в спокої”. Але чи всі сервіси в ній розгорнуті одночасно? Та що таке одночасно? Чи були помилки? Ще цікавіше, коли є активні розгортування. Якщо всі активні — тут все ясно. А коли не всі? Не всі запустилися? Або деякі вже встигли завершитись? Успішно чи не успішно? І так далі.

На початку я дивився на цю задачу саме як агрегацію станів — відповідно, намагався визначити з різних комбінацій, який груповий статус вони представляють: “однорідний”, “неуспішний”, “неоднорідний” (це коли всі успішні, але в різних ревізіях) тощо. Така задача вибухає комбінаторно. Коли на практиці деяка комбінація агрегується не так, як потрібно — то не зрозуміло, куди додавати розгалуження, щоб не порушити іншу логіку.

Тому придумав ось що: все, що мені потрібно — це вибір між декількома станами для групи. Ба більше, ці стани виконують чітку потребу, тому їх легше зрозуміти, ніж абстрактні комбінації статусів. Залишилось для кожного групового стану виписати ознаки: наприклад, “однорідний” значить, що всі розгортування завершені, успішні та відбулися в один час.

В результаті отримую схожий набір розгалужень як в першому варіанті. Але тепер вони мають справжній зміст. Та доповнювати їх буде легше: бо буду знати, з якого стану потрібно “відрізати” умову, та в який додати.

🚶‍♂️ Не випадково, щоб до цього дійти, потрібно було відкласти роботу та сходити на прогулянку.


15.10.2024

PostgreSQL та JSON

PostgreSQL, окрім того, що є вірним вибором бази даних SQL, цілком може також бути вірним вибором бази NoSQL. Принаймні, якщо від NoSQL вам потрібна можливість зберігати JSON, а не здатність до масштабування або всі інші відзнаки, за які зазвичай беруть NoSQL.

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

В такому випадку корисно знати, що в PG можна скористатися вбудованими типами json/jsonb. Це дає купу переваг: зміст поля буде гарантовано коректним JSON, за ним можна спеціальними операторами робити пошук. Навіть будувати індекси за окремими атрибутами, включаючи унікальні, та використовувати в умовах JOIN.

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

Єдине, чого PostgreSQL не вміє робити, так це перевіряти зміст поля — окрім його синтаксису. Ну, та ще унікальності атрибута за індексом. Для перевірки за схемою є доповнення pg_jsonschema - його можна додати як перевірку CHECK.


14.10.2024

gin / slog / sentry

Промучився з нібито простою задачею про моніторинг gin - вебсервера на Go.

Для журналювання в slog є slog-gin. Для збору помилок - sentry-go/gin. Для перекидання помилок з журналу в Sentry - slog-sentry - нещодавно вже про це писав. Ніби всі потреби покриті.

Тепер. Помилок є два різновиди: окрім непередбачених панік в Gin можна завжди ввічливо зареєструвати помилку через виклик ctx.Error(). Тут починається… slog-sentry збирає тільки паніки. А slog-gin - тільки “ввічливі” помилки. Ба більше, помилок за один запит може бути декілька, тому slog-gin показує “агреговану” помилку, яка навіть з однією виглядатиме як Error #1: some error, що мене бісить. Також він чомусь не проставляє в журнал атрибут err, а тільки показує помилку як текст.

Виходить, slog-sentry з slog-gin не дружить (хоч і написаний одним автором!) Бо в Sentry потрапить ото агреговане повідомлення про помилки, а сама помилка (чи помилки) - ні — бо вони беруться з err. Якщо додати обидві інтеграції - slog-gin та sentry-gin - то буде не так, як в коді, що не стосується Gin. А окрім того, жодна ще не дивиться на обидві категорії помилок, тож доведеться розвʼязати це два рази окремо.

Можна не інтегруватися з Gin, а надсилати помилки напряму. Тоді все добре (бо вже налаштовано для решти проєкту.) Але ж так ми не отримаємо контексту вебзапиту - IP, URL, та все інше.

Скоріше за все, адаптую інтеграцію slog-gin, щоб вона генерувала структури, потрібні slog-sentry. Та додам обробник, щоб паніки перетворювались в “ввічливі” помилки. Чому все повинно бути так складно?


13.10.2024

Пошук в застосунку SwiftUI

Робив сьогодні пошук для власного застосунку — бо змісту стало достатньо, щоб без пошуку стало складно. Тут два спостереження.

По-перше, пошук — це складно! Навіть просто за рядком. Принаймні, якісний пошук. Та я не знайшов готових рішень. Пошук тільки починається зі збігів за підрядком. Але також обовʼязково треба ігнорувати регістр, причому не тільки в англійській мові — на щастя, зараз я бачу підтримку цього в більшості бібліотек для UTF-8. Далі, пошуковий рядок може міститись не в одному, а в декількох атрибутах обʼєктів, причому збіги в атрибуті “назва” повинен зʼявлятися вище, ніж в атрибуті “нотатки”. Тобто в нас зʼявляється до фільтра ще й ранжування.

Одним словом, розумієш, скільки всього гарного робить ElasticSearch. Знайшов теоретично два рішення: або Search Kit - фреймворк для системного пошуку macOS, який також здатний шукати в межах застосунку. Або повнотекстовий пошук SQLite FTS3 - SQLite взагалі відомий за простоту інтеграції, тож можна користуватися ним суто як рушієм пошуку. Але мені поки вистачає пошуку за підрядком. :)

По-друге, інтерфейс пошуку у SwiftUI виконаний через набір обгорток та користуватися ним досить неприємно, бо всі ці обгортки заплутують логіку. Спочатку є обгортка searchable() - її потрібно додати на… зміст навігаційного контейнера та це дасть там пошуковий рядок. Окрема обгортка searchSuggestions() дозволить описати, як показувати пропозиції результату. А вже всередині пропозицій є обгортка searchCompletion(), яка й робить компонент пропозицією, яку можна обрати. Трохи нагадує час HOC (компонентів вищого порядку) в React.

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

Все це щоб зробити звичайний інтерфейс пошуку, як в будь-якому рідному застосунку macOS чи iOS (а точніше, в обох, бо код пошуку є переносним.) Веброзробники до того не звикли — та звісно, можна було взяти звичайне текстове поле та список та зробити з них пошук. Тільки “рідним” воно не буде, та й відтворити всі зручності не так легко.