Махровый fullstack
16 subscribers
66 photos
27 links
Личный блог @nornik:
- Бэкенд, фронтенд и случайные мысли - в произвольных сочетаниях.
- Иногда литература, иногда всё, что взбредёт в голову.
Download Telegram
Читальня. Питер В. Бретт – Трон черепов

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


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

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

К началу 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