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

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

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

27.11.2024

Keep-Alive та як воно працює

В мене тут намалювалася помилка, здається, через Keep-Alive. А саме, клієнт внутрішнього HTTP API в Go час від часу віддає помилку io.EOF. Не найкраща помилка, якщо чесно, бо ніяк не пояснює, що відбулося. А відбулося ніби те, що сервер час від часу закриває підключення.

Keep-Alive (“залиш живим”) можна побачити у двох контекстах: TCP-сокетах та HTTP. Давайте спочатку про HTTP. Тут є заголовок Keep-Alive. Такий заголовок з боку клієнта каже “я б хотів використати це підключення більш ніж для одного запиту”. А з боку сервера, “так, я на це згодний”. На цьому участь HTTP в Keep-Alive закінчується. Ба більше, в HTTP/2 та далі цей заголовок взагалі заборонений! Бо в HTTP/2 сталість підключення є нормою. А в HTTP/3 втрачає сенс, оскільки він побудований на UDP, де передача даних відбувається взагалі без підключень.

Але що треба запамʼятати, то Keep-Alive на шарі HTTP тільки інформує, а справжня реалізація Keep-Alive відбувається на шарі TCP. Та для клієнтів API, гадаю, це все ще актуально, бо HTTP/3 з UDP не має сенсу. (А ви як думаєте?)

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

Для того в налаштуваннях підключення (в Go це в net.Dialer) є параметри Keepalive, які й вказують, чи надсилати такі пакети, та як часто. Я знав, що базовий клієнт Go вже підтримує стале підключення, а тепер я ще дізнався, що він також надсилає Keepalive, але надто рідко для цього серверу API. Отак.


26.11.2024

Несподівані речі з верстки для друку

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

Розмір сторінки здивував, бо зазначений був як A5 (148x210 мм), але на практиці виявився 145x200. Але насправді треба зробити 149x204, щоб врахувати технічні поля на обрізку. А коли обкладинку робити, то ще цікавіше, бо там ще буде корінець, а товщина корінця залежить від кількості сторінок.

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

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

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


25.11.2024

Трасування зображень

Розкажу трохи більше про техніку трасування, яку я використовував вчора для карти. Трасування, або обведення — це спосіб перетворити растрове зображення в стилізоване векторне.

Взагалі нічого тут складного немає. Нам буде потрібний графічний редактор з шарами — Фотошоп, Pixelmator, GIMP, Paint.NET. Ставимо оригінал зображення на фон та починаємо обводити зверху всі потрібні деталі. Можна векторними інструментами (лініями, багатокутниками тощо), але можна й растровими — головне, щоб зміни були в окремих від фона шарах. Коли буде готово, вимикаємо фоновий шар та отримуємо чисте обведення. Звісно, якщо щось не подобається, можна завжди знову увімкнути фон та скоректувати.

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

Також трасують супутникові знімки, щоб створити векторну мапу. Наприклад. в редакторі OpenStreetMap кожен може спробувати (тільки, будь ласка, відповідально та обережно).

Ще я нещодавно, щоб зробити детальний план квартири (порожньої), сфотографував стіни, виміряв загальні розміри, а потім, переконавшись у дотриманні масштабу, переніс елементи зі світлини на план.

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


24.11.2024

Ремастеринг книжки

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

Книжку мені дали в вигляді сканів тих самих розібраних аркушів. По-перше, потрібно було скласти з них сторінки в правильному порядку (бо на одному аркуші друкувалися сторінки 3 та 157, 4 та 156 тощо.) На то я зробив скрипт на Ruby із застосуванням ImageMagick:

  left_index = index
  right_index = 163 - index
  angle = index.odd? ? 90 : -90
  `magick '#{page}' -crop 100%x50% +repage output_%d.jpg`
  `magick output_0.jpg -rotate #{angle} output_0.jpg`
  `magick output_1.jpg -rotate #{angle} output_1.jpg`
  FileUtils.move('output_0.jpg',  "#{'%03d' % left_index}.jpg")
  FileUtils.move('output_1.jpg',  "#{'%03d' % right_index}.jpg")

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

Ще було питання, в якому редакторі робити чистку. Я почав у Google Docs, бо думав що це зараз найкращий вибір, але ні — зокрема там не вистачило банальної можливості вказати різні поля для лівої та правої сторінки. Тому більшість роботи виконав в Apple Pages. Не знаю, хто їм окрім мене користується, але насправді це повноцінний та потужний текстовий редактор. (Так, я знаю, що, мабуть, ідеальним вибором був би MS Word, але його в мене немає. Чи може Ворд зараз теж є онлайн?)

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


23.11.2024

Місце для Offline First

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

Розробка вебзастосунків псує людину: ти забуваєш, що можна робити застосунки, які взагалі не мають серверної частини. Але застосунків без серверної частини повно, та ми всі користуємося ними щодня. Навіть веброзробники з вебтехнологіями вільні взяти рішення на кшталт Electron та робити собі SPA без серверної частини. Також рішення з локальною базою буде простіше, ніж всяке задумане під синхронізацію, тож навіть Offline First варто брати тільки якщо вам дійсно потрібно щось синхронізувати з хмарою. Тому якщо хочете зробити щось однокористувацьке, вистачить SQLite, а то й JSON. Ваша продуктивність вам віддячить.

Головна проблема такого рішення — як і його головна сила — що дані залишаються на одному пристрої. Для веброзробника це теж ненормальний стан речей, але це може виявитися зовсім не проблемою — дивлячись на застосунок. Он, Apple Health досі не має ніякої синхронізації. Також, обмін файлами на цей час — розвʼязана задача, тому може нам ніякий не Offline First потрібний, а файли в спільній теці чи хмарному диску.

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

А ось коли ми хочемо, щоб дані зберігалися в першу чергу локально, але зміни з одного пристрою зʼявлялися миттєво на іншому — тут вже доведеться шукати рішення “Offline First” - Firebase, Realm, CloudKit - та готуватися витрачати на нього зусилля: навіть коли ніби синхронізація є вбудованою, безплатною вона не буде.


22.11.2024

Теги в каналі — тепер й на сайті

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

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

Хмару тегів в наш час легко зробити з CSS, як ось тут пояснюють. Тільки там кожному тегу призначається клас розміру, а я його обчислюю на ходу з кількості постів. Ще залишається нормалізувати її відносно максимальної, що звучить просто, але в шаблонах Go арифметики немає, а є тільки виклики функцій, та ще й в Польській нотації: font-size:{{add 1 (mul $pageCount $multiplier)}}em.

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


21.11.2024

Розширена інформація після невдалого тесту в Go

Захотілося, щоб після невдалого інтеграційного тесту я бачив журнал сервісів з Docker Compose. Бо в мене на CI вже й так журнал друкується наприкінці збірки, але за тим загальним журналом складно знайти потрібне місце.

Перше питання стало — як взагалі в Go викликати код після тесту? Рішення в стандартній бібліотеці не знайшов, але ті тести виконуються пакетом testify/suite, а в нього є метод TearDownTest(), проміж інших. Тож тут проблем немає.

…Далі, як дізнатися, що тест був невдалий? Я очікував якогось аргументу в колбеці, але ні. Насправді той обʼєкт *testing.T, навколо якого обертаються тести, має метод t.Failed(). (Взагалі раджу його вивчити детальніше, бо там багато чого цікавого, наприклад, можливість отримати тимчасову директорію для одного тесту - t.TempDir().) А в testify/suite, до речі, через s.T() отримуємо той самий обʼєкт.

(PS: а ще в testify/suite знайшов suite.HandleStats - якщо його оголосити, то цей метод буде викликаний наприкінці всього пакету зі статистикою про час виконання та результати тестів.)

Окей, тепер, як отримати логи? Спробував через програмний клієнт Docker… надто низькорівневий, бо він повертає для логи конкретного контейнера, та ще й потоком. А мені потрібні зведені логи всіх контейнерів в проєкті. Тому просто викликаю консольну команду docker compose. Для того є зручний високорівневий тип exec.Cmd. Йому можна прямо сказати “спрямуй вивід команди в мій вивід” і все, готово.


20.11.2024

Як я робив 3D-рушій

🎈🐴🧑‍🚀 В далекому 2004 році я писав той самий “другий проєкт”, про який тут згадували в коментарях: а саме, ідеальний рушій для 3D-ігор у вакуумі. На той час я вже трохи наробив ігор на Паскалі, але треба було кудись рости… Тому новий рушій був на С++. Тоді мене в C++ закохала можливість перевантаження операторів та, порівняно з C, можливість ООП (яка й в Паскалі вже була знайома.)

💯 Почав з математики. (Та, як можна уявити, я все хотів писати самостійно.) Лінійна алгебра вона наче не дуже складна, але в реалізації легко заплутатись, бо всі ті формули з підручників потрібно було переписати. (А про юніт-тести я тоді ще й не знав.) Вектори, матриці то ще добре, а от від кватерніонів досі жахаюсь. Кватерніонами робляться оберти в 3D-просторі - про ті оберти цілий айсберг нюансів.

🎥 На базовій математиці будується модель простору. Бо щоб щось показати на екрані, для початку треба визначитись, де камера, та як відносно неї розташовані предмети. Це все купа матриць; матрицями задаються перетворення систем координат. За основу я взяв OpenGL - низькорівневий графічний API. Так от йому подавай матрицю камери та світові координати точок… суцільна математика.

🚛 А далі — що показувати? Обʼєкти та їхні текстури потрібно завантажувати з файлів. Тому я, звісно, сам написав завантажувач з BMP (найпростіший формат графічних файлів, до речі!) та моделей Milkshape 3D (оце вебдизайн!)

🏗️ Зверху того рушію потрібна модель виконання (тобто петля з таймером, кадри, таке інше), інтерфейс (вікна, HUD, шрифти), механізми керування. І це ще ми не дійшли до головного — до гри!

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

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


19.11.2024

Користуйтеся посиланнями замість буквальних імен

З Terraform метрику AWS CloudWatch видалив, а тривога за метрикою залишилась. Та ще й негайно стривожилася. Та підняла інцидент в PagerDuty. (Гарно хоч, що не посеред ночі.) Цього можна було уникнути, якби ресурс тривоги посилався на ресурс метрики:

resource "aws_cloudwatch_metric_alarm" "foo" {
  # ✅
  alarm_name = aws_cloudwatch_log_metric_filter.foo.name
  # ❌
  alarm_name = "foo"
}

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

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

В динамічних мовах гірше, бо там менше різниці між рядками та ідентифікаторами. Он, в Ruby багато всього працює через :символи. Все одно, коли символ використовується всюди в проєкті, краще покласти його в якусь константу чи метод. (Всім знайомий приклад: Rails.env.development? замість Rails.env == "development")

А ще в Ruby є погана тенденція конструювати ті імена на ходу, як-от "#{attribute}_exists?". Та й в Terraform такого багато. Тоді взагалі ходи шукай ті назви.


18.11.2024

Варто починати дослідження з написання тесту

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

Для мене в цьому більше сенсу, ніж в “test-driven development”, оскільки я не люблю писати тести на порожньо. Ще я не люблю вручну відтворювати обставини — особливо на неповноцінній локальній версії — а тест дозволяє це автоматизувати.

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

Тому в мене час від часу зʼявляються ПРи, в яких є тільки пара тестів, та ще може коментарі по коду. Написав, перевірив, код виявився вірним, а тест — навіщо його викидати?

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

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