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

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

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

25.02.2023

Інтуїція про швидкодію мов програмування

Яка мова програмування швидка, яка ні? Загальне правило тут старе та просте: інтерпретовані, тобто скриптові, мови — повільні, компільовані — швидкі. Але сучасна арена технологій складніша. Будь-яка успішна інтерпретована мова — будь то Ruby, JavaScript, або Perl - компілюють код під час виконання, щоб його прискорити.

Взагалі програми повільні через невизначеність. Чим більш чітко мова задає програму та її структури даних — тим краще буде працювати. (Компіляція під час виконання саме й аналізує код на наявність “визначеної” частини.) Наочний приклад: Rust проти Go; в Rust треба явно назначати “власність” памʼяті, проте це дозволяє уникнути збірки сміття, яку вимушений робити Go; тож Rust - швидша мова. Керування памʼяттю — це найголовніший фактор невизначеності, що робить програми повільними; системною мовою програмування С вся памʼять виділяється та звільняється вручну — абсолютно визначено та без зайвих витрат часу. Але ручне керування робити складно, та призводить до критичних помилок. Тому більшість сучасних програмістів користуються мовами, що звільняють памʼять самі, неявно. Проте ефективне автоматичне керування памʼяттю — досі не вирішене питання (про що також свідчить існування мови Rust.)

Окрім керування памʼяттю, дорого коштує відсутність типів та усіляка гнучкість. Наприклад, програма на Go під час виконання чітко знає, де в памʼяті знаходиться та чи інша функція; а програма на Ruby має спочатку перевірити, чи не заманкіпатчили її, та взагалі, чи є така функція у даного обʼєкту.

Скриптові мови існують, бо не завжди потрібна максимальна швидкодія. Здатність писати простіші (та менш визначені) програми теж дуже цінна, бо це дозволяє досягнути складнішої логіки меншим кодом. До того ж будь-яка скриптова мова — це неодмінно тільки шар, під яким знаходиться швидкий компільований код. Причому шар порівняно тонкий. Наприклад, може здатись, що в вебі переважають саме скриптові мови. Але ж за додатком на Ruby або PHP або Python стоїть база даних, написана на мові C. А перед ним — балансувальник навантаження, теж на мові C. Тож наш додаток не так вже й багато робить сам по собі.


24.02.2023

Погано чи ненормально?

За результатами вимірювань, інструмент для виявлення SpamAssassin обробляє один лист за 25 секунд. Це добре чи погано?

…Питання з каверзою. Головне, що це ненормально. Пробачте за відсутність кращих термінів, але я маю на увазі, що якщо вимірювання показали такий результат, то щось пішло не так. Звідки це випливає? Наприклад, з того, що SpamAssassin - широко використовувана система, а листи приходять частіше, ніж раз на 25 секунд, тож з такою швидкодією вона просто має не впоратись.

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

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


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. Це єдине місце, де я знаю, що рядки набувають кодування. Бо якщо рядок має некоректні символи, то такий цикл їх зіпсує. Або я майже впевнений, що деякі комбінації кодів він буде нормалізувати. Одним словом, краще бути обережним.