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

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

Підписатись на RSS
📢 Канал в Telegram @stendap_sogodni
🦣 @stendap_sogodni@shevtsov.me в Федиверсі

25.04.2023

Наступність схеми даних

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

В реляційних базах даних з цим трохи простіше, та підходи зрілі (принаймні, в Ruby on Rails це так.) Але дані криються не тільки в базі. Наприклад, в будь-якій черзі теж треба передбачати наступність. Власне, це мене сьогодні й вкусило. Та ще й не на Ruby, а на Go. Тут все ускладнюється типізацією — бо якщо в Ruby можна сподіватись що так-сяк спрацює, то в Go несумісний тип не проходив через json.Unmarshal, та на цьому все завершувалось.

Щоб не мати проблем, потрібно спочатку в одній версій коду додавати поле, але тільки в наступній з ним щось робити. Так само з видаленням полів: спочатку припинити використання та зробити omitempty, а потім вже видаляти з типу. Особливо цікаво це коли треба поміняти тип поля, та я б цього взагалі не радив робити без перейменування (тобто фактично створення нового поля та видалення старого.)

Але якщо вже так сталося, що схема поламана, що робити тоді? Тоді “гарячим” виправленням треба ввести проміжний код, який буде підтримувати обидві схеми. Ну, наприклад, як саме просте рішення, можна пробувати розкодувати в новий тип, а якщо не вдасться — то в старий та адаптувати. Або розкодувати щось в інтерфейс та потім дивитись на тип. (До речі. цікавий момент: якщо в Go розкодовувати чисельне значення з JSON в interface{}, отримаєш тип float64, бо технічно в JSON немає різниці між цілими та дробними числами.)

Раджу обмежити виправлення вхідною частиною програми, а далі використати тільки чистий новий тип — як всередині бізнес-логіки, так і на запис. Це локалізує “нечіткий” код та уникне помилок в головній частині програми.


24.04.2023

Розширення та знущання з Ransack

Сьогодні половину дня намагався зробити новий фільтр для Ransack. То є така бібліотека для Ruby on Rails, щоб будувати запити до бази з користувацьких форм. Використовується для всілякого пошуку; в тому числі пошук в ActiveAdmin використовує саме його.

Власне, шляхів розширення в Ransack вистачає, але майже всі вони мають відповідати закладеній абстракції: до деякого атрибута прикладається деякий оператор, можливо — зі значенням. Наприклад: {"name_eq": "Grigory"}. Оператор має бути виражений в Arel, тому якщо це не вдається, вони пропонують манкіпатчити Arel, щоб доповнити його власними предикатами.

Але в мене задача вийшла ще складніше. Мені треба було поміняти поведінку оператора equals, але тільки для одного поля. Тобто зовнішній інтерфейс залишити без змін. Гарного способу це зробити я не знайшов; негарний складався з того, що перед запитом я підміняю оператор на інший, custom_field_eq. Але як можна було помітити в попередньому параграфі, запит склеює поле та оператор, тобто остаточно фільтр виглядає як {"custom_field_custom_field_eq": "Grigory"}.

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

Можна подумати, що якщо так, то не треба було додавати оператора. Але ні — здається. скоупи мають нижчий пріоритет, тому на фільтр custom_field_eq викликатиметься стандартна логіка, а не скоуп.


23.04.2023

Декілька можливостей сучасного CSS

Погода сьогодні хороша, тож прогресу не так багато. Ще декілька штук з CSS, про які я дізнався за останні дні:


22.04.2023

Прогрес по фотозвіту про походи

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

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

Далі, ідея в тому, що зміст поста, з усіма світлинами, відповідає домовленостям Hugo. Тобто пост пишеться в Markdown. Як до Markdown додати “журнальний формат”? Власними шорткодами. Шорткоди можуть додавати до тексту будь-яку розмітку. Вони можуть як вставляти її (наприклад, шорткод світлини), так і загортати текст (так можна створити виноски тощо.)

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

На останок додам, що сучасний CSS - це приємно. Я людина травмована попередньою епохою верстки на floatах. Флоати були складною та неприродною поробкою. Плюс до того браузери працювали по-різному. Тому CSS був езотеричною технологією, де без глибокого досвіду було важко зробити щось красиве, зате легко — поламати.

Зараз все набагато краще. Нюанси поведінки браузерів практично зникли. Верстка на flexbox підтримується у 99% клієнтів. Модель flexbox прозора, її легко опанувати, та вона робить те, що від неї очікуєш. Багато всього, що раніше робилось за допомогою JavaScript, тепер вирішується засобами CSS. От сьогодні дізнався про scroll-snap; чудово!

Тож раджу спробувати, може й тебе CSS приємно здивує.


21.04.2023

Мінімальний масштаб сервісів

Байдуже, наскільки мало навантаження є на сервіс або мікросервіс — в мене є базовий стандарт, нижче якого “живі” продукти не мають спускатись.

Перше — будь-який сервіс повинен мати цілий CPU. Проблема не в тому, що дробового CPU вам не вистачить — багато сервісів ніколи не здіймаються вище за 10% використання CPU. Проблема в тому, що, за моїм досвідом, дробовий час CPU - це абстракція. Він не забезпечується надійно та рівномірно, та в непередбачуваний момент сервіс може не отримати свою долю. (що ззовні спостерігається як затримка) Тому, тільки цілий CPU, або декілька. Чи потрібні декілька? Залежить від того, наскільки сервіс здатний до паралелізації. Якщо паралелізація досягається шляхом створення декількох процесів — як роблять сервери додатків Ruby - то є наступне питання — робити одну репліку з декількома CPU, або багато реплік з єдиним CPU?

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


20.04.2023

Стендап Сьогодні — тепер і в Twitter

Є така концепція WOPE (Write Once Post Everywhere). Зміст її в тому, що в наш час закритих платформ (Телеграм, Твіттер, Фейсбук і так далі) має сенс дублювати свій контент на всіх платформах (та, звісно, у відкритому Інтернеті в першу чергу) - так до нього матимуть доступ найбільша кількість споживачів. Тож працюю потрохи в цьому напрямку. З сьогоднішнього Стендапу канал також транслюється у Twitter @stendap_sogodni.

Зрозуміло що для мене, як для інженера, найцікавіша в цьому технологічна складова, тож надсилання в Twitter виконує той самий скрипт, що й в Telegram, а в майбутньому скрипт має розростися в потужний WOPE-комбайн. Трохи специфіки:


19.04.2023

Кілька порад по графіках в AWS CloudWatch

📊 AWS CloudWatch - апарат Amazon Web Services для збору та аналізу метрик. Я успішно користуюся їм вже декілька років. Консоль CloudWatch, хоч і потужний інструмент, дуже мало пояснює, що з нею можна робити — тож ось кілька порад.


18.04.2023

64 біти має вистачити кожному

Ми живемо у вік рясності цілих чисел. Майже будь-який сучасний процесор має розрядність у 64 біти. Це розмір “нормального” цілого числа, або, як його називають, машинного слова - 64 біти, 8 байтів, або ж, у десятковій системі, більш ніж 10^19 можливих значень. Це буквально астрономічне число, якого достатньо, щоб підрахувати практично все, що нам може бути потрібно.

⭐ Може, зірки ми рахувати й не будемо, але можемо впевнено рахувати наших користувачів та створені ними обʼєкти. Таймстемпи з мілісекундною точністю займають всього 44 біти; це залишає ще 20 бітів вільними для іншого призначення. У 64 біти можна закодувати множину з 64 можливих значень — та робити над нею зручні побітові операції. Але головне, що 64 біти дозволяють адресувати будь-який можливий масив памʼяті.

Про це варто думати хоча б тому, що не так давно такої рясності не було. Максимальне 32-бітне число — всього лише 4 мільярди (а зі знаком - 2). Це вже зовсім досяжне значення; та з усіх лічильних речей, його перед усім не вистачало для памʼяті. Чотири мільярди — це лише 4 ГБ адресного простору памʼяті. Та якщо оперативна памʼять у 4 ГБ — не так вже й мало, то з дисками доводилось робити хитрі речі — розбивати на сегменти, наприклад. Розмір файлів в типовій 32-бітній файловій системі теж був обмежений двома гігабайтами.

Та й легендарна фраза 640 кілобайтів вистачить кожному має корені саме в розрядності машинного слова. Бо на той час процесор Intel 8088 більше не міг адресувати; пізніші процесори дозволяли адресувати додаткову памʼять за допомогою дуже хитрих систем - EMS та інших — та пересічному програмісту, який хотів більше памʼяті, треба було у всьому цьому розбиратись.

Так що, давайте цінувати те, що зараз ми можемо просто взяти тип Integer та не турбуватись про його переповнення; мати компʼютери з 24 ТБ памʼяті; та файли робити такими великими, як нам захочеться.

(Раніше: про UUID)


17.04.2023

Бот для Телеграму вчиться генерувати посилання на пости в Телеграмі

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

Загальна ідея: впровадити особливу розмітку для посилань на пости. Власне, розмітка така вже є — це шорткоди ref та relref в Hugo. Оскільки пости на сайті обробляються саме через Hugo, то іншого варіанту, практично, немає. Посилання виглядає приблизно так: [вчорашній пост](/stendap/2023-04-16/). Залишається тільки додати підтримку такого коду в рендерер постів для Телеграму.

Як я писав раніше, рендерер використовує той самий пакет Goldmark, як і сам Hugo. Тому перша ідея була — використати код Hugo. Я навіть знайшов місце, яке обробляє шорткоди, втім, рендерер Hugo виявився дуже складним, та не модульним. Оскільки мені тільки потрібно переробляти один-єдиний шорткод, то я не став зʼясовувати, як саме витягнути той рендерер з коду Hugo (на що він не розрахований) та адаптувати для мене.

Тому почав копати в бік розширення Goldmark власноруч. Для впровадження власного коду розширяють парсер. Для того треба реалізувати інтерфейс parser.InlineParser, та визначити маркер початку коду, та парсер для нього. Парсер може бути просто регуляркою. (Власне, про це можна та треба велику статтю писати.) Все було б добре, але ні — виявилось, що власний код не може бути всередині адреси посилання. Це досить логічно, якщо подумати, бо в адресі не може бути й жирного шрифту або, наприклад, іншого посилання. Технічно так відбувається тому, що зміст адреси зчитується парсером посилань як єдине ціле, та не передається для подальшого розбору.

Наступна спроба — трансформувати адреси. Для цього є інше місце розширення Goldmark - parser.ASTTransformer. Типовий підхід тут — викликати вбудовану функцію обходу синтаксичного дерева ast.Walk, та виконувати дії над конкретними вузлами. В моєму випадку, якщо бачу посилання, та в адресі сидить шорткод — то його можна регуляркою замінити на справжню адресу. Це майже розвʼязало задачу, от тільки Goldmark має деякі обмеження на зміст адрес, та шорткод в чистому вигляді ламає парсер адреси та залишає посилання необробленими.

Остаточне рішення гібридне. Спочатку регуляркою знаходжу по тексту поста шорткод, та заміняю на форму, що підходить для адреси: PLACEHOLDER/docname/. Потім — пропускаю через Goldmark, та за допомогою трансформера перетворюю цю форму в адреси. Нарешті — після Goldmark знову регуляркою повертаю решту замінених шорткодів назад в оригінальну форму. (Останній крок потрібний, наприклад, щоб зробити приклад шорткоду з другого параграфу.)


16.04.2023

Заголовки постів — тепер в Телеграмі!

Зробив сьогодні маленьку фічу, яку давно хотів. У моїх постів досить давно є заголовки, але вони зʼявляються тільки на сайті та RSS. В Телеграмі заголовків не було, бо мій скрипт їх не підтримував. Нарешті, коли заголовки реалізовані, поділюся проблемами та нюансами.

Заголовки постів, як вони є, містяться в front matter до поста. Вони не є частиною тексту. Так Hugo може показати заголовки на сторінці переліку постів, в RSS, в тезі <title> і так далі. Але для мого скрипту для постінгу в Telegram це проблема, бо він генерує текст посту в Телеграмі шляхом перетворення вхідного тексту в Markdown у HTML; при цьому титул, як і вся інша передня частина, ігнорується. Ну, точніше, не ігнорується, а виноситься в окрему змінну за допомогою плагіну goldmark-meta; але потім титул треба приєднати до поста… або рудиментарним шаблоном, або — як я поки роблю — просто склейкою. Та при цьому не забути про санітаризацію.

Друга проблема — як уникнути масової зміни постів. Річ у тім, що мій скрипт дозволяє не тільки постити, а й редагувати пости. Це практично корисно тільки для виправлень сьогоднішнього посту, але можливість редагувати залишається для будь-якого з всієї історії. Для цього я зберігаю співвідношення між постами в Hugo та в Телеграмі. Щоб не перепощувати всі пости кожний раз, я зберігаю також контрольну суму тексту; якщо контрольна сума не змінюється, то запит до API Телеграму не відбувається. (Взагалі, контрольних сум дві; одна на весь зміст вхідного файлу, а інша — на результативний текст посту.)

Але. Якщо додати в скрипт титули, то текст абсолютно всіх постів зміниться, та всі вони будуть оновлені в Телеграмі. Мені це не подобається — краще, якщо старі пости залишаться як є. Тому додав також версіювання до моєї таблиці співвідношення. Всі пости отримують версію 0, а нові пости, починаючи з цього — версію 1. Скрипт має також поточну версію; пости з версією, що старіше поточної, просто ігноруються. Таким чином, зберігається можливість редагувати, але додавати функції. Тобто відкриває двері для майбутніх покращень.