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

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

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

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. Скрипт має також поточну версію; пости з версією, що старіше поточної, просто ігноруються. Таким чином, зберігається можливість редагувати, але додавати функції. Тобто відкриває двері для майбутніх покращень.


15.04.2023

Робота з відволіканнями — підсумки тижня

Звітую про підсумки тижневої спроби знизити кількість відволікань.

На наступному тижні буду працювати з планами реалізації.


14.04.2023

Чим круті канали в Go

Сьогодні переробляв надсилання подій в AWS Kinesis Firehose на пакетний режим, методом PutRecordBatch. В самому по собі API нічого цікавого немає, проте реалізація цього на Go досить цікава.

Оскільки події відбуваються в багатопотоковій системі, то щоб зібрати їх до купи, потрібна якась модель асинхронної поведінки. Та де в гіршій мові треба було б брати мьютекс, в Go є елегантне та простіше в розумінні рішення — канали.

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

Канали — то моя найулюбленіша особливість Go, хоча, треба зізнатись, місць, де їх доводиться впроваджувати, зустрічається небагато. Головне, що треба усвідомити про канали — що вони синхронні. Цим Go відрізняється від більшості асинхронних моделей, де відправлення повідомлення не гарантує його прийняття (модель акторів, наприклад.) В Go, якщо одна горутіна кладе дані в канал, то можна бути впевненим, що інша забирає їх у той самий час. (Якщо канал не буферизований.) Це дозволяє впевнено міркувати про потік даних в системі. Операції з каналами створюють в програмі точки синхронізації, та багатопотоковий алгоритм набуває схожості до часового механізму. Особливо це корисно, коли треба ізолювати власність над даними — як в сьогоднішньому випадку.


13.04.2023

T-Shaped Full-Stack Developer

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

Головна функція фул-стек інженера — мати розуміння всієї технічної складової продукту. Це суттєво спрощує керування розробкою, порівняно з тим, коли кожною частиною займається хтось інший. Фул-стек інженеру можна доручити цілу фічу, без нарізання по підсистемах. Або виправлення помилок невідомо де. Існування ролі фул-стек інженера обґрунтовано потребами бізнесу.

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

Такий підхід не обмежений програмуванням. Наприклад, в медицині є терапевт, що готовий прийняти тебе з будь-якою скаргою. Та від рутинних болячок саме терапевт й лікуватиме. Але якщо виникає потреба в більш серйозних діях, то терапевт делегує лікарю-спеціалісту. Терапевт — то є фул-стек лікар. 👩‍⚕️

Проте, чесно кажучи, найняти та утримувати вузького спеціаліста — складно. Простіше, коли в кожного фул-стек інженера є своя заглиблена спеціалізація, з якою він може допомогти колегам. Це й називається T-образний інженер. Тобто “—” то є фул-стек, а “|” то є твоя спеціалізація. В більшості випадків T-образний інженер працює над загальними задачами, але якщо в команді виникають складнощі, то перемикається в режим допомоги.

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

(До речі, як на мене, “фул-стек” як назва посади стала архаїчна. Колись, у 2010-ті, всі вебдодатки робились за однією схемою — додаток на сервері, база даних, HTML, CSS, може трошечки JavaScript для блиску. Тож “фул-стек” значило вміння працювати з усім цим. Але зараз стек може бути абсолютно різний, а посада залишилась.)


12.04.2023

Парсинг та обробка HTML в Go

Якщо в Go доведеться парсити HTML, то для того є непоганий офіційний пакет golang.org/x/net/html. Він здійснює потокову обробку документа; тобто ніякого DOM-дерева не будує, а замість того пропонує пройтись по документу циклом та відреагувати на всі потрібні елементи. Це дуже відрізняється від роботи з HTML у JavaScript та від культової бібліотеки Nokogiri для Ruby.

Сучасні вебінженери звикли бачити HTML як дерево, та адресувати це дерево селекторами CSS: div#header a.nav-link. Це просто в розумінні, але складно алгоритмічно: бо потребує спочатку побудови дерева, а потім ще й пошуку по ньому.

Натомість при потоковій обробці ми маємо заздалегідь запланувати, що хочемо отримати з документа, та власноруч стежити за станом обробки. Наприклад, коли зустрінемо тег div, то перевіримо, чи є в нього атрибут id==header, та якщо є, увімкнемо змінну inHeader = true. А коли зустрінемо тег a, то перевіримо, чи увімкнена змінна inHeader, та якщо так, запишемо в масив navLinks.

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

Але є ситуація, де потокова обробка прямо набагато краще. Це коли документ ми змінюємо. Річ у тім, що розбір документа в дерево — операція зазвичай незворотна; вона втрачає нюанси форматування, та особливо — помилки синтаксису. А з таким неформатним форматом як HTML, помилка синтаксису цілком може бути помилкою тільки для даного парсера — та її “виправлення” навпаки документ поламає (прибере “неправильний” тег, наприклад). Тому й варто нічого не міняти без потреби.

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


11.04.2023

Підтести в Go з функцією T.Run

Сьогодні весь день писав тести на Go. Та, на сьомий рік написання тестів, нарешті дізнався про механізм створення підтестів: t.Run.

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

Я теж постійно використовую матричні тести. З ними є маленька проблемка: коли тест падає, то незрозуміло, в якому саме випадку. Тому я зазвичай до кожного очікування дописував повідомлення, по якому можна знайти випадок, якось так:

assert.Equal(t, e.expectedResult, myCode(e.param), e.name)

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

Торік дізнався про існування пакета testify/suite. Їм можна загорнути набір тестів у спільний контекст, такий, як підключення до бази даних. Саме suite мене й наштовхнув на роздуми — а як це вони роблять тести у вкладеному форматі - Suite/TestName? Виявилось, що це вбудована можливість тестового середовища Go, називається підтести, та створюється функцією T.Run.

Підтести це те, чого мені не вистачало в матричних тестах, бо тепер достатньо просто загорнути тіло тесту в t.Run та отримати як підпис кожного підтесту в результатах, так і можливість фільтрувати їх ключем --run. А з пакетом testify/suite можна ще й додавати до підтестів контекст.

for _, e := range examples {
  t.Run(e.name, func(t *testing.T) {
    assert.Equal(t, e.expectedResult, myCode(e.param))
  })
}

Може, це й щось очевидне, але я загуглив пару топових туторіалів та вони матричні тести показують, а підтести — ні. Отакої. TIL.