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

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

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

23.02.2023

Декілька думок про гарний сон

Сьогодні намагаюсь лягти спати раніше. Як я колись писав, сон — найґрунтовніший фактор якості життя, разом з харчуванням та фізичною активністю. Але ж знати це простіше, ніж влаштувати собі якісний та довгий сон. Кілька думок про це.

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

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

Також, я не доктор, але можу порадити препарати магнія+B6 для покращення якості сну. Причому краще над усе знайти таурат магнію (magnesium taurate). Магній також допомагає концентрації, тож для роботи головою це важлива добавка. (Але магнієм єдиним, на жаль, всього не виправиш.)


22.02.2023

Mermaid - формат побудови схем

Якщо доводиться писати документацію у Markdown, раджу ознайомитись з генератором схем Mermaid. Бо в багатьох редакторах з Markdown підтримка Mermaid вбудована; достатньо помітити блок з вихідним кодом мовою '''mermaid та він буде замінений на відповідну схему. Так роблять GitHub, Obsidian, та навіть попередній перегляд Markdown у VSCode.

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

Ще, випробував сьогодні Redshift Serverless для запуску тестів на CI. Ой, погана це ідея. Виходить замість $216 на місяць за кластер близько $0.72 за хвилину тестів, причому швидше працювати вони не стали. На мою думку, Serverless підійде тільки для випадків, коли базою користуються в ручному режимі та рідко — може, щоб згенерувати звіт раз на день.


21.02.2023

Виправляв помилки з матеріалізованими розрізами в Redshift

Сьогодні день видався виснажливий. З ранку до ночі шукав, чому мої розрізи, які так чудово працювали в локальному Postgres та в редакторі запитів Redshift, відмовились створюватись на CI. Встиг навіть створити та розвʼязати запитання на StackOverflow. Нарешті, знайшов — методом спроб та помилок. Це те, що мені найбільше не подобається в Redshift - деякі особливості ніде не пояснені, при цьому текст помилок веде на зовсім невірний хід думок. Мабуть, вже треба писати свій гайд.

Що трапилось. Спочатку була помилка, що у користувача не вистачає прав на якусь системну таблицю. Раніше я робив експерименти від суперкористувача, тому вирішив, що у CI користувача немає прав. Як дати права? Команда GRANT працює тільки після факту. Є ALTER DEFAULT PRIVILEGES - не працює. Придумав створити процедуру, яка робить GRANT: якщо у процедури буде атрибут SECURITY DEFINER, то вона надасть CI користувача право на цю команду (щоб дати собі права.) Задоволений таким кмітливим рішенням, запустив тести на CI та… виявилось, що хоч створити розрізи вдалося, але тепер їх неможливо оновити. Чудово.

Виходить, що залежність розрізу від системної таблиці — це в принципі неправильно, та Redshift мав би так і сказати, а не водити за ніс. Тоді чого не вистачає моєму розрізу-основі, щоб цієї залежності не було? Щоб визначити це, зробив копію коду, що створює розрізи, та почав викидати з нього все підряд, запускаючи, щоб перевірити, чи не зникла проблема. Та нарешті виявилось, що треба не викидати, а додати — а саме, всі стовпчики з GROUP BY мали бути присутніми в SELECT. Напевно, інакше Redshift не може відстежувати зміни до розрізу, тому створює системну таблицю. Якби це був тільки один розріз, то я б й не помітив, а з залежностями маємо такі незрозумілі помилки.

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


20.02.2023

Навіщо писати тести потім?

Сьогодні практично весь день писав тести для коду, який вже був написаний. Насправді навіть не для коду, а для розрізів в базі. Але тести на RSpec, бо кращого способу не знаю.

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

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

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


19.02.2023

Про ключі в 1Password, Git та ssh-agent

Продовження історії з ключами в 1Password.


# .envrc
export AWS_ACCESS_KEY_ID="op://My Vault/AWS Access Key/access key id"
export AWS_SECRET_ACCESS_KEY="op://My Vault/AWS Access Key/secret access key"
# виклик
op run -- terraform apply

Можна знайти в інтернеті згадки про використання ssh-agent з macOS Keychain. Насправді у Keychain зберігатиметься тільки парольна фраза, а не весь ключ. Ключ так само залишається в на диску. Тому я не впевнений, що це так потрібно. (Сподіваюсь, очевидно, що ключі обовʼязково мають бути зашифровані парольною фразою.)

Додатково зʼясував, що підписування змін в Git теж можна робити без 1Password, це функція звичайного ssh-agent:

# ~/.gitconfig
[user]
  signingkey = (ssh-ed25519 публічний-ключ...)
[gpg]
  format = ssh


18.02.2023

Навігація як одна з кореневих механік у Morrowind

Одна з визначних та рідких особливостей Morrowind - це присутність навігації як головної механіки. У 2023, після того, як ми бачили Death Stranding, де взагалі вся гра — це перетин місцевості, таке вже не звучить дуже дивно. Але підхід Morrowind інший. Тут фокус на розуміння інструкцій, орієнтацію на місцевості… а також навички користування публічним транспортом!

З першого вигляду, Morrowind - такий самий “відкритий світ”, як і безліч сучасних ігор, взяти хоча б ЇЇ нащадків Oblivion та Skyrim. Так само більшість завдань складається з “йди туди, принеси те”. Але якщо в інших іграх завжди є маркер, що вказує ціль на карті, то в Morrowind маркерів немає — та й взагалі карта рудиментарна. Кожне завдання доводиться спочатку знайти, користуючись приблизними інструкціями, наприклад “йди вздовж східної річки на південь, як дійдеш до озера то печера буде на острові посередині”. Може здатись, що вимкнувши маркери, можна з будь-якої гри зробити те саме. Але справа не тільки в необхідності навігації, а більше в наявності інструментів переміщення та можливості їх опанувати. Саме можливість набуття навичок й робить навігацію приємною механікою.

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

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

Хоча “літаємо” можна зрозуміти й буквально, бо інший інструмент переміщення — то магічні покращення руху. Закляттями можна прискорити ходу (ніяких коней не потрібно), або взагалі левітувати, в тому числі — перелітати через всю карту. Магія в Morrowind неперевершена. Наприкінці гри чаклуванням можна фактично поламати всі труднощі та обмеження гри, та на навігацію це беззаперечно розповсюджується. (Це, до речі, чудово перекликається з сюжетом, де персонаж буквально стає божеством. Повна людонаративна гармонія!)


17.02.2023

Розрізи з інкрементальним оновленням в Redshift

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

На жаль, здогадатись, що розріз не здатний до інкрементального оновлення, важко. Тож додав автоматичний тест, який перевіряє це для всіх розрізів, якось так: SELECT name, state=1 AS is_incremental FROM stv_mv_info. Без цього в майбутньому ми приречені наробити собі проблем.

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

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

  2. На агрегаційні функції можна накладати інші функції, або навпаки, вкладати, та це не порушить інкрементальність. Наприклад, можна зробити date(min(created_at)).

  3. АЛЕ! Коли один розріз залежить від одного, то інкрементальним він стане тільки якщо не залежить від стовпчиків з “загорнутою” агрегаційною функцією. (Це, напевно, псує відстеження змін.) Тож, щоб виправити, можна внести функцію під агрегацію: min(date(created_at)). (Якщо це дозволяє логіка.)

  4. Ще можу рекомендувати редактор запитів Query Editor v2, що вбудований в AWS Console. В ньому є дуже корисна функція “записничок” (notebook) - мені було зручно готувати експерименти з матеріалізованими розрізами. Бо експериментувати доведеться багато, це вже зрозуміло. Перед тим, як впроваджувати розріз в додаток, раджу випробувати його вручну та наживу, та перевірити, чи є він інкрементальним.


16.02.2023

Чому в Go є тип string та byte[]

Сьогоднішній баг стосувався того, що в Go у змінну з типом string передавали ззовні значення nil. (Там вхід з кодеком, що побудований на рефлексії, тож таке технічно можливо.) Виправилось це заміною типу на *string, який може мати або значення nil, або вказувати на рядок. Наскільки я знаю, доволі ідіоматичне рішення. Але воно наштовхнуло на роздуми — якщо в Go рядок це технічно слайс байтів, а слайси можуть бути nil, то чому рядки не можуть? Моя відповідь — напевно, щоб абстракція рядків була красивішою, тобто щоб рядки виглядали більше як “примітивний” тип.

Ось інші нюанси роботи з типами string та byte[]. Конверсія з рядка в слайс не безплатна, на відміну від інших. Операції string(someBytes) та []byte(someString) створюють копію даних. Це потрібно тому, що рядок — тип незмінний, тому не може розділяти памʼять зі змінним типом — слайсом. При цьому є багато API, які приймають тільки слайси або тільки рядки, тож деколи конверсії не уникнути. В мене був навіть випадок, коли рядкову функцію довелось переписати для слайсів, щоб прискорити вузьке місце. Ще, наприклад, функції форматування приймають обидва: можна написати fmt.Printf("contents of file are: %s", byteSlice).

Можна подумати, що рядки мають якусь семантику символів, тобто кодування. Але ні — в Go рядки складаються з байтів. Довжина вимірюється в байтах, позиції теж. Є один нюанс — цикли з range перебирають символи UTF-8. Це єдине місце, де я знаю, що рядки набувають кодування. Бо якщо рядок має некоректні символи, то такий цикл їх зіпсує. Або я майже впевнений, що деякі комбінації кодів він буде нормалізувати. Одним словом, краще бути обережним.


15.02.2023

Історія сьогоднішнього багу

Тільки закінчив випускати хотфікс. Випускати щось в продакшн вночі — завжди цікаво, та завжди з історією. Що ж трапилось на цей раз?

Баг зʼявився у надсиланні пошти. Його помітили ще на стейджингу — чомусь листи не відправлялись з помилкою SMTP 503 5.5.1 Error: authentication not enabled. Але на стейджингу принаймні я багато уваги цьому не приділив, бо було більше схоже на проблеми з налаштуваннями середовища, ніж на реальну проблему. Але ж на продакшні баг відтворився, та почав перешкоджати надсиланню чесних користувацьких листів. Довелося терміново розвʼязувати.

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

З нещодавніх змін до проєкту підозрою є оновлення Ruby on Rails. Оновлення мінімальне та стосується тільки патч-версії, але все ж таки це глобальна зміна, яка теоретично може вплинути на будь-що. А конкретно, при перегляді різниці файлу Gemfile.lock бачу, що помінялася також версія гему mail. Його має сенс перевірити в першу чергу.

І нарешті, в переліку змін гему mail бачу цікавий рядок “Bug Fixes: Regression: accept enable_starttls_auto: false”. Знайшов! (Окреме питання, що написано, що вона unreleased, хоча вже 2 тижні як released. Але буває.)

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

Що робити, щоб такого не було? Напевно, головне, це підтримувати стейджинг в повністю робочому стані, як пріоритет №1. Щоб впевнено шукати помилки, навіть якщо вони не повʼязані очевидно з поточними змінами.


14.02.2023

Інкрементальні розрізи подій у Redshift

Нарешті сьогодні розкусив дві проблеми, що мене турбували по Redshift.

Перша — як я вчора писав, події краще комбінувати… логічною вершиною такого підходу буде взагалі позбавитись від різних потоків для типів подій та звалювати все в одну велику таблицю. Далі це дозволить нам згрупувати таблицю по спільному ключу, та агрегувати результат. Головне, щоб агрегація підпадала під інкрементальне оновлення. Бо тоді ми звільняємося від необхідності сканувати всю таблицю, а будемо завжди працювати тільки з новими подіями — це виводить здатність масштабування на новий рівень. Обмеження для інкрементального оновлення досить суворі, але коли всі події в одній таблиці, то можна робити цікаві речі, наприклад: COUNT(NULLIF(event_type = 'purchase', false)) AS purchase_count.

Друга — була така задача, щоб в межах GROUP BY взяти деяке поле з найсвіжішої події. Визначити час цієї події легко - MAX(created_at)… але ж агрегаційні функції працюють в межах стовпчика, тому інші стовпчики за таким запитом не знайдеш. Винайшов трохи хитрий підхід. Якщо інше поле — то рядок, то можна додати до нього час події як префікс. Час має бути в форматі з лексикографічним порядком — наприклад, ISO8601. Тепер можемо знайти останнє за часом значення будь-якого стовпчика - SUBSTRING(MAX(prefixed_item_id), 24). Як бачите, до результатів агрегаційних функцій можна застосовувати інші функції, та це не зашкодить інкрементальному оновленню.