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

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

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

08.02.2025

Новини по HugoWope / OmniWope

Доробляю потрохи бот/скрипт/застосунок для ретрансляції блогу куди тільки можна.

Мій оригінальний скрипт займався “всім відразу” - читав пости з диска, розбирав метадані, будував дані для Telegram. Такий скрипт дуже важко зробити загально доступним, бо там навіть на назву розділу на сайті є завʼязка.

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

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

Отже, тепер проєкт називається OmniWope (Write Once Publish Everywhere). Але почнеться з модулів для підтримки Hugo та Telegram; до якого невдовзі додасться ActivityPub, заради якого я все це роблю.

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

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


07.02.2025

Особливості помилок в Go: квазістектрейс

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

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

(Зокрема, в error немає детальної типізації (ну як, вона може бути, бо error - це тільки інтерфейс, але це буде нестандартно.) Щоб впізнати потрібну помилку, її роблять константою, та порівнюють за значенням.)

Також в error немає стектрейсу (є бібліотеки, які його додають, але не загально прийнятні.) Але замість стектрейсу, в Go є загортання помилок. Коли вам повертають помилку, традиційно функцією fmt.Errorf додають до неї власну інформацію.

res, err := http.Request(myURL)
if err != nil {
  return fmt.Errorf("loading URL failed: %w", err)
}

На верхньому рівні отримуємо щось на кшталт: failed to calculate balance: failed to load user: database request failed: io timeout. Так виглядає типова помилка у Go. Та ось сьогодні збагнув, що це ж той самий стектрейс! Тільки ручної роботи. Можна грепати його частини та знайти, звідки вони.

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

Власне, тому важливо завжди загортати помилки. Навіть є лінтер wrapcheck, який на це перевіряє.


06.02.2025

Go - що це за мова?

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

Для мене Go - це мова синіх комірців. Не академічна. Не поетична. Не модна. Робітнича. Але при тому Go ніяк не є архаїчною мовою — такі аспекти, як керування залежностями, знаходяться на передній межі сучасності.

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

Для мене Go - мова “бери та й роби”. Мова YAGNI. Мова “я встигну це за вихідні”. Мова “мене цікавить результат, а не процес.” Мова для інженерів, а не програмістів.

(Але якимсь чином цей підхід тут масштабується, бо на Go написані такі величезні проєкти, як Docker, Kubernetes чи Terraform, а також Hugo, Grafana, Ollama та багато всього іншого.)

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


05.02.2025

Контексти: слова-паразити в Go

Друга, новіша ідіома в Go, яка засмічує код — це контексти. Я вже писав, що контексти надійно розвʼязують складну проблему скасування операцій, в тому числі тайм-аутів. Але також передача контекстів зроблена… скажімо так, якби це придумали в нас на проєкті, то я б всіма силами протистояв.

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

func (s *Service) GetUserBalance(ctx context.Context, id int) (*Results, error) {
  cachedResults, err := s.cache.Get(ctx, s.cacheKey(id))
  if err != nil { // і цього не забудемо
    return nil, error
  }
  if cachedResults {
    return cachedResults, nil
  }
  user, err := s.queries.GetUser(ctx, id)
  // ... and so on
}

Причому контексти самі собою нічого не скасовують. Це задача самої функції. На практиці вони впливають тільки на операції вводу-виводу. Щоб скасувати, скажімо, тривалі обчислення, доведеться час від часу перевіряти стан контексту вручну.

Але головне, що, як і з помилками, обробка контекстів в 99 зі 100 випадків однакова: вони тільки передаються далі. Гадаю, було б краще, якби в Go 2 контекст передавався в кожну функцію сам собою, за замовчуванням. Або ще краще, якби його передача відбувалася тільки там, де компілятор бачить в контексті потребу (тобто залежність від функцій, які самі потребують контекст.)

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


04.02.2025

Обробка помилок: слова-паразити в Go

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

res, err := client.Get("https://api.site.com")
if err != nil {
  return nil, fmt.Errorf("can't get: %w", err)
}
err = json.Unmarshal(res, &data)
if err != nil {
  return nil, fmt.Errorf("can't unmarshal: %w", err)
}

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

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

Враховуючи те, що ця ідіома одна з найпоширеніших, можна тільки сподіватись, що в Go 2 щось додадуть на заміну. Наразі в трекері 12 відкритих та 182 закритих пропозицій з обробки помилок — тема дійсно гаряча. Особисто мені подобається res := try fn() (причому помилка неявно передається нагору) або res := fn!() - тобто щоб того блоку обробки взагалі не було.


03.02.2025

Обробка відмови в умовах високої надійності

Давайте продовжу тему цього посту та розкажу про власний досвід виконання вимог високої надійності.

Питання там стоїть таке: що повинен робити сервіс, залежність якого (база, наприклад) недоступна: 1) валитися напрочуд, чи 2) відмовляти клієнтам та чекати відновлення бази? Я погоджуюся, що краще друге, але на те в мене є ще декілька причин.

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

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

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

І нарешті, для найкритичніших випадків залежність від бази можна перекривати локальним кешем. Наприклад, якщо база потрібна для авторизації: зберігати в памʼяті користувачів за 10 хвилин, та ми й базу розвантажимо трохи, й зможемо врятувати деяку частину запитів в разі відмови бази.

До речі, щодо оточень, де помилки легко можуть вести до вивалювання сервісу — можу запропонувати Go - там паніка (тобто “виняток”, помилка без явної передачі) в будь-якому з потоків моментально кладе весь процес.


02.02.2025

Рік з фітнес-кільцем Oura

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

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

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

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

Дуже подобається мова, якою спілкується Oura - вона не така бадьоро-позитивна, як завжди, а дійсно дружня. Якщо ти гарно відновлюєшся — вона підбадьорює. Коли наступає чорна смуга — співчуває та пропонує відпочити. Мені в житті цього не вистачало.

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


01.02.2025

Робота з iCloud Drive та резервне копіювання

🛟 Сьогодні намагався (та зробив!) виконати ще одне прохання користувачів: додати резервне копіювання бази (таймтрекеру.) Функція очевидна, та ще й врятує мене, якщо раптом випущу збірку, що псує дані. Звісно, як місце для збереження копій я обрав iCloud Drive. Бо він вже доступний для всіх без зайвої авторизації. Отже.

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

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

Потім… потім воно працює з коду (ну, не на симуляторі, якщо тільки не увійти там у свій iCloud). Але в iCloud Drive файлів я не бачив. Було важко зрозуміти — що саме не так. Для iCloud Drive цей повсюдний контейнер потрібно додатково оголосити публічним та призначити йому імʼя - після чого… ні, все ще нічого не відбувається.

З вище вказаної статті збагнув, що щоб файли зʼявилися у Drive, вони повинні бути в піддиректорії Documents, а не прямо в контейнері. Тоді… ще потрібно було збільшити версію збірки, і ось нарешті тека з файлом зʼявилася і в Files на айфоні, і невдовзі на макбуці теж. 😮‍💨

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


31.01.2025

Ping - сайт

Як і планував, нова версія вже у TestFlight. Також нарешті дістався до створення сторінки для застосунку: ping.leonid.codes. Поки сторінка майже порожня, але на з цим продуктом є стимул її розвивати.

Саму сторінку я скопіював із останньої такої задачі. Взагалі гарно б було натренуватись робити мінісайти для (менших) проєктів, бо інакше вони часто залишаються без всякого маркетингу.

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

Про час: відзначу, що розгортування на Cloudflare Pages зайняло лише пару хвилин. Це з урахуванням того, що мій домен вже під Cloudflare. Бо завдяки цьому він сам робить всі налаштування, достатньо підʼєднати репозиторій та вказати піддомен.


30.01.2025

Завтра випускаю нову збірку трекера. Зміни доторкнулися головної частини — алгоритму пропонування тегів. Частково повернув ту логіку, що була до міграції на SQLite, а також додав нові круті речі.

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

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

Я почав з того, що витягував суміжні теги до кожного обраного, та поєднував за частотою. Але це дає дивні результати; умовно якщо обрати теги desktop, coding, то в пропозиціях будуть gaming як суміжний із desktop та laptop (!) із coding.

Тому треба було знаходити теги, суміжні до всіх обраних відразу. Що не так легко, бо тегування зберігаються в табличці tagging - знайти суміжний до одного тегу це один join, але якщо до декількох — не плодити ж джойни? Винайшов чудове рішення, що знаходить всі пінги, де зустрічалися всі обрані теги:

SELECT ts, count(*) cnt
  FROM tagging
  WHERE tagId IN (1,2,3)
  GROUP BY ts
  HAVING cnt = 3

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

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