Махровый fullstack
16 subscribers
66 photos
27 links
Личный блог @nornik:
- Бэкенд, фронтенд и случайные мысли - в произвольных сочетаниях.
- Иногда литература, иногда всё, что взбредёт в голову.
Download Telegram
Не каналами едиными
- Динамическое логирование через Monolog без config/logging.php -

Что ж, поговорим о логировании.

TL;DR
Если каналы Laravel не подходят - используем Monolog напрямую. RotatingFileHandler, маленький сервис - и каждый генератор пишет в свой лог.


Laravel под капотом имеет удобную систему логирования на основе библиотеки Monolog и каналов логирования. В большинстве случаев этого более чем достаточно, но бывают и исключения, когда каналы начинают мешать.

Мне необходимо было генерировать собственные логи под определённую рутину. ✍️

Что за рутина? Микросервис по рассылке автоматических push-уведомлений.

Микросервис на основе генераторов формирует и отправляет пользователям push-уведомления. При этом необходимо логировать процесс генерации: логика довольно ветвистая, сценариев много, а потому нужен глаз да глаз.

Загвоздка заключалась в том, что генераторы создаются автоматически через UI, а поэтому заранее предугадать список каналов логирования невозможно - а логировать надо. Можно было бы всё писать в один файл... да поди потом разберись в мегабайтах текста что там вообще происходит. 😰

Не долго кручинясь и не сильно вешая буйну голову, накидал небольшой сервис логирования, который работает без заранее прописанных каналов: 💡
class LoggerService
{
protected Logger $logger;

public function __construct(string $filename = 'custom.log')
{
$this->logger = new Logger('custom');

$this->logger->pushHandler(
new RotatingFileHandler(
storage_path('logs/' . $filename),
14,
Logger::DEBUG
)
);
}
....
}

RotatingFileHandler автоматически создаёт ежедневные файлы логов:
storage/logs/push-generator-1-2026-04-07.log
storage/logs/push-generator-1-2026-04-08.log

Ну а дальше всё просто:
$logger = new LoggerService('push-generator-1.log');

$logger->info('Начало генерации');
$logger->warning('Устройство пропущено', ['device_id' => 42]);
$logger->error('Ошибка отправки', ['token' => $token]);

В итоге каждый генератор может писать в свой собственный лог, не требуя объявления каналов в config/logging.php.

Иногда лучший способ использовать Laravel - это перестать использовать Laravel. 😏

#laravel #logging #monolog #backend #web #разработка
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
Как я перестал бояться SQL... и создал полезного монстра

— Как один SQL заменил пачку запросов и килограммы PHP —

Ох, маркетинг, такой маркетинг!

TL;DR
Собрал сегмент “недосмотренных сериалов” одним SQL-запросом через WITH + оконные функции. Убрал N+1, сократил нагрузку на бэкенд и получил готовые данные под пуши прямо из БД.

Есть задача: вернуть пользователя к просмотру сериала, который он недосмотрел, и отправить ему push с призывом продолжить. 🍿

Изи-пизи... если бы не ряд условий:
- надо найти пользователей, которые смотрели хоть что-то; 
- при этом отсеять тех, кто уже досмотрел сериал; 
- выбрать “приоритетный” сериал:
— сначала по дате последнего просмотра; 
— если одинаково - по количеству просмотренных эпизодов;

Решая задачу «в лоб», на горизонте замаячило большое количество запросов к БД, обработка данных на стороне бэкенда, забитая память и прочие радости. В общем, в перспективе запахло это всё не очень, и я начал терять волю. 🤨

А ещё это ломало мою выстроенную парадигму:
запрос к БД -> получение данных -> подготовка данных -> отправка сообщения.
Так как требовало дополнительной реализации обработчика, а я человек ленивый. 🙄

Вспомнив своё банковское прошлое (ммм, какие там делали причудливые запросы к БД) решил попробовать утащить всю логику в SQL.

В итоге решение можно представить в виде такой схемы: 💡
view_histories (история просмотров)
↓
series_stats (агрегация)
↓
episodes_total (общее число эпизодов)
↓
filtered_series (отсекаем досмотренные)
↓
ranked_series (ранжирование)
↓
финальный SELECT

Не совсем тривиальный SELECT, да? 😄

Весь SQL приводить не буду - он разросся строк до ~70 и слегка пугает неподготовленного зрителя, но, в целом, картина примерно такая:
WITH series_stats AS (
...
),
episodes_total AS (
...
),
filtered_series AS (
SELECT
ss.*,
et.total_episodes
FROM series_stats ss
INNER JOIN episodes_total AS et
ON et.content_id = ss.content_id
WHERE ss.watched_episodes < et.total_episodes
),
ranked_series AS (
SELECT
fs.*,
ROW_NUMBER() OVER (
PARTITION BY fs.app_user_id
ORDER BY fs.last_viewed_at DESC, fs.watched_episodes DESC, fs.content_id DESC
) AS rn
FROM filtered_series AS fs
)
SELECT DISTINCT
...
JSON_OBJECT(
'content_id', rs.content_id,
'watched_episodes', rs.watched_episodes,
'total_episodes', rs.total_episodes,
'last_viewed_at', rs.last_viewed_at
) AS params
FROM app_users AS au
...
INNER JOIN ranked_series rs
ON rs.app_user_id = au.id
AND rs.rn = 1
...

Да, выглядит немного монструозно, но результат - просто конфетка. ❤️

Что тут интересного:
- использую цепочку CTE (Common Table Expression) через WITH для подготовки данных;
- считаю просмотры и отдельно количество эпизодов;
- отсекаю полностью просмотренные сериалы прямо в БД;
- ранжирую сериалы внутри каждого пользователя через оконные функции (ROW_NUMBER);
- собираю результат в JSON (JSON_OBJECT), чтобы не городить DTO на бэкенде;

⚠️ Но WITH не всегда бесплатно. Иногда MySQL может материализовать CTE и положить данные физически на диск, поэтому в тяжёлых запросах не забываем про EXPLAIN.

It's alive! It's alive!

#sql #cte #with #backend #разработка
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4
Читальня. Д. Кюнскен – Квантовый сад

— Обзор первой книги - Квантовый волшебник


— "Срань Господня!" - крикнул Стилс. - "Он антиматерией стреляет!" —

На манеже всё те же — Белизариус Архона снова тащит нас в квантовую шизофрению. Перед нами прямое продолжение немного неоднозначного, но всё-таки весьма интересного «Квантового волшебника».

Мы оказываемся ровно там, где закончилась история первой части: Белизариус всех объегорил, умыкнул врата времени у Суб-Сахарского Союза и нашёл свою асисяй. И тут бы жить-поживать, да добра наживать, но суровая жопа реальности находит нашего героя в виде Конгрегации.

Тут Белизариус рефлексирует.

Конгрегация задумывается о возможностях Homo quantus и в итоге начинают видеть угрозу в квантовых людях. Дабы пресечь угрозу на корню - решают проблему основательно и геноцидят весь родной род Архоны.

Тут Белизариус снова рефлексирует.

Основные действия книги происходят 39 лет назад - да, ребята воспользовались вратами времени, и полетели в прошлое, в те времена, когда Суб-Сахарский Союз был молодым и неокрепшим. Члены экспедиции спали в плохо отапливаемых казармах, над головой висел коричневый карлик, обещая вот-вот погаснуть, а сама экспедиция погрязла в политических интригах и расприях.

На этом фоне появляются двое из ларца: Белизариус и Иеканджика - их задача не наломать дров в прошлом, дабы будущее осталось нетронутым. Решая задачу временных парадоксов, герои оказываются втянутыми в шпионские страсти, с весьма драматичным финалом.

В казармах Белизариус тоже рефлексирует.

Но самое интересное, что герои находят необычную квантовую форму жизни - растения, чей жизненный цикл основан на принципах квантовой суперпозиции. У неё даже есть свой язык, основанный на запахах, и учёные суб-сахарского союза создали девайс-переводчик для общения с ними. Но в итоге судьбу Hortus quantus решает интерпретация фон Неймана — Вигнера (даже пришлось подсмотреть что это такое). 💡

Над растениями пуще прежнего рефлексирует Белизариус.

Ещё я бы отметил один момент - это преобразование в Пугало - универсального солдата Конгрегации. И хотя всю книгу сей персонаж сидит в замкнутом пространстве и, в целом не сильно отсвечивает, но история его становления и преобразования кажется неплохой разбавкой общему тону книги. Да и вообще, развить бы историю персонажа - отличный был бы антогонист.

И тут... Ха! Нет! Тут Белизариус не рефлексирует! 😄

Итого: книга ощутимо слабее Квантового волшебника - душевных переживаний слишком много, описаний 22-х мерного пространства хватило ещё в первой книге, а сюжет какой-то невзрачный по итогу. И знаете... возглас Стилса очень хорошо описывает послевкусие от книги. Не-не, я не про антиматерию ;)

Но при всём при этом, если у нас издадут продолжение, я скорее всего его почитаю 💅 (говорят там подинамичнее происходящее) - вселенная же ж интересная, а геномодифицированные люди до конца себя ещё не показали.

5 пугало из 10

#книги #почитать #ДерекКюнскен #Кюнскен #КвантоваяЭволюция #КвантовыйСад #хроно #фантастика #космос
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3🔥1
Призраки в сети — тайна пропадающего домена

— О чём молчат минималистичные Docker-образы —

TL;DR
Если Docker-контейнер на php:*-alpine периодически ловит cURL error 6: Could not resolve host, - проблема может быть в musl, а не в Docker или DNS.


Интересный кейс случился на днях. В логах приложения стали сыпаться сообщения:
cURL error 6: Could not resolve host: here-some-secret-domain.com

🤌

Первым делом проверил что там видит docker у себя внутрях при резолве домена:
docker compose exec php getent hosts here-some-secret-domain.com

Оказалось - нифига он не видит.

Проверил что видит сам хост:
dig here-some-secret-domain.com
;; communications error to 172.16.2.231#53: timed out

Видит! Видит таймаут. 🤦‍♂️

Пропинговал DNS-сервер, которые выдал провайдер - не пингуется.

На всякий случай проверил резолв на общеизвестных DNS-серверах:
dig @8.8.8.8 here-some-secret-domain.com
; <<>> DiG 9.10.6 <<>> @8.8.8.8 here-some-secret-domain.com
; (1 server found)

И получил вполне бодрый ответ - домен резолвится отлично.

Сделав единственно логичный вывод, - проблема на стороне хостера с DNS-сервером - приступил к лечению:
— отредачил /etc/resolv.conf, указав, что используем 8.8.8.8 и 4.4.4.4;
— обновил DNS на интерфейсе resolvectl dns eth0 4.4.4.4 8.8.8.8;
— прописал адреса DNS для docker пожёстче:
services:
php:
dns:
- 8.8.8.8
- 4.4.4.4

Отписал в поддержку о проблеме и пошёл пить кофе, и жевать конфеты! ☕️

В безмятежном состоянии пребывал не долго - повальная ошибка cURL error 6: Could not resolve host ушла, но периодически нет-нет, да и появится. И это было весьма болезнено. 😭

Картина получалась интересная: на хосте домен резолвился без каких-либо замечаний, а вот из докера домен периодически не резолвился - то есть DNS то жив, то мёртв, то притворяется.

И вот тут я реально приуныл, потому что никаких явных причин для такого поведения просто не было.

Пробовалось многое:
— перевод запросов к DNS на tcp;
— разумеется, несколько ребутов;
— пересоздание сетей docker;
— молитвы Омниссии 🤖;

В общем, весь опыт накопленный человечеством оказался бесполезен. 😡

Дабы долго не томить - проблема оказалась в образе - php:8.4-fpm-alpine. Alpine использует musl вместо glibc, и у musl исторически бывают очень нестабильные отношения с DNS, Docker networking и резолверами.

Известные проблемы musl с DNS:
— упрощённый DNS‑клиент. musl не реализует все механизмы glibc, например, сложные стратегии ретраев и таймаутов;
— ограниченная обработка ошибок. При проблемах с первым DNS‑сервером musl может не переключиться на следующий в списке;
— особенности кэширования. Поведение кэша DNS в musl отличается от glibc - возможны устаревшие ответы или пропуски обновлений;
— сетевые взаимодействия. В Docker‑сетях (особенно overlay) musl иногда некорректно обрабатывает ответы от встроенного DNS‑сервера Docker (127.0.0.11).
— таймауты. musl имеет жёсткие таймауты по умолчанию, что критично при задержках в сети;

❗️Мораль❗️
Не гонись, поп, за дешевизной -
Малый образ - геморрой длиной в полжизни!


Ну, а alpine теперь буду использовать только для локальной разработки, а ведь образы так нравились своей минималистичностью.

Моё сердце разбито! 😭

#docker #php #laravel #alpine #musl #devops #backend #web #разработка
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4
Хирургическая очистка кэша в Yii2

— Не все зависимости одинаково вредны —

TL;DR
Волшебный TagDependency позволяет привязать к одному тегу сколько угодно записей кэша и потом инвалидировать их одной командой.


Что ж друзья, сегодня поговорим о зависимостях. Все знают, что это такое. Сначала кажется, что всё под контролем. Потом они начинают влиять на твою жизнь. А потом ты уже не представляешь, как жил без них. 😍

Зависимости бывают у всех. У людей, у проектов, у npm-пакетов... и даже у кэша в Yii2. И штука эта, надо сказать, офигенная, хотя пользуются ей незаслуженно редко. За всю свою практику я ни разу не встречал её на реальных проектах. Обычно же как: ключ, данные, TTL — и погнали дальше.

Кароч! Рассказываю...

Делал я одну фичу. Суть её сейчас не важна, но появилась необходимость складывать данные, получаемые в ходе работы, в кэш. Ну надо так надо - складываем. А что делают с кэшированными данными? Правильно — периодически чистят для поддержания гигиены и актуальности. И вот тут нашла коса на камень.

Ключи кэша в данной фиче составные и зависят от ряда параметров. Ключи примерно такие — cache.for.feature.key1.key2.key3. И их конечное количество неизвестно. Ну как неизвестно... Примерно дохреналиард их. 😫

И как это всё очищать спросите вы. Ведь спросите же?

Вариантов решения вроде бы хватает. Можно хранить список ключей отдельно, можно искать по префиксам, можно генерировать какие-нибудь служебные ключи для очистки, но всё это выглядит как костыли. Не, можно ещё беспощадно сделать — Yii::$app->cache->flush(). Очищай больше, хрен ли его жалеть!

Другое дело — зависимости кэша. 💡

Для своей задачи я использовал: TagDependency — cвязывает кэшированные данные элемента с одним или несколькими тегами и позволяет инвалидировать данные кэша по этому тэгу.

Синтаксис максимально простой и понятный:
Yii::$app->cache->set(
$cacheKey,
$cacheData,
$cacheTtl,
new TagDependency(['tags' => $tag])
);

А инвалидация кэша делается ещё проще:
TagDependency::invalidate(
Yii::$app->cache,
$tag
);

Разумеется, работает это только с кэш-бэкендами, которые поддерживают зависимости, но для типичных Redis, Memcached и файлового кэша проблем обычно не возникает.

И всё — никаких тебе поисков по префиксам, генерации ключей и прочего мракобесия.

Просто, изящно, профессионально! ☕️

#php #yii2 #cache #dependency #backend #web #разработка
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥2👍1
Читальня. Питер В. Бретт – Трон черепов

— Обзор первой книги - Меченый
— Обзор второй книги - Копьё пустыни
— Обзор третьей книги - Дневная битва


– Власть развращает, а абсолютная власть развращает абсолютно –

Новая порция приключений на просторах Тесы.

К началу 4-го тома события цикла окончательно разделились на две истории. Мир живёт сам по себе, а Избавители — сами по себе. Да, их пути ещё пересекаются отдельными оказиями, но в целом это уже две разные истории.

У Избавителей есть дерзкий план покончить с подземниками раз и навсегда, и они его придерживаютя. Хотя в этой книге избранные отходят на второй план, без экшена не обошлось — будут и сражения, и магия, и копья, и метки.

А вот остальной мир занят более приземлёнными вещами — борьбой за власть и финансовые потоки. Как только мессии канули в лету после эпичной битвы, места вождей внезапно оказались вакантны, и привычная человеческая сущность вылезла наружу.

Первенцы Ахмана искренне желают смерти друг другу и так же искренне хотят усесться на Трон Черепов. Но на пути к заветному стулу стоят и мать, и дядя. И даже Аббан пристраивается сбоку бантиком, чтобы оказаться как можно ближе к трону. Аббан вообще сам себе на уме парень — хорош, чертяка!

Но и северяне времени даром не теряют. Играют тоньше — интриги, союзы, предательства и прочая подковёрная возня. Даже Лиша умудрилась вляпаться во власть, которой никогда особо не искала.

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

Так же появляется ещё больше новых персонажей. Хотя их уже накопилось столько, что хватило бы ещё на несколько томов приключений, пока Избавители занимаются спасением мира.

По гамбургскому счёту получается странная книга. Событий много. Персонажей много. Политики очень много. А вот основной сюжет почти не двигается вперёд.

В то же время заскучать окончательно не получится. Здесь гибнут важные персонажи, рушатся планы, армии терпят поражения, а некоторые герои внезапно заканчивают свой путь раньше, чем ожидаешь. Но всё это воспринимается скорее как хроника борьбы за власть.

Итого: читать интересно, но впервые поймал себя на мысли, что местами было скучно. Не из-за отсутствия событий — их как раз хватает. Просто почти все они второстепенны и мало влияют на главное противостояние людей и демонов.

7 кастратов из 10.

#книги #почитать #ПитерВБретт #Бретт #ВселеннаяМеченого #ТронЧерепов #тёмное #фентези #магия #монстры
❤2👍2
Когда Sanctum слишком старается

— Или как перестать насиловать БД UPDATE'ами —

TL;DR
Не каждый запрос достоин отдельного UPDATE. Уменьшаем количество обращений к БД без ущерба для бизнес-логики.


Да что вы знаете о бестолковой работе! А серверы знают. И тоже устают, когда их заставляют выполнять совершенно лишние операции.

Поговорим немного об оптимизации API, а в частности, снизим немного нагрузку на БД при использовании Laravel Sanctum.

Туфля не мой, но решение полезное, что не рассказать о нём было бы преступлением.

Костя, привет! 🍺


В чём соль: sanctum, по умолчанию, обновляет поле last_used_at при каждом запросе с токеном. По правде сказать, в большинстве проектов такая точность попросту не нужна. Для небольших аппов такое поведение не критично, но чем выше RPS, тем ощутимее становится лишняя нагрузка на БД.

Мне сложно представить бизнесовую задачу, где действительно важно знать время использования токена с точностью до каждого запроса. А посему можем слегка умерить энтузиазм sanctum и настроить дросселирование, он же тротлинг.

Но мы пойдём другим путём и вынесем механику в универсальный трейт, который можно использовать и в других моделях.
trait ThrottlesAttributeSaves
{
protected function shouldThrottleAttributeSave(string $attribute, int $throttleSeconds): bool
{
$dirty = $this->getDirty();

if (!isset($dirty[$attribute]) || count($dirty) !== 1) {
return false;
}

$original = $this->getOriginal($attribute);

if (!$original) {
return false;
}

if (!$original instanceof Carbon) {
$original = Carbon::parse($original);
}

return now()->diffInSeconds($original) < $throttleSeconds;
}
}

Что тут происходит:
– $dirty = $this->getDirty(); — cмотрим, что изменилось в модели;
– if (!isset($dirty[$attribute]) || count($dirty) !== 1) — если меняется только один атрибут. то включаем троттлинг;
– если прошло меньше чем $throttleSeconds секунд — притворяемся, что сохранили модель, но на самом деле ничего в БД не пишем;
– во всех остальных случаях — сохраняем как обычно через parent::save();

А в самой модели перепишем save():
public function save(array $options = []): bool
{
if ($this->shouldThrottleAttributeSave(
'last_used_at',
self::THROTTLE_SECONDS
)) {
return true;
}

return parent::save($options);
}

Получается своеобразный rate limit для самого Eloquent.

Ну по красоте же получилось! Сервера точно скажут спасибо.🙏

#php #laravel #sanctum #оптимизация #backend #web #разработка
Please open Telegram to view this post
VIEW IN TELEGRAM
❤2🔥2
НЛО снова не прилетело, но это не повод не отметить

– Truth, как говорится, is out there! Но это не точно. –

Вчера, 2-го июля, как-то незаметно прошёл День уфолога. Впрочем, всё, что связано с НЛО, обычно проходит незаметно.

А между тем именно 2 июля 1947 года в штате Нью-Мексико, недалеко от городка Розуэлл, местный фермер нашёл на своём ранчо странные обломки, напоминавшие тонкую фольгу со странными символами, которая не поддавалась ни механическому, ни тепловому воздействию. Журналисты тут же раструбили о захваченной летающей тарелке военными. А военные заявили о метеорологическом зонде и о свете с Венеры, который отразился от верхних слоёв атмосферы и вызвал взрыв болотного газа. 🛸

Тем не менее именно этот случай считается отправной точкой, сформировавшей большой интерес к уфологии и теориям заговора. А ведь загадок-то тьма-тьмущая:
— Воронежский инцидент октября 1989 года;
— ЧП в пустыне Калахари в мае 1989 года;
— проект «Синяя книга» 1952–1969;
— культовая Зона 51;
— Петрозаводский феномен, произошедший 20 сентября 1977 года;
— бельгийская волна НЛО 1989-1990;
— невероятные Огни Феникса 1997 года;

Да и недавняя активность вокруг НЛО тоже подливает маслица в огонь:
— правительство США публикует очередные рассекреченные материалы об НЛО/UAP — правда, без зелёных человечков в приложении;
— интернет уже связал в одну теорию заговора десяток пропавших и умерших учёных, так или иначе работавших рядом с космосом, оборонкой или ядерной физикой;
— международная академия астронавтики обновила рекомендации SETI и просит учёных не отвечать на внеземные сигналы без международных консультаций уровня ООН. То есть и тут без дейлика не обойтись.

Видимо, агент Малдер всё-таки что-то знал...🛸

Однако, кругов на полях становится всё меньше, тарелки летают только на кухне, йети и чупакабры впали в спячку, а зелёные человечки перестали похищать людей, видя какой дурдом творится на Земле...

Или же на других планетах тоже сокращают финансирование космических программ?

#внезапно #позновательно #изюм #нло
Please open Telegram to view this post
VIEW IN TELEGRAM
👾3
Читальня. Ч. Мьевиль – Крысиный король

— Если ты танцуешь под джангл тебе нужен только бас —

В одном мрачном-мрачном Лондоне, на одной мрачной-мрачной улице, в одном мрачном-мрачном доме жил молодой человек с отцом. И однажды с отцом приключилась беда — он выпал из окна.

И жизнь простого британского парня - Гарамонда, Савла Гарамонда - перевернулась с ног на голову. Его задержали, допрашивали и посадили в кутузку. В кутузке жизнь от парня не отстала и переворачивает мир повторно, но не ставит на место, а ещё больше сносит крышу.

И носит по крышам...

Савл сбежал из-под замка благодаря странному незнакомцу и попал в необыкновенный сказочный мир - в мрачный, затхлый, вонючий и дерьмовый мир. Этот андеграундный мир населён различными люмпенами и монархами: крысиный король, паучиный король, птичий король и коварный Флейтист. И эта королевская братия решает некий давний конфликт, возникший между этой разношёрстной компанией и Флейтистом. И Савлу в этом конфликте отведена не последняя роль.

Пока Савл носится по крышам и отсиживается в коллекторах, автор старательно пичкает нас названиями лондонских улиц, переулков и проходных дворов — какая с какой пересекается и под каким углом. Я не пытался даже запомнить - сложно и не нужно, но пытливый ум может взять карту и поводить пальчиком.

Побываем в лондонской канализации - автор, как по мне, очень натуралистично описывает интерьер коллектора, что текст начинает давить на психику узкими проходами, вонью сточных вод и едким запахом крысиной мочи (сам не знаю, но автор, похоже, в вопросе разбирается). Периодически будем наведываться в мусорные баки и доедать что-то недоеденное и не очень-то свежее.

Окунёмся в музыкальный андеграунд и познакомимся с некоторыми аспектами jungle и drum'n'bass. Без этого элемента не существовало бы и истории - он здесь играет ключевую роль.

Признаюсь, упоминание Goldie в тексте выдавило скупую ностальгескую мужскую слезу. И заставило стряхнуть пыль с пары древних треков. Но почему забыли Aphrodite?! Несправедливо!

Драм здесь, драм там. Драааааам! Бам! Бам! Бам!

Много рассуждений о музыке и субкультуре, которые поймут не только лишь все. А под занавес нас ждёт рейв-вечеринка, которая разматывает в хлам вообще всех: людей, крыс, пауков и всех, кто оказался не в том месте и не в то время. Кровища, кишкища, непотребства - вечерина удалась одним словом!

И только инспектор Кроули, задержавший Савла, грустит, подшивая очередные глухари.

Но есть и хорошие новости - в крысином королевстве случился коммунизм. 👴

А возможно, вся эта история горячечный бред, вызванный передозом спидов на рейв-вечеринке?

Кто ж знает...

Итого: необычная, мрачная и жестокая история. Понравится - не понравится тут на вкус каждого, но в любом случае книга не останется незамеченной. Такая, что захочется с кем-то обсудить всю ту дичь, что наворотил автор.

8 мусорных баков из 10

#книги #почитать #ЧайнаМьёвиль #КрысиныйКороль #городское #тёмное #фэнтези #необычное #толькобасспасётнас
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥3❤1
Мутанты на проводе или Всевидящий MutationObserver

— Когда мир перестаёт быть реактивным, остаётся только наблюдать —

TL;DR: 
Иногда не нужно менять архитектуру. Достаточно наблюдать и DOM сам расскажет, что произошло.


Давненько у нас не было фронтовых баек, а пожалуй что вот оно!

👩‍💻 Уютный мир: ламповый laravel, приятные vue-компоненты, встроенные в шаблоны. Всё живёт в безмятежном неведении надвигающейся катастрофы.

👩‍💻 Но тут появляется внешний JS-скрипт, который начинает управлять состоянием vue-компонента извне. В частности - открывает попап, а vue-форма внутри должна менять своё поведение.

Должна, но не может!

Классические приемы не работают: 🤦‍♂️
— Пропсы не пробросить - компонент уже смонтирован;
— События не проэмитить - нет родителя;
— Пересоздавать компонент с переносом логики - слишком жирно. Да и внешний скрипт менять нельзя.

Можно было бы отчаяться и уйти из IT на ферму кроликов разводить, но на помощь приходят мутанты-наблюдатели! 🐢

Да, MutationObserver - наше спасение. Всевидящее Око Саурона в мире DOM.

Создаём наблюдателя. Теперь он в засаде и зорко наблюдает за всеми изменениями у родителя:
const component = el.parentElement as HTMLDivElement | null;
let observer: MutationObserver | null = null;

onMounted(() => {
observer = new MutationObserver(() => {
if (component.classList.contains('popup_active')) {
// наша логика
}
})

observer.observe(component, {
attributes: true,
attributeFilter: ['class'],
});
});

Но что бы наблюдатель был не слишком наблюдателен, ограничим его кругозор через фильтр: attributeFilter и следим строго за классом.

Ну и, конечно, по-джентльменски освобождаем наблюдателя при смерти компонента.
onUnmounted(() => { 
observer?.disconnect(); 
observer = null;
});


Если состояние меняется вне зоны реактивности компонента - это не приговор.

Мутанты среди нас, и они всегда помогут! ❤️

#vue #mutationobserver #javascript #frontend #web #разработка
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
Достучаться до Reverb: без зависимости от окружения

– Как подружить Laravel Reverb с Nginx без головной боли –

tl;dr: 
Подключаемся к WebSocket на текущем домене, /app проксируем через nginx — и получаем один билд для всех окружений.


Давненько слышал про этот пакет, но всё как-то руки не доходили. А тут случилась оказия - надо настроить оповещение для редакторов, если кто-то уже правит контент.

Ни слова больше!

Разумеется вариант с периодической долбёжкой сервера запросами по таймеру был отметён сразу. Таким дети в пубертате занимаются, а мы то уже огого - знаем что такое WebSockets! 💡

Вариант с socket.io тоже был отметён - чай не 2010 год на дворе. Чатов на нём за эту жизнь уже написано достаточно, да и прочие realtime-приложения реализованы. Хотелось попробовать что-нибудь новое.

Кароч! Reverb - наш герой сегодняшнего дня.

— Умеет только websockets, без fallback-механизмов (long polling и проч.);
— Клиентская либа laravel-echo из коробки;
— Публичные, приватные и presence-каналы, со стандартной Laravel-авторизацией;
— Self-hosted, нативно дружит с Laravel и удобный режим отладки;

Но, если бы на этом всё, я бы и писать не стал.

Сетап Reverb оказался не без сюрпризов. Ничего криминального, но некоторые особенности таки застали меня врасплох.

Docker, Reverb и первые грабли

В локальном окружении я использую Docker, поэтому Reverb оформлен отдельным сервисом.
reverb:
  build:
    context: ./dockerfiles
    dockerfile: php/Dockerfile
    args:
      NGINX_PATH: ${NGINX_PATH}
  volumes: !override
    - ./src:${NGINX_PATH}
  env_file:
    - ./src/.env
  environment:
    NGINX_PATH: ${NGINX_PATH}
  command: php artisan reverb:start
  depends_on:
    - php
    - redis
    - queue
  restart: unless-stopped

Настроек у Reverb из коробки довольно много, что сначала и ввело меня в некоторое замешательство.

Локально всё завелось, я довольно потёр руки, выкатил это добро на сервер — и... Не работает!

Выделывать коленца стал фронт, потому что сборка зависит от окружения. 🤪

О, сколько нам открытий чудных
Готовят просвещенья дух,
И опыт, сын ошибок трудных...


Идея, в итоге такая: браузер всегда подключается к WebSocket на текущем домене, а nginx проксирует /app в Reverb.

То есть снаружи адрес всегда выглядит одинаково: wss://мой-крутой-сервис.рф/app/...

А что там дальше — Docker, localhost или отдельный сервер — браузеру совершенно неинтересно.

1. Упрощаем Echo

Убираем из echo.js окружение-зависимые хосты и прочие радости:
window.Echo = new Echo({
    broadcaster: 'reverb',
    key: import.meta.env.VITE_REVERB_APP_KEY,
    wsHost: window.location.hostname,
    wsPort: window.location.protocol === 'https:' ? 443 : 80,
    wssPort: 443,
    forceTLS: window.location.protocol === 'https:',
    enabledTransports: ['ws', 'wss'],
});

Оставляем только ключ: VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"

2. Настраиваем Laravel и сам Reverb

Серверную часть Laravel будет читать из .env:
REVERB_HOST=reverb
REVERB_PORT=8888
REVERB_SCHEME=http
REVERB_SERVER_HOST=0.0.0.0
REVERB_SERVER_PORT=8888


У Reverb фактически есть два разных набора координат:
REVERB_SERVER_* — где слушает сам Reverb
REVERB_*      — куда Laravel отправляет события
VITE_REVERB_*   — куда подключается браузер

3. Ставим nginx посередине

Добавляем проксирование WebSocket-соединений:
location /app {
    proxy_http_version 1.1;
    proxy_pass http://reverb:8888;
    proxy_set_header Host $host;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
    proxy_read_timeout 60s;
    proxy_send_timeout 60s;
}

Теперь браузер локально ходит так: wss://мой-крутой-сервис.рф/app/...

Итоговая логика
Browser -> текущий домен /app -> nginx -> Reverb
Laravel -> внутренний REVERB_HOST:8888 -> Reverb


Фронт в итоге собирается одинаково для всех окружений, а бэкенд легко настраивается только через env и nginx upstream. Никаких environment-specific во фронте, отдельных билдов и плясок вокруг конфигурации.

Это сильно упрощает жизнь, экономит время, нервы и делает стул регулярным.

#php #laravel #reverb #websockets #backend #web #разработка
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
Неуникальная уникальность, и при чем тут дни рождения

– Ведь в мире их еще миллиарда четыре! А быть может и пять, как знать? –

tl;dr
Почему 2^32 вариантов — не так много, как кажется?
Немного математики о коллизиях и обманчивой уникальности.


Наткнулся на днях на интересное понятие - Парадокс дней рождений (birthday paradox). Это ситуация, когда интуиция нас подводит в оценке вероятностей.

В чём суть

Классический пример - кажется, что, раз в году 365 дней, то чтобы с вероятностью 50% у кого‑то в группе совпали дни рождения, то понадобится (на глазок) что-то около половины от 365 - то есть под две сотни человек. Но математика говорит другое: достаточно всего 23 человека, чтобы вероятность совпадения хотя бы у двоих была примерно 50,7%.

Так происходит потому, что сравнивается не "один человек со всеми датами", а все пары людей между собой.

Для 23 человек пар будет: 23 * 22 / 2 = 253 пары

У каждой пары есть шанс совпасть. И чем больше таких пар, тем быстрее растёт вероятность, что хотя бы в одной из них случится совпадение.

Почему об этом важно помнить в типовых задачах? 

Зачастую мы генерируем всякие уникальные значения: UUID, slug, реферальный код и прочие идентификаторы каким-нибудь эдаким способом:

$slug = substr(sha1($string), 0, 8)

Разберём

Один hex-символ хранит 4 бита. Значит 8 hex-символов - это 32 бита. А это, на минуточку, 2^32, примерно 4,29 млрд возможных значений 🤑. Дофига, не правда ли? На первый взгляд много, но из-за парадокса коллизии становятся вероятными намного раньше.

Вероятность хотя бы одной коллизии можно приблизительно оценить по формуле:

p ≈ 1 - exp(-n(n - 1) / (2 × 2^32)).

Оценочно:
10 000 значений   -> ~1.2% шанс коллизии
50 000 значений   -> ~25% шанс коллизии
77 000 значений   -> ~50% шанс коллизии
100 000 значений -> ~69% шанс коллизии


То есть проблема не в том, что 4 млрд быстро закончатся, а в том, что при случайном распределении два значения могут получить один и тот же 8-символьный slug уже на десятках тысяч записей.

Как исправить

Увеличить пространство значений, чтобы сделать коллизию практически невозможной.

Например, 16 hex-символов — это уже 64 бита. Для 100 000 записей вероятность коллизии составляет примерно 0,000000027%.

А если идентификатор обязан быть уникальным, то никакая теория вероятностей не заменит UNIQUE на поле и обработку коллизии при генерации.

—

Дурацкая математика! Вечно портит красивые решения! 😵

#php #коллизии #хеширование #birthdayparadox #backend #web #разработка
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Не долбимся как кролики с RabbitMQ и yii2-queue

– retry, delay, exponential backoff и немного jitter без лишнего насилия над очередями –

tl;dr
yii2-queue с amqp_interop может повторно отправлять упавшую задачу практически сразу. Добавляем DeliveryDelay - и получаем retry с паузой.

Ну что же, у нас классика на столе - yii2 и система нотификаций, реализованная через yii2-queue и rabbitmq в качестве брокера очередей.

У очередей настроены попытки - пусть 5 для определённости.

Как будто-бы всё хорошо:
джоба падает → yii2-queue делает retry → жизнь продолжается.

Но есть нюанс.


При использовании amqp_interop повторная отправка сообщения идёт через redeliver(). И идёт она сразу.
То есть внешний сервис прилёг, API отвечает ошибкой - а мы такие:
— Не получилось?
— Не получилось.
— А сейчас?
— Нет.
— А сейчас?
— Всё ещё нет.
— А СЕЙЧАС?!
Не retry, а тревожный разработчик какой-то перед дедлайном. 😃

Улучшаем - добавляем паузу

Сам yii\queue\amqp_interop\Queue почти всё уже умеет. Но немножечко надо допилить метод redeliver и добавить отправку сообщений с задержкой.

protected function redeliver(AmqpMessage $message): void
{
$attempt = (int)$message->getProperty(self::ATTEMPT, 1);
$delay = $attempt * 60;

$newMessage = $this->context->createMessage(
$message->getBody(),
$message->getProperties(),
$message->getHeaders()
);
$newMessage->setDeliveryMode($message->getDeliveryMode());
$newMessage->setProperty(self::ATTEMPT, $attempt + 1);
$newMessage->setProperty(self::DELAY, $delay);

$producer = $this->context->createProducer();
$producer->setDeliveryDelay($delay * 1000);
$producer->send($this->context->createQueue($this->queueName), $newMessage);
}


Теперь между попытками появляется простенький linear backoff:
1 retry → 60 секунд
2 retry → 120 секунд
3 retry → 180 секунд
...


Если не нравится линейный вариант, то можно сделать exponential backoff:
$delay = 60 * (2 ** ($attempt - 1));

И тогда будет
attempt 1 → 60 сек
attempt 2 → 120 сек
attempt 3 → 240 сек
attempt 4 → 480 сек
...


Нет предела совершенству - добавляем jitter

Представим, что в очереди 10 000 сообщений. Все получили ошибку отправки уведомления через внешний сервис примерно одновременно и происходит retry для всей очереди.

Йеее - словили thundering herd problem, он же «эффект громового стада»! 😆

Чтобы не положить внешний сервис окончательно или не словить его rate limit, добавим немного случайности:

Full jitter - retry распределится по всему диапазону от 0 до $delay.
$jitter = random_int(0, $delay);

Equal Jitter - гарантированно ждём хотя бы половину задержки, а остальное отдаём на волю рандома.
$half = (int) ($delay / 2); 
$jitter = $half + random_int(0, $half);


Decorrelated Jitter - cледующая задержка зависит от предыдущей (оставим для самостоятельного изучения 😄)

🚨 !!! ВАЖНО !!!

Уход на retry с новой попыткой срабатывает только тогда, когда джоба выбрасывает Exception.

Не возвращает false, null или что-нибудь ещё в духе "ну, не шмогла".

А именно падает: throw new RuntimeException('Notification sending failed'); Иначе для yii2-queue джоба успешно завершилась и повторять там, собственно, нечего.

#php #yii2 #rabbitmq #yii2queue #queues #retry #amqp #backend #web #разработка
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2🔥1
❗️Тут такое дело!

Некоторое время назад, я таки решил разделить контент по тематике и о прочитанном развлекательном пишу тут не далеко - в паре кликов. А тут оставлю техническое.
Please open Telegram to view this post
VIEW IN TELEGRAM
Алишер Бекет - Сэхсвет

– Всё пожирается смертью; ведь гибель — закон, а не кара –

Пожалуй, начну в начале - с конца. Да! Однозначно читать!

И вот почему.

С первых страниц книги нас встречает прекрасное человеческое будущее. Люди летают по городам в гравикарах. Общественный транспорт, который тут незатейливо называется транспОртом, может вас доставить прямо из города прямо на северный полюс за пару-тройку часов. Здесь многие болезни побеждены, а импланты не только увеличивают грудь, но и решают очень даже серьёзные проблемы со здоровьем, а не только косметические хотелки. Но, собственно, сделать динамическую смену цвета глаз тоже запросто.

Кстати, встречает нас местный бомж, который не ел уже несколько дней и шарящийся по мусорным бакам в надежде найти что-нибудь, что ещё не отрастило свои ложноножки и не будет против употребления в пищу.

В общем, всё как мы любим - богатство, комфорт, роскошь, социальное неравенство, чёрный рынок трансплантологии и имплантологии, наркоманы и имплантоманы.

Дааа! Родной неоновый киберпанк завезли!

Но сколько бы у тебя ни было денег, имплантов и гравикаров, смерть здесь всё равно приходит за каждым. Причём буквально - в капюшоне и с правом выбора - гульнуть напоследок на все деньги и уйти в мир иной или сыграть в рулетку и попытаться остаться в этом мире. Но не всё так просто...

И только ты начинаешь погружаться в этот мир и проблемы героев: Джил, Кросстана, Мэйтта, как неожиданно человечество начинает месить друг друга в кровавую кашу. Появляются телепаты и начинают вытворять с окружающими разное нехорошее - да так, что где-то в сторонке нервно закуривает «Эльфийская песнь» Линна Окамото.

И ты такой - да какого хера случилось-то?!

И вот ты вроде бы смирился с мыслью - ну ок, пусть будут телепаты. Импланты есть. Смерть приходит с дождём. Почему бы и не телепаты! Мало ли, как коротит мозги у одержимых на фоне приёма интокса.

И тут - херак! - тебя выкидывают вообще в другое время, а человечество стало бессмертным.

И ты такой - да что происходит-то?!

А происходит то, что все те события, которые книга до этого заботливо наваливала тебе на голову, внезапно начинают обретать смысл. Киберпанк, импланты, старики, особенности Джил, Кросса и Мэйтта... Телепатия в конце концов! Одно цепляется за другое, вопросы начинают получать ответы, а предыдущие события обретают совершенно новый смысл.

И ты такой — да лаааадно!... Вон оно чо, Михалыч!

И чем больше впитываешь объяснения, тем сильнее поражает масштаб происходящего. Волосы начинают шевелиться против ветра, а мурашки маршируют строем под звуки «Прощание славянки».

Да вы даже не представляете, кто правит миром!

И вот ты уже сидишь накачанный, с кипящими мозгами, листаешь дальше и думаешь: Ну давай! Ломай меня полностью!

И финал делает это - обламывает!
Не ломает, а обламывает, Карл!

И ты такой — э-э-э... чего?

Не, мысль понятна. Причина замеса ясна. Но после всей той накачки в предыдущих частях я ожидал от финала чего-то другого. Чего-то столь же крышесносного, как всё, что к нему привело.

Честно, я был уже готов расчехлять десятку на своём личном шкалометре, но финал...

Эх, финал...

Итого: отличный киберпанк, интересные концепции будущего человечества, сюжет, раз за разом переворачивающий доску происходящего, и сногсшибательные объяснения всего движа. И финал, оставивший один большой вопрос: «Для чего?» Для чего вся эта суета была?

Тем не менее книга однозначно заслуживает внимания.

9 интоксов из 10

#обзор #АлишерБекет #Сэхсвет #научнаяфантастика #киберпанк #трансгуманизм #бессмертие
❤2🔥2
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥3👍1
Территориальный конфликт: война за 172.19.0.0/16

— Когда контейнер ищет внешний сервер у себя под носом —

TL;DR
Разбираемся, куда пропадают пакеты, при чём тут маршрутизация и как закончить войну за IP-адреса.


Кто‑то скажет — сделал из мухи слона! Да у меня каждый день такие приключения! Но для меня получился интересный кейс, и я лучше зафиксирую его для потомков.

Случилось страшное – из docker-контейнера недоступен удалённый хост. При этом с хост-машины никаких проблем с доступность нет – что надо пингуется, что пингуется, то роутится. Хост для определённости - 172.19.10.50.

WTF?!

Диагностика. Раунд первый

Для начала проверяем подсети, которые создал docker:
docker network inspect $(docker network ls -q) \
--format '{{.Name}}: {{range .IPAM.Config}}{{.Subnet}} {{end}}'

В моём случае обнаружились:
bridge:             172.17.0.0/16
traefik: 172.19.0.0/16
app-backend: 172.23.0.0/16
app-notifications: 172.29.0.0/16

В целом, картина уже ясна – видим пересечение сетей docker и внешнего хоста.

Для пущей убедительности проверяем маршрут в сетевом пространстве docker:
docker run --rm --network host \
nicolaka/netshoot \
ip route get 172.19.10.50

Бинго!

172.19.10.50 dev br-caf585119744 src 172.19.0.1

Cреда docker считает, что внешний сервер находится непосредственно в локальной сети.

Первое лечение. Неудачное

Перенёс сеть traefik в другой диапазон – 10.250.10.0/24, предварительно убедившись, что подсеть не используется где-то ещё.
networks:
traefik:
name: traefik
driver: bridge
ipam:
config:
- subnet: 10.250.10.0/24

Тут надо сказать, что app-backend и app-notifications используют общие external-сети. Поэтому сети создавались отдельно, а не средствами docker compose.
docker network create app-backend
docker network create app-notifications

Остановил использующие сеть проекты, удалил старые сети, поднял сети заново.
И… внешний сервер снова недоступен.

WTF?!

Диагностика. Раунд второй

Смотрим подсети ещё раз:
traefik:            10.250.10.0/24
app-backend: 172.18.0.0/16
app-notifications: 172.19.0.0/16

Постоянству docker можно позавидовать – не отпускает подсеть 172.19.0.0/16.

Лечение. Теперь окончательное

После повторного конфликта стало понятно: доверять docker автоматическое распределение адресов конкретно в этом окружении — идея так себе. Повторение мать заикания, как говорили классики, но: остановил использующие сеть проекты, удалил старые сети.

И поэтому сети создаём с явным указанием подсетей:
docker network create \
--driver bridge \
--subnet 10.250.11.0/24 \
app-backend

docker network create \
--driver bridge \
--subnet 10.250.12.0/24 \
app-notifications

Проверяем итоговую адресацию:
docker network inspect traefik app-backend app-notifications \
--format '{{.Name}}: {{range .IPAM.Config}}{{.Subnet}}{{end}}'

Получаем:
traefik:           10.250.10.0/24
app-backend: 10.250.11.0/24
app-notifications: 10.250.12.0/24

Проверяем доступность внешнего сервера:
docker run --rm \
--network container:notifications-php \
nicolaka/netshoot \
ping -c 3 172.19.10.50

И вот теперь всё заработало как надо!
Для кого-то это база, а для меня кирпичик опыта!

Полезное открытие: nicolaka/netshoot

У минималистичных docker-образов, таких как alpine, debian:slim часто отсутствуют даже базовые сетевые утилиты: ping, curl, nslookup, ip. Когда возникает необходимость разобраться с сетевыми проблемами, то можно установить пакеты в рабочий контейнер. Но удобнее использовать образ nicolaka/netshoot с целым арсеналом сетевых инструментов.

Но самое интересное — возможность подключиться к сетевому пространству уже работающего контейнера:
docker run --rm -it \
  --network container:notifications-php \
  nicolaka/netshoot

Теперь мы находимся в отдельном диагностическом контейнере, но используем сетевые интерфейсы, IP-адреса и маршруты notifications-php.

При этом исходный контейнер не пересобирается, а после выхода диагностический контейнер автоматически удалится благодаря --rm.

Отличная DevOps-выручалочка!

#docker #dockercompose #networking #devops #backend #разработка
👍3
Тысяча и одна ошибка

— Не будь как Шазерезада — экономь квоту! —

tl;dr
Одна ошибка может случиться тысячу раз, но в Bugsnag достаточно сообщить о ней один раз за 15 минут.

Кто о чём, а вшивый о багах.

Подключил я намедни проект Laravel к Bugsnag для сбора ошибок в коде, дабы не проспать чего апокалиптического. Но апокалипсис устроил сам... 😈

Проект ворочает довольно приличными объёмами данных и в случае катаклизма Bugsnag быстро бы оброс репортами.

Сказано - сделано!

Буквально за ночь доступные квоты были израсходованы! Источник проблем оказался внешний, но лимиты от этого не увеличились. 🤔

Чтобы решить подобную проблему на будущее было решено немного притормозить отправку репортов.

Основные требования к решению:
— Единый механизм дросселирования для любых типов исключений;
— Легкое добавление нового типа репорта без изменения существующей логики;
— Возможность самостоятельно определять, какие ошибки считать одинаковыми;
— Единая точка настройки ограничений, без размазывания логики по всему проекту;
— Максимально использовать штатные возможности Laravel и избнжать вендорлок сервиса мониторинга;
— Ограничение частоты репортов не должно влиять на выполнение бизнес-логики или обработку самих исключений;

1. Готовим максимально простой интерфейс
interface ThrottledReport
{
public const int THROTTLE_MINUTES = 15;
public function throttleKey(): string;
public function throttleMinutes(): int;
}

Основная идея - это throttleKey. Именно он определяет, какие ошибки считать одинаковыми. А throttleMinutes - определяет на сколько притормозить отправку повторяющихся репортов.

2. Добавляем trait с реализацией таймера по умолчанию
trait HasReportThrottle
{
public function throttleMinutes(): int
{
return static::THROTTLE_MINUTES;
}
}


3. Далее подключаем обработку эксепшена в bootstrap
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->throttle(function (Throwable $e) {
if ($e instanceof ThrottledReport) {
return Limit::perMinutes($e->throttleMinutes(), 1)
->by($e::class . ':' . $e->throttleKey());
}

return null; // остальные исключения без ограничений
});
})

Тут есть важный нюанс: троттлинг работает на уровне Laravel Exception Handler, поэтому подавленные репорты не попадут не только в Bugsnag, но и в стандартные логи приложения. Но для моего решения — это приемлемо.

И ещё момент — важно учесть, что лимит хранится в кэше, поэтому важно иметь настроенный механизм кэширования. Например, redis.

4. Создаём своё исключение
final class DoNotSpamException extends RuntimeException implements ThrottledReport
{
use HasReportThrottle;

public const int THROTTLE_MINUTES = 60;

public function throttleKey(): string
{
return 'my-unique-key-for-error';
}
}


5. И используем
report(new DoNotSpamException('Something went wrong'));


‼️ Но тут важно помнить — это решение уменьшает частоту сообщений об ошибках, а не частоту возникновения самих ошибок. О количестве и качестве ошибок заботимся сами! ☕️

#php #laravel #exception #throttling #backend #web #разработка
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
Как перенести дамп в 20Гб и не потерять ни бита

— Когда mysqldump уже не вывозит —


TL;DR
mydumper + myloader — интересная альтернатива классическому mysqldump для больших баз. Параллельная выгрузка, дробление таблиц, многопоточный импорт и возможность возобновить восстановление.

Для многих вынос дата-центров яндекса в начале октября обернулся неудобствами. Для кого-то финансовыми потерями и потерей данных. И мне досталось рикошетом и небольшим опытом...

В инфраструктуре яндекса лежала БД с весьма чувствительными данными. И как только сервис стал доступен встала задача извлечь данные из облака и скачать на флешку и положить под матрац и перебраться на резервную площадку. В моём случае — перенести какие-то жалкие 20Гб.

По старинке снял дамп через mysqldump и начал заливать на резервный стенд. Поставил заливку и пошёл кофе пить, а когда вернулся получил вполне ожидаемы результат — ноутбук ушёл поспать. Вторая и третья поптыки так же завершились неудачно, но причина была уже или в сети, или в сервере, или в магнитных бурях — ноутбуку спать не давал и теребонькал его за тачпад и клавиши — а аплоад рвался на разных этапах.

Двое из ларца – mydumper/myloader

— mydumper — снимает дамп в несколько потоков, разбивая таблицы на отдельные файлы. Полноценного автоматического продолжения прерванного дампа нет, но если данные гарантированно не изменялись, можно сохранить полностью выгруженные таблицы и отдельно доснять недостающие.
— myloader — параллельно восстанавливает базу и умеет продолжать прерванный импорт через --resume при наличии файла состояния.

Первая часть марлезонского балета

Снимаем дамп — синтаксис максимально похож на привычный mysqldump:

mydumper \
--host=<DB_HOST> \
--port=3306 \
--user=<DB_USER> \
--ask-password \
--ssl \
--ssl-mode=VERIFY_IDENTITY \
--ca=CA.pem \
--database=<DB_NAME> \
--skip-ddl-locks \
--no-backup-locks \
--sync-thread-lock-mode=LOCK_ALL \
--outputdir=./backup-sql \
--threads=4 \
--rows=100000 \
--trx-tables \
--verbose=3


Из интересных параметров:
--threads=4 — четыре параллельных потока выгрузки.
--rows=100000 — ориентир на 100 тысяч строк в одном фрагменте большой таблицы.
--trx-tables — оптимизация блокировок для транзакционных таблиц, например InnoDB.
--skip-ddl-locks — отключает дополнительные DDL-блокировки.
--no-backup-locks — запрещает использовать backup-locks.
--sync-thread-lock-mode=LOCK_ALL — синхронизирует потоки с помощью блокировок таблиц вместо глобального FLUSH TABLES WITH READ LOCK.

Облачный mysql оказался довольно ревнивым к своим привилегиям: сначала не дал BACKUP_ADMIN, потом отказал в FLUSH TABLES. Поэтому пришлось подбирать режимы синхронизации эмпирически.

Вторая часть марлезонского балета

Когда дамп наконец снялся, выяснилось, что myloader на macOS категорически не желает отключать SSL, которого на целевом сервере не было.

Попытки уговорить его через волшебный --ssl-mode=DISABLED закончились ничем. Оказалось, моя сборка использовала MariaDB Connector/C, с которым возникли проблемы при отключении SSL.

Пришлось доставать docker-образ с mysql-клиентом:

docker pull mydumper/mydumper:latest

И дальше дело пошло веселее. Для импорта через Docker команда получилась такой:

docker run --rm -it \
--entrypoint myloader \
-v "$(pwd)/backup:/backup" \
mydumper/mydumper:latest \
--host=<DB_HOST> \
--port=3306 \
--protocol=tcp \
--user=<DB_USER> \
--ask-password \
--ssl-mode=DISABLED \
--database=<DB_NAME> \
--directory=/backup \
--threads=4


В итоге база переехала, данные остались целы.

Осталось только купить флешку и положить её под матрац.

Так...на всякий случай.

#mysql #mydumper #myloader #devops
👍1