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

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

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

17.11.2025

З JSON до SQLiteData.swift

Вже понад рік я користуюся власним застосунком для GTD. Це, безумовно, успіх — в мене таке може вперше, щоб система так довго не розвалювалася. Але є проблема: він зберігає дані в єдиному файлі JSON.

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

Отже, на вихідних взявся за переписування всієї моделі даних на повноцінну базу даних. Минулого року я залучив GRDB, а цього разу хотілося випробувати SQLiteData. Ця бібліотека, власне, є надбудовою для GRDB, а відрізняє її можливість синхронізувати базу за CloudKit - тобто забриніла можливість зробити й версію для iOS, що нарешті зробило б цей проєкт продуктом.

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

Головних перепон тут було дві. Перша — вони використовують багато макросів (для побудови SQL з Swift.) Через них компілятор часто не міг дати нормальну помилку та доводилося її шукати. Друга — з мало відомою бібліотекою болюче не вистачає документації та взагалі пояснень. Та ШІ в таких умовах теж мало допоможе!

Тому доводилося читати код самої бібліотеки та шукати там відповіді навіть на прості нібито питання. Як-от, запит не дозволяє написати $0.completedAt < currentDate, якщо стовпчик completedAt може бути нульовим. (Цей вираз, звісно, не виконується буквально, а перекладається в SQL тими самими макросами.) Та я поки знайшов лише такий дикий синтаксис, щоб це обійти: $0.completedAt.map { $0 < currentDate } ?? false.

Так чи інакше, застосунок принаймні скомпілювався та майже повністю працює.


14.11.2025

Трошки з адміністрування OpenSearch

…Передісторія: так склалося що в одного з індексів OpenSearch цілих 100 шардів на приблизно 5 Гб даних. Стандартні рекомендації — це шарди розміром у 10-50 Гб, тобто десь у 1000 разів більші. Ну буває.

Десь місяців із 9 тому я писав, що це ні на що не впливає, але виявляється, все залежить від навантаження. Отже, стала задача звести кількість шардів до розумної… ну, наприклад, 2.

Робити це “наживу” OpenSearch не вміє. Тобто нам доведеться замінити індекс. (До речі, саме тому в OpenSearch також є псевдоніми індексів (alias) - щоб легко підміняти індекси або цілі набори.)

Є операція reindex. Вона бере кожний документ та копіює його в новий індекс. Триває все це кілька годин. Я вже писав, що все в OpenSearch, що працює на рівні документів — дуже повільне. Під час цього, якщо старий індекс відкритий до запису, накопичуються ще зміни, які наздоганяти мені не захотілося.

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

Перепоною стало те, що для виконання shrink всі шарди індексу повинні бути на одному вузлі. Для того є інструкція прямо в документації вище. Але на практиці в нас OpenSearch починав цей переніс та не закінчував. Виявилося, що є обмеження на кількість шардів на вузол.

Причому (!) в AWS OpenSearch обмеження це — як ми визначили — залежить не від розміру вузла. Та точно не від налаштувань — такі речі AWS не дає змінити, щоб ми не “наробили дурниць”. Дякуємо!

Ні — насправді AWS OpenSearch прагне зберегти баланс в кластері, а саме рівний розподіл шардів між вузлами. Ну й добре… створили ще один, порожній, індекс на 100 шардів. Після чого перенесення справжніх шардів на один вузол завершилося успіхом. Ну а далі й shrink відбувся без подальших сюрпризів. Роботу зроблено.

Це я ще не торкнувся того, наскільки безпорадним є UI OpenSearch Dashboards, в якому одне світле місце - Dev Console, тобто можливість надсилати до кластера команди HTTP API.


13.11.2025

Sintra тепер англійською

Крайніми вихідними тихенько випустили англійський переклад нашого застосунку для ведення бюджету Sintra.

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

Може здатися, що додати третю мову там, де є дві — легко. Так, це дійсно незрівнянно простіше, ніж додавати другу мову. Але окрім перекладу, довелося також виконати локалізацію. Наприклад: впровадити підтримку тижня, що починається з неділі. Чи форматування сум, де знак валюти йде спочатку: $100.

Величезною підпорою був ШІ, він і зробив чернетку перекладу, і допоміг Саші з локалізацією. Я практично тільки переглядав зміни… та займався окремо застосунком на React Native. Бо в застосунків для iOS своя система локалізації. А ще є App Store Connect, а там кільканадцять сторінок, де теж можуть ховатися не перекладені рядки. От виявив, що ми забули про переклад посилання на політику приватності — і таке є!

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

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


12.11.2025

Рухоме середнє

Є в мене утиліта split_tests, вона вміє розбивати пакет тестів на частини рівної тривалості. Для того тривалість кожного файлу передається в широко відомому форматі JUnit XML.

Та все б гарно, але тести не завжди мають ідеально рівну тривалість. (Особливо інтеграційні.) Та оскільки ми спираємося на результати останнього запуску, де-факто розбіжність між частинами складає до 20%. Знаєте, як дратує покладатися на емпіричні виміри, та все одно отримувати таку розбіжність?

Рішення, яке мені напрошується, це обчислювати усереднені тривалості. Та для того зовсім не обовʼязково зберігати статистику кожного запуску за все життя. Нам знадобиться формула рухомого середнього:

T нова = 0.9 * T стара + 0.1 * T поточна

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

Отже, тепер у split_tests є новий режим:

split_tests -junit-update=old/*.xml -junit-new=new/*.xml -junit-out=updated.xml

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


11.11.2025

Сила LLM - в поєднанні понять

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

Наприклад. Сьогодні оновив Miniflux на fly.io та несподівано він почав викидати помилку store: unable to get entries: pq: time zone "Europe/Kyiv" not recognized. Як завжди в таких ситуаціях, немає в мене часу ретельно перевіряти, в чому тут справа. Починаю шукати в Perplexity.

Перший крок лише підтверджує мою інтуїцію: помилка в тому, що PostgreSQL чомусь застряг зі старою назвою поясу Europe/Kiev. Тут же ж через Perplexity перевіряю: так, моя версія PG дійсно має застарілу базу поясів.

А далі починається поєднання понять. Питаю: як заапгрейдити PG на fly.io? Виявляється, що тільки через створення нового серверу з ручним перенесенням даних — на що в мене точно не вистачить терпіння. Ну, значить, доведеться поки повернути стару назву.

Але як то зробити, якщо застосунок просто не працює — ніяк? Значить, залазимо через базу. А як то зробити на fly.io? Виявилось, є проста команда: fly postgres connect. Складаємо запит до бази та команду, туди-сюди, пояс замінено, застосунок працює. Кризу якщо не пройдено, то принаймні відкладено. (Можливо, перейду на повністю локальне рішення для RSS.)

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

Ось весь діалог, якщо кому цікаво.


10.11.2025

Time Ticker - а коли це буде в інших країнах?

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

Це нікому не зручно, тому одного дня я захотів задизайніти кращий формат, та дійшов такого:

12.11.25 🇺🇦🇷🇴 12:00 𑈺𑈺 🇵🇱 11:00 𑈺𑈺 🇵🇹 10:00

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

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

Минулого тижня згадав про цей скрипт… та перетворив у вебзастосунок time-ticker.leonid.codes. Звісно, в цьому мені допоміг Cursor, та це був, напевно, найповніший “вайбкодінг” в моєму житті. Хоча, я принаймні перевіряв код та давав корекції.

Початкова інструкція була на кшталт “ось скрипт — зроби з нього вебверсію з використанням Svelte”. Та результат мене дуже задовольнив! Звісно, можна сказати, що “його буде складно доробляти”. Але з одного боку, ШІ мені вже доробив початковий результат, бо в ньому не було збереження стану, рядка пошуку та визначення домашнього часового поясу. Також на початку ШІ згенерував мені список поясів, та потім ми його замінили на @vvo/tzdb. А з іншого боку, куди там його далі доробляти?

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


07.11.2025

RESTful нічого не значить

Аж три роки тому я писав, що REST мені не подобається. Але з подальшим досвідом зрозумів, що ця назва повністю втратила зміст.

Річ у тому, що справжнє означення REST ми загубили. Наприклад, ось стаття автору цього терміну; зміст статті взагалі важко прикласти до сучасних API. З іншого боку, ось відносно сучасна стаття з блогу StackOverflow, яка розповідає про щось абсолютно інше.

Практично кожний сучасний API використовує дієслова HTTP та ресурси-іменники. І це чудово, насправді! Я не думаю, що хтось повинен відповідати стандарту REST, справжньому чи уявному. Моя теза в тому, що цей термін став порожнім, та використовується лише для красоти.

І це в гарному випадку, що люди не сприймають епітет “RESTful” та пропускають мимо. А можна ж натрапити на педанта, який серйозно присічеться до вашого API та скаже, що там все неправильно, а ваша команда — дилетанти.

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


06.11.2025

Навіщо потрібний S3?

Натрапив сьогодні на питання: навіщо s3? та в чому його плюси? та чи можна написати свій?. Люблю такі питання, бо хоч воно здається наївним, але я не сумніваюся, що дехто карго-культить (що таке карго-культ?) S3 та дарма не замислюється. Отже.

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

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

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

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

S3 став стандартом де-факто для збереження “файлів”. Настільки, що багато (якщо не більшість) сховищ пропонують сумісний з S3 API: Cloudflare, Backblaze, Hetzner і так далі. Тобто я хочу сказати, що якщо вам не подобається Amazon, можна знайти альтернативу та зберегти зрозумілий режим доступу.

Або навіть захостити власний: MinIO, Localstack.


05.11.2025

Як шукати причину збільшення CPU

Є у вас сервіс чи база, та в нього раптом виросло споживання процесора. Куди та як дивитися?

По-перше, тобі потрібні метрики. Та не тільки на цей момент, а історичні. Без метрик ти біжиш в темряві: це весело рівно до першої ями.

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

Зростання бувають двох типів. Або планомірні, завдяки збільшенню попиту. Або — коли відбулося несподіване. Скоріше за все, ми дивимося на несподіване зростання — планомірне просто не так часто трапляється. Та з можливих причин я б ставив на нову фічу з необачним споживанням. Щоб це підтвердити, шукаємо smoking gun, тобто прямий доказ.

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

Потім можна перейти в інструмент APM - New Relic, Sentry тощо — та перевірити, яка саме активність корелює. Тут-то ми напевно і побачимо нашу проблемну фічу.

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


04.11.2025

Смачні страви польської кухні

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

А ще є пирОги, запіканки, пачки, крижана зубрівка…. Та макарон з трускавкою. 🍓