Стендап Сьогодні
Що я зробив, що я хочу зробити, і що це все значить.
Повсякденні здобутки в форматі стендапу.
Детальніше в статті
Підписатись на RSS
📢
Канал в Telegram @stendap_sogodni
🦣
@stendap_sogodni@shevtsov.me в Федиверсі
30.04.2023
Тривимірна карта нарешті працює
Вийшло зробити ті ілюстрації, які хотів — наближення з глобуса до локальної карти, та демонстрація треку на тривимірній карті. Звісно, ще є над чим працювати, але база є.
Щодо глобуса: розібрався з формулою для меркаторівських координат і все запрацювало. Допоміг аркуш паперу та ця сторінка з вікі OSM. Наступний етап — плавне наближення до точки. Окрім інтерполяції камери, що досить просто, треба підміняти текстуру на більш деталізовану. Для цього треба розвʼязати, які текстурні координати будуть у деталізованої текстури — над цим теж довелося помізкувати. (Я заздалегідь завантажив кожний рівень деталізації для даної точки на карті. Для цього є ще скрипт на Ruby, що вміє звантажити плитки карти та зшити їх разом за допомогою ImageMagick.)
Щодо локальної тривимірної карти: для цього починаю з локальної карти (такої самої, як останній рівень деталізації глобуса), та додаю до неї Terrain RGB. З даних висоти легко побудувати квадратну поверхню; наклеїти на неї локальну текстуру карти тривіально, бо вона збігається один в один.
Далі, трек; він в мене вже був у форматі масиву координат (які я власноруч записав в поході). Але при переході до локальної карти географічні координати втрачають дійсність, та заміняються системою координат плитки локальної карти, яку ми взяли за основу. Тому тепер довелося розписати формулу, по якій від меркаторівських координат плитки можна перейти назад до граничних значень широти та довготи для неї. А коли є граничні значення, то привʼязати до них трек — задача простого перенесення та масштабування. Надзвичайно приємно, коли трек та карта, взяті з абсолютно різних джерел, сходяться в єдине зображення.
На наступний раз — збагачення карти маркерами, покращення стилю треку, та обʼєднання глобуса та тривимірної карти в єдину сцену.
29.04.2023
Роблю глобус на three.js
Для постів про подорожі намагаюся зробити красиву тривимірну карту. Причому через мої дикі вимоги “просто” карта на кшталт Google Maps або Leaflet не подобається — роблю свою. Ідея в тому, що замість повноцінної карти це буде обмежена та зрежисована анімація.
Вирішив робити на three.js - бібліотеці тривимірної графіки на JavaScript. На сучасних пристроях вона повністю прискорена технологією WebGL та графічним процесором, тому працює все дуже добре і можна робити речі набагато складніше, ніж глобус.
Перший крок — власне, намалювати глобус. Для цього знайшов класну статтю; код з неї запрацював майже без корекції на сучасну версію three.js. Потім достатньо легко зʼясував, як перекладати географічні координати на “графічні” (для цього дуже корисним є клас Spherical) та орієнтувати глобус на той бік, що мені потрібно. Навіть анімувати переліт з однієї точки в іншу.
Далі задача була така, щоб камера наближувалась до об’єкта, а карта збільшувала масштаб в міру наближення. Як всі нормальні карти роблять — але в мене ідея завантажувати потрібні мені шари карти заздалегідь, щоб це було гладенько. Власне, зображення плиток можна взяти багато де, наприклад, на MapTiler. Залишається накласти їх на глобус. І тут все пішло не так…
Якщо дуже коротко, то стандартна проєкція онлайн-карт — Меркатор — не сходиться зі стандартним способом накладання текстури на сферу. Меркаторівська проєкція витягується біля полюсів. Тому просто накласти текстуру на глобус — як це зробив автор статті-прикладу — можна тільки якщо карта підготована “в форматі текстури”. Щоб відображати карти, треба обчислювати текстурні координати для всіх точок на сфері. Тут вже починається серйозна тригонометрія, на якій я поки застряг — може тому, що вже друга година ночі.
А ще, сфера Three.js хороша, як сфера, але не як глобус. ЇЇ розподіл на полігони більш нагадує волейбольний мʼяч, ніж глобус. Тому я й генерацію глобуса переписав. Алгоритм досить простий — біжиш по широті та довготі, генеруєш трикутники парами. А ось тригонометрія складна. На ній поки й залишаю.
28.04.2023
Про наше локальне Docker-хазяйство
Хочу ще розказати про те, як зробив локальну емуляцію складної інфраструктури з продакшна — для інтеграційних тестів або ж просто для розробки.
Для того потрібний Docker Compose. Це доповнення до Docker, яке оркеструє багато контейнерів у вигляді сталої схеми. За допомогою Docker Compose можна всі контейнери відразу зібрати, запустити, звʼязати між собою, подивитися журнал, видалити, і ще багато всього.
(Docker Compose мій другий вибір подібного рішення; першим буде Foreman - утиліта, що може запускати декілька процесів з одного терміналу та збирати їх вивід в один. Але Foreman рішення обмежене — наприклад, з ним не вдасться підмінити DNS, звертатись до сервісів за хостом або IP, або запускати на macOS програми, що є тільки під Linux.)
Все це можна робити з Docker Compose. Наприклад, у нас все середовище складає на цей час 19 контейнерів. Частина з них — власні сервіси, але є й загальнодоступні — наприклад, бази даних, або dnsmasq. Вони всі розташовані в віртуальній мережі. Для деяких функцій потрібно звертатись до фіксованого IP, то це легко зробити.
Конфігураційний формат Docker Compose гнучкий. Оскільки це YAML, в ньому можна вживати якірі, тобто виносити частини конфігурації, що повторюються. Можна навіть посилатися на інший файл. Так можна суттєво знизити кількість копіпасти.
Щоб ще узагальнити конфігурацію, можна використати змінні оточення. Наприклад, завдяки змінним, можна легко зробити дві копії інфраструктури — одну для тестів, одну для ручного використання. Або виправити мілкі незручності, які заважають запускати конфігурацію як на macOS, так і на Linux (для CI) - а такі незручності трапляються.
А щоб сховати довжелезні команди docker-compose з купою параметрів, я написав Rakefile. Rake - то аналог класичного Make, написаний на Ruby, та я нерідко вживаю його за межами проєктів на Ruby, щоб спростити скрипти. Адже системні скрипти — це вроджений талант Ruby.
27.04.2023
Зручна маршрутизація для Docker Compose
В продовження вчорашньої теми про створення повноцінного тестового середовища - стала задача відтворити маршрутизацію, яку в продакшні робить AWS Load Balancer. В залежності від шляху в URL він направляє запити на один сервіс, або на інший. Оскільки AWS ALB ми, звісно, локально не запустимо, то поки що цей фрагмент інфраструктури не відповідав продакшну, та тести його обминали.
Що робити? Вірний вибір, безумовно, nginx. Якби не можливості ALB, то він би в нас і в продакшні маршрутизував та балансував. Ось тільки налаштовувати nginx - окрема праця, як зараз, так і потім в підтримці. Тому пошукав готові рішення вищого рівня. Знайшов nginx-proxy. Це як раз маршрутизатор для середовища Docker, який конфігурується напівавтоматично на основі змінних оточення, назначених контейнерах. Вміє він й назначати віртуальні хости, й робити TLS шифрування (разом, наприклад, з mkcert).
Проте все, що було потрібно мені — це зібрати два сервіси під єдиною адресою, та до того ж на localhost за визначеним портом. Для цього корисні дві опції nginx-proxy:
-
DEFAULT_HOSTназначає той хост, який буде використовуватись, коли ніякий не підійшов; на локалхості це саме те, що треба. Сюди пишу аби що. Але головне, що він має збігатись з налаштуваннямVIRTUAL_HOSTна моїх сервісах. -
VIRTUAL_PATHдозволяє як раз маршрутизувати за шляхом; причому, по-перше, шлях використовується як префікс, а по-друге, один з сервісів може бути за замовчуванням з порожнім шляхом.
Далі, nginx-proxy встановлюється на потрібний порт, сервіси отримують налаштування VIRTUAL_HOST, VIRTUAL_PATH та можливо VIRTUAL_PORT, запускається docker-compose, і все працює само по собі! Дуже зручне рішення, краще й уявити не міг.
26.04.2023
Неочевидні переваги інтеграційних тестів
В підсумках року я колись писав, що в нас дуже крутий пакет інтеграційних тестів. Сьогодні його ще розширив, покрив додаткову підсистему. До речі, раніше вона була не покрита тому, що сиділа в іншому репозиторії, а це суттєво все ускладнює. Згодом підсистема перепливла в головну репу, та тестувати її стало простіше. Втім, розповідь не про переваги підходу “монорепо”, а про переваги тестів.
Інтеграційні тести мають покривати якнайбільшу частину вашої системи. Якщо це додаток та база, то це легко, але зі створенням мікросервісів та впровадженням всіляких білінгів та черг задача ускладнюється. Проте мета залишається — інтеграційні тести мають вміти відтворити програмно будь-які дії користувача.
Тепер, про переваги. Ясно, що мати можливість перевірити всю систему у звʼязці — дуже корисно та допомагає виявити найбільш приховані баги. Окрім того, інтеграційні тести допомагають в документації. Я не раз відкривав тести, щоб нагадати собі ту чи іншу поведінку системи. Та для того їх навіть не потрібно запускати — гарні тести описують всі очікування.
Також наявність тестової запряжки дозволяє моделювати поведінку через програмування. Це корисно не тільки при пошуці багів (хоча це в першу чергу!), а й для дослідження. Чим складніша система, тим важче зрозуміти, що вона зробить в той чи іншій ситуації. Тоді замість того, щоб відтворювати вручну складну або навіть неможливу ситуацію, можна просто написати тест і подивитись, що станеться. Так я теж постійно роблю.
Тому дуже радий, що ще один аспект продукту тепер перетворився з загадкового “треба буде перевірити на продакшені” в зрозумілий “ось як воно все працює”.
25.04.2023
Наступність схеми даних
Якщо у вас є дані, що зберігаються поза процесом програми, та переживають її перезапуск, то при зміні схеми цих даних необхідно подумати про наступність, тобто здатність нового коду читати не тільки нову, але й стару схему. Більш за те, треба також передбачити зворотне — тобто старий код має вміти читати нову схему. Бо інакше як ти зможеш відкотити додаток на попередню версію? Спалювати цей міст необачно — виправлення помилок без можливості відкату набуває особливої пікантності.
В реляційних базах даних з цим трохи простіше, та підходи зрілі (принаймні, в Ruby on Rails це так.) Але дані криються не тільки в базі. Наприклад, в будь-якій черзі теж треба передбачати наступність. Власне, це мене сьогодні й вкусило. Та ще й не на Ruby, а на Go. Тут все ускладнюється типізацією — бо якщо в Ruby можна сподіватись що так-сяк спрацює, то в Go несумісний тип не проходив через json.Unmarshal, та на цьому все завершувалось.
Щоб не мати проблем, потрібно спочатку в одній версій коду додавати поле, але тільки в наступній з ним щось робити. Так само з видаленням полів: спочатку припинити використання та зробити omitempty, а потім вже видаляти з типу. Особливо цікаво це коли треба поміняти тип поля, та я б цього взагалі не радив робити без перейменування (тобто фактично створення нового поля та видалення старого.)
Але якщо вже так сталося, що схема поламана, що робити тоді? Тоді “гарячим” виправленням треба ввести проміжний код, який буде підтримувати обидві схеми. Ну, наприклад, як саме просте рішення, можна пробувати розкодувати в новий тип, а якщо не вдасться — то в старий та адаптувати. Або розкодувати щось в інтерфейс та потім дивитись на тип. (До речі. цікавий момент: якщо в Go розкодовувати чисельне значення з JSON в interface{}, отримаєш тип float64, бо технічно в JSON немає різниці між цілими та дробними числами.)
Раджу обмежити виправлення вхідною частиною програми, а далі використати тільки чистий новий тип — як всередині бізнес-логіки, так і на запис. Це локалізує “нечіткий” код та уникне помилок в головній частині програми.
24.04.2023
Розширення та знущання з Ransack
Сьогодні половину дня намагався зробити новий фільтр для Ransack. То є така бібліотека для Ruby on Rails, щоб будувати запити до бази з користувацьких форм. Використовується для всілякого пошуку; в тому числі пошук в ActiveAdmin використовує саме його.
Власне, шляхів розширення в Ransack вистачає, але майже всі вони мають відповідати закладеній абстракції: до деякого атрибута прикладається деякий оператор, можливо — зі значенням. Наприклад: {"name_eq": "Grigory"}. Оператор має бути виражений в Arel, тому якщо це не вдається, вони пропонують манкіпатчити Arel, щоб доповнити його власними предикатами.
Але в мене задача вийшла ще складніше. Мені треба було поміняти поведінку оператора equals, але тільки для одного поля. Тобто зовнішній інтерфейс залишити без змін. Гарного способу це зробити я не знайшов; негарний складався з того, що перед запитом я підміняю оператор на інший, custom_field_eq. Але як можна було помітити в попередньому параграфі, запит склеює поле та оператор, тобто остаточно фільтр виглядає як {"custom_field_custom_field_eq": "Grigory"}.
Потім, висловити цей оператор у формі оператора Arel теж вийшло непросто, оскільки перевіряється значення алгебраїчного виразу. Тоді знайшов трохи прихований, але дуже зручний вихід — можна як фільтр використати скоуп. Залишається в моделі оголосити скоуп custom_field_custom_field_eq, та робити в ньому що завгодно.
Можна подумати, що якщо так, то не треба було додавати оператора. Але ні — здається. скоупи мають нижчий пріоритет, тому на фільтр custom_field_eq викликатиметься стандартна логіка, а не скоуп.
23.04.2023
Декілька можливостей сучасного CSS
Погода сьогодні хороша, тож прогресу не так багато. Ще декілька штук з CSS, про які я дізнався за останні дні:
-
background-attachment: fixed- привʼязує фон до екрана, а не до елементу, тобто якposition: fixed, тільки для фону. Проте фон застосовується тільки для того елементу, де він вказаний. Дозволяє відтворити ефект паралакса на чистому CSS. -
scroll-snap-align- робить прокрутку “липкою”, тобто спонукає її зупинятись на зазначених елементах. Якщо на сторінці є візуально окреслені блоки, то така прокрутка працюватиме акуратніше, чіткіше. Можна навіть примусити зупинятись тільки на заданих точках (scroll-snap-type: mandatory), що корисно, наприклад, для презентацій. -
@media (max-aspect-ratio: 1)- медіазапит на відношення сторін екрана. Так можна робити адаптивний дизайн для вертикальних пристроїв (вони ще відомі як “смартфони”). Я раніше робив тільки за шириною, що в принципі теж надійно, але менш явно. Тим більше, що маю потребу розміщати елементи по-іншому й для великих вертикальних екранів — планшетів, наприклад. -
gap- при верстці наflex(абоgrid, дякую, Андрію) більш не треба робити хаків на кшталт “робити відступ справа всім елементам, окрім останнього”. Можна просто задати властивістьgap- відстань між елементами — та забути про дубльовані відступи. -
vw, vh- ці одиниці вимірювання привʼязані до розміру вікна. Тому в них можна вказувати розміри, що відповідно адаптуються. Є щеvmaxтаvmin- максимум чи мінімум з обох.Наприклад, зараз в мене в блозі розмір шрифту на великому екрані125%, на екрані планшета110%, а на телефоні100%; якби я переробив це наvmax, можна було задати розмір один раз, наприклад,2vmax. Ще ці одиниці зручні та інтуїтивні також для повноекранної верстки.
22.04.2023
Прогрес по фотозвіту про походи
Продовжую роботу над своїм “журнальним форматом” звітів про подорожі на основі Hugo. Прогрес величезний, але показувати поки рано. Залишилось, головним чином, адаптація під маленькі екрани, та редакторська робота над самим постом.
Щоб додати новий різновид постів, в Hugo є типи контенту. Для даної задачі головне, що в кожного типу — свої шаблони. Подорожі будуть представлені в широкоформатному вигляді, без сайдбару, який є у звичайних постів — тож без нового шаблону не обійтися.
Далі, ідея в тому, що зміст поста, з усіма світлинами, відповідає домовленостям Hugo. Тобто пост пишеться в Markdown. Як до Markdown додати “журнальний формат”? Власними шорткодами. Шорткоди можуть додавати до тексту будь-яку розмітку. Вони можуть як вставляти її (наприклад, шорткод світлини), так і загортати текст (так можна створити виноски тощо.)
Моє бачення задачі таке: зміст поста розбивається на презентаційні блоки, впорядковані вертикально. Кожний блок матиме різний формат — це може бути текст, світлина, галерея тощо. Конфігурація блоку задається в метаданих поста; якщо потрібний текст, то він пишеться прямо всередині. Точніше навпаки: шорткоди, що створюють блоки, пишуться всередині тексту; так його легше редагувати, ніж коли текст порізаний на YAML або інший конфігураційний формат. Власне, пост пишеться як звичайно, тільки доповнюється шорткодами — і це дуже важливо, бо реалізація робиться не на один пост, а на майбутнє.
На останок додам, що сучасний CSS - це приємно. Я людина травмована попередньою епохою верстки на floatах. Флоати були складною та неприродною поробкою. Плюс до того браузери працювали по-різному. Тому CSS був езотеричною технологією, де без глибокого досвіду було важко зробити щось красиве, зате легко — поламати.
Зараз все набагато краще. Нюанси поведінки браузерів практично зникли. Верстка на flexbox підтримується у 99% клієнтів. Модель flexbox прозора, її легко опанувати, та вона робить те, що від неї очікуєш. Багато всього, що раніше робилось за допомогою JavaScript, тепер вирішується засобами CSS. От сьогодні дізнався про scroll-snap; чудово!
Тож раджу спробувати, може й тебе CSS приємно здивує.
21.04.2023
Мінімальний масштаб сервісів
Байдуже, наскільки мало навантаження є на сервіс або мікросервіс — в мене є базовий стандарт, нижче якого “живі” продукти не мають спускатись.
Перше — будь-який сервіс повинен мати цілий CPU. Проблема не в тому, що дробового CPU вам не вистачить — багато сервісів ніколи не здіймаються вище за 10% використання CPU. Проблема в тому, що, за моїм досвідом, дробовий час CPU - це абстракція. Він не забезпечується надійно та рівномірно, та в непередбачуваний момент сервіс може не отримати свою долю. (що ззовні спостерігається як затримка) Тому, тільки цілий CPU, або декілька. Чи потрібні декілька? Залежить від того, наскільки сервіс здатний до паралелізації. Якщо паралелізація досягається шляхом створення декількох процесів — як роблять сервери додатків Ruby - то є наступне питання — робити одну репліку з декількома CPU, або багато реплік з єдиним CPU?
Друге — будь-який сервіс повинен бути запущеним принаймні у двох екземплярах. Знов-таки, питання не в навантаженні. Просто, якщо сервіс відвалиться через одну з безлічі причин — памʼять закінчилась, трапилась критична помилка, якісь довготривалі запити зайняли всю чергу — то треба мати резерв. Більше копій — більше надійності, але одна додаткова копія нескінченно надійніша ніж жодної.

