<?xml version="1.0" encoding="utf-8" ?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:tt="http://teletype.in/" xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/"><title>Oleg Mifle</title><author><name>Oleg Mifle</name></author><id>https://teletype.in/atom/mifleo</id><link rel="self" type="application/atom+xml" href="https://teletype.in/atom/mifleo?offset=0"></link><link rel="alternate" type="text/html" href="https://teletype.in/@mifleo?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=mifleo"></link><link rel="next" type="application/rss+xml" href="https://teletype.in/atom/mifleo?offset=10"></link><link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></link><updated>2026-05-22T12:19:24.610Z</updated><entry><id>mifleo:system-design-courier-location</id><link rel="alternate" type="text/html" href="https://teletype.in/@mifleo/system-design-courier-location?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=mifleo"></link><title>Разбор задачи system design - передача местоположения курьеров.</title><published>2025-11-04T08:16:32.281Z</published><updated>2025-11-04T08:16:32.281Z</updated><summary type="html">Привет. В комментариях к посту с историями про собесы была просьба разобрать задачу. Выполняю)</summary><content type="html">
  &lt;p id=&quot;AnNa&quot;&gt;Привет. В комментариях к &lt;a href=&quot;https://t.me/ask_for_oleg/108&quot; target=&quot;_blank&quot;&gt;посту&lt;/a&gt; с историями про собесы была просьба разобрать задачу. Выполняю)&lt;/p&gt;
  &lt;blockquote id=&quot;VaPH&quot;&gt;Я попробовал восстановить так, как это было на самом собесе, ничего не добавляя постафктум. Решение не может отличается от того, как сделали бы вы, это нормально.&lt;/blockquote&gt;
  &lt;p id=&quot;jTI5&quot;&gt;Задача была поставлена следующим образом (с учётом собранных мною требований):&lt;/p&gt;
  &lt;p id=&quot;BNTH&quot;&gt;Представьте, что вы проектируете бэкенд для клиентского приложения доставки. Пользователь открывает экран заказа и хочет увидеть, где сейчас находится его курьер.&lt;/p&gt;
  &lt;p id=&quot;LPSI&quot;&gt;На вход у нас есть два внешних API, на которые мы не можем повлиять:&lt;/p&gt;
  &lt;ol id=&quot;z0xq&quot;&gt;
    &lt;li id=&quot;hDJH&quot;&gt;Order API — по order_id возвращает данные по заказу, в т.ч. и courier_id. Среднее время ответа — 1 секунда.&lt;/li&gt;
    &lt;li id=&quot;vRwy&quot;&gt;Courier API — по courier_id возвращает текущие координаты. Среднее время ответа — ещё 1 секунда.&lt;/li&gt;
  &lt;/ol&gt;
  &lt;p id=&quot;m22G&quot;&gt;Наш сервис должен принимать запрос от клиентского приложения вида&lt;/p&gt;
  &lt;pre id=&quot;APGC&quot;&gt;GET /orders/{order_id}/location&lt;/pre&gt;
  &lt;p id=&quot;4kOP&quot;&gt;и отвечать координатами курьера: широта и долгота.&lt;/p&gt;
  &lt;p id=&quot;dflo&quot;&gt;Жёсткое требование:&lt;/p&gt;
  &lt;p id=&quot;Ocvx&quot;&gt;время ответа сервиса не должно превышать&lt;strong&gt; 300 мс&lt;/strong&gt;,&lt;/p&gt;
  &lt;p id=&quot;YAMs&quot;&gt;даже при высокой нагрузке и географически распределённой системе.&lt;/p&gt;
  &lt;p id=&quot;Kzvj&quot;&gt;Дополнительные условия:&lt;/p&gt;
  &lt;p id=&quot;eU8Z&quot;&gt;- Мы не знаем список заказов заранее — о заказе узнаём, когда клиент впервые запрашивает его позицию.&lt;/p&gt;
  &lt;p id=&quot;k0fd&quot;&gt;- Внешние API трогать или ускорять нельзя.&lt;/p&gt;
  &lt;p id=&quot;ea0D&quot;&gt;- Сервис должен быть масштабируемым, отказоустойчивым и геораспределённым. 🐱&lt;/p&gt;
  &lt;p id=&quot;XdKs&quot;&gt;То есть нам нужно уложиться в SLA 0.3 секунды, хотя единственное место, где есть нужные данные, отвечает ~2 секунды. 😨&lt;/p&gt;
  &lt;p id=&quot;hF00&quot;&gt;Как это сделать? Чистая архитектура, никаких костылей.&lt;/p&gt;
  &lt;p id=&quot;O2aG&quot;&gt;Чтобы уложиться в SLA по времени отклика и при этом не зависеть от скорости внешних API, система строится вокруг нескольких ключевых принципов:&lt;/p&gt;
  &lt;h3 id=&quot;Y10T&quot;&gt;1️⃣ Асинхронное наполнение данных (warm-up).&lt;/h3&gt;
  &lt;p id=&quot;CgBc&quot;&gt;Первый запрос по заказу не вызывает цепочку из двух медленных API. Вместо этого сервис мгновенно возвращает location = null, а фоновый воркер начинает процесс получения данных:&lt;/p&gt;
  &lt;ul id=&quot;Fmsu&quot;&gt;
    &lt;li id=&quot;gCtt&quot;&gt;обращается к Order API, чтобы получить courier_id;&lt;/li&gt;
    &lt;li id=&quot;Qlid&quot;&gt;по этому courier_id запрашивает координаты в Courier API;&lt;/li&gt;
    &lt;li id=&quot;m5PF&quot;&gt;сохраняет результат в Cassandra.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;Whzy&quot;&gt;После этого все последующие запросы по данному заказу читают данные напрямую из базы.&lt;/p&gt;
  &lt;h3 id=&quot;ODb4&quot;&gt;2️⃣ Хранение в Cassandra без промежуточных кэшей.&lt;/h3&gt;
  &lt;p id=&quot;gsnL&quot;&gt;Сервис не использует локальные in-memory или Redis-кэши. Все данные хранятся централизованно в Apache Cassandra, которая обеспечивает:&lt;/p&gt;
  &lt;ul id=&quot;4Aie&quot;&gt;
    &lt;li id=&quot;TIYB&quot;&gt;быструю запись (до десятков тысяч upsert-ов в секунду на ноду),&lt;/li&gt;
    &lt;li id=&quot;AecH&quot;&gt;горизонтальное масштабирование без единой точки отказа,&lt;/li&gt;
    &lt;li id=&quot;NKxa&quot;&gt;репликацию между регионами.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;b4zG&quot;&gt;Используется стратегия NetworkTopologyStrategy с ReplicationFactor = 3 на каждый регион и уровень согласованности LOCAL_QUORUM — это позволяет читать и писать данные локально, не дожидаясь удалённых DC.&lt;/p&gt;
  &lt;h3 id=&quot;cdYI&quot;&gt;Почему Cassandra, а не Redis?&lt;/h3&gt;
  &lt;p id=&quot;RyD5&quot;&gt;При выборе хранилища ключевым фактором стали характер нагрузки и требования к надёжности.&lt;/p&gt;
  &lt;ul id=&quot;Ir6K&quot;&gt;
    &lt;li id=&quot;gBni&quot;&gt;Профиль нагрузки — write-heavy&lt;br /&gt;Каждый активный курьер обновляет координаты каждые 2–5 секунд. При десятках тысяч активных курьеров это десятки тысяч UPSERT-операций в секунду.&lt;br /&gt;Такая нагрузка типична не для кэш-систем, а для постоянных write-heavy хранилищ, оптимизированных под линейное масштабирование и дешёвые вставки.&lt;/li&gt;
    &lt;li id=&quot;QgH3&quot;&gt;Геораспределённость и отказоустойчивость&lt;br /&gt;Redis, даже в кластерном режиме, не предоставляет аналогичной модели актив-актив без внешней прослойки (обычно нужен либо CRDT-слой, либо дорогая мульти-мастер-репликация).&lt;/li&gt;
    &lt;li id=&quot;L1AF&quot;&gt;Консистентность и долговечность&lt;br /&gt;Для геопозиции допустима eventual consistency, но при этом важно, чтобы данные не терялись.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;Tdql&quot;&gt;Cassandra гарантирует сохранность записей после достижения кворума реплик, даже если узлы падают.&lt;/p&gt;
  &lt;p id=&quot;LCj5&quot;&gt;Redis, напротив, ориентирован на скорость чтения и простоту, а не на долговечность — потеря узла или выключение snapshot-механизма может привести к частичной потере данных.&lt;/p&gt;
  &lt;h3 id=&quot;VZc7&quot;&gt;3️⃣ Разделение API и воркеров&lt;/h3&gt;
  &lt;ul id=&quot;pFcj&quot;&gt;
    &lt;li id=&quot;jnDB&quot;&gt;API-слой отвечает только за быстрые запросы клиента и формирование ответов (200 OK с координатами или warmingUp:true).&lt;/li&gt;
    &lt;li id=&quot;MvTu&quot;&gt;Воркеры асинхронно взаимодействуют с внешними сервисами, обновляют данные в Cassandra и управляют фоновыми циклами обновления координат курьеров.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;PkQ7&quot;&gt;Один воркер может обслуживать сотни активных курьеров, выполняя обновления каждые 2–5 секунд.&lt;/p&gt;
  &lt;h3 id=&quot;jFvL&quot;&gt;4️⃣ Схема хранения данных.&lt;/h3&gt;
  &lt;p id=&quot;5bgY&quot;&gt;Используются две основные таблицы:&lt;/p&gt;
  &lt;pre id=&quot;UjUV&quot;&gt;CREATE TABLE order_to_courier (
  order_id  text PRIMARY KEY,
  courier_id  text,
  assigned_ts timestamp
);
  
CREATE TABLE courier_location_current (
  courier_id  text PRIMARY KEY,
  lat double,
  lon double,
  provider_ts timestamp,
  updated_ts  timestamp
);&lt;/pre&gt;
  &lt;p id=&quot;i0EC&quot;&gt;order_to_courier заполняется один раз на заказ, courier_location_current обновляется каждые несколько секунд.&lt;/p&gt;
  &lt;h2 id=&quot;7IsV&quot;&gt;&lt;strong&gt;Поведение системы&lt;/strong&gt;&lt;/h2&gt;
  &lt;h3 id=&quot;bTcS&quot;&gt;1️⃣ Клиент делает запрос GET /orders/{orderId}/location.&lt;/h3&gt;
  &lt;p id=&quot;e1lJ&quot;&gt;Сервис обращается к Cassandra:&lt;/p&gt;
  &lt;ul id=&quot;FTR1&quot;&gt;
    &lt;li id=&quot;VyvC&quot;&gt;если данные есть — возвращает координаты;&lt;/li&gt;
    &lt;li id=&quot;N1el&quot;&gt;если нет — отдаёт null и ставит задачу в очередь.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;h3 id=&quot;hqx6&quot;&gt;2️⃣ Воркеры обрабатывают очередь:&lt;/h3&gt;
  &lt;ul id=&quot;4iFG&quot;&gt;
    &lt;li id=&quot;HBHr&quot;&gt;получают courier_id и координаты;&lt;/li&gt;
    &lt;li id=&quot;G1XH&quot;&gt;обновляют таблицы order_to_courier и courier_location_current.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;h3 id=&quot;JZkU&quot;&gt;3️⃣ Следующие запросы обслуживаются из Cassandra — без дополнительных внешних вызовов.&lt;/h3&gt;
  &lt;p id=&quot;uJRn&quot;&gt;&lt;/p&gt;
  &lt;figure id=&quot;srh6&quot; class=&quot;m_retina&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/77/b6/77b65737-6a7f-4e4e-bf02-32af2aa5a548.png&quot; width=&quot;581.5&quot; /&gt;
    &lt;figcaption&gt;Схема приложения&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;figure id=&quot;7Twv&quot; class=&quot;m_retina&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/ee/0f/ee0f347d-c506-4bcb-b71c-2e45546dd7bf.png&quot; width=&quot;773.5&quot; /&gt;
    &lt;figcaption&gt;Течение данных&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;nTiI&quot;&gt;Что бы вы сделали иначе?&lt;/p&gt;
  &lt;p id=&quot;yxoy&quot;&gt;⸻&lt;/p&gt;
  &lt;p id=&quot;LheR&quot;&gt;Давайте оставаться на связи  - https://t.me/ask_for_oleg ☄️&lt;/p&gt;

</content></entry><entry><id>mifleo:my-network</id><link rel="alternate" type="text/html" href="https://teletype.in/@mifleo/my-network?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=mifleo"></link><title>Строим домашнюю сеть</title><published>2025-08-27T13:47:28.262Z</published><updated>2025-08-27T13:59:04.647Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img4.teletype.in/files/32/1f/321f9687-069a-4489-bb27-d6527f527142.png"></media:thumbnail><summary type="html">&lt;img src=&quot;https://img2.teletype.in/files/93/df/93df486a-766c-4e7b-afca-9fe44bd61ba6.png&quot;&gt;Всем привет. Сегодня пусть будет не про архитектуру. Точнее, и про архитектуру, но не ту.</summary><content type="html">
  &lt;figure id=&quot;Qxhk&quot; class=&quot;m_retina&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/93/df/93df486a-766c-4e7b-afca-9fe44bd61ba6.png&quot; width=&quot;768&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;V6s9&quot;&gt;Всем привет. Сегодня пусть будет не про архитектуру. Точнее, и про архитектуру, но не ту.&lt;/p&gt;
  &lt;p id=&quot;0ZKk&quot;&gt;Год назад мы с семьёй переехали в свою квартиру. Получился ещё тот квест, но это совсем другая история. Обустраиваемся тут, заканчиваем ремонт. Мне, как удалёнщику со стажем, важно сделать себе рабочее место и провести интернет хорошего качества. В принципе, первое, что появилось в доме, - это роутер. Причём это Huawei от Ростелекома. Не совсем отвратный, но и не топ. Причина появления именно такого роутера в том, что подключение происходит по оптике, а не витой паре. Мой прежний роутер такое не умел. В целом интернет работал, скорость была нормальная, всё в порядке. Так я думал, пока мы окончательно не перебрались домой. Не знаю, из чего у нас сделаны стены, но сигнал роутера нормального качества есть только в прихожей, где он и находится. Ну или в зоне прямой видимости. В других комнатах качество сигнала сильно деградирует.&lt;/p&gt;
  &lt;p id=&quot;Lt38&quot;&gt;Я скачал тулзу для измерения уровня сигнала и походил по комнатам. В спальне уровень сигнала 30%, на рабочем месте - всего 20%. При этом если смещаюсь на 30 сантиметров назад и встаю напротив двери, откуда роутер почти видно, сигнал уже 85%. То есть до рабочего места сигналу надо преодолеть 5 стен, и это его почти полностью гасит. В старой квартире ситуация была почти такой же (по препятствиям), но там сигнал и качество деградировали не так сильно (по субъективной оценке, я тогда не замерял).&lt;/p&gt;
  &lt;p id=&quot;jfJ5&quot;&gt;Помучался я так пару недель и понял, что не готов терпеть, потому что проще переключиться на мобильный интернет и раздавать через телефон - скорость выше, сигнал стабильнее.&lt;/p&gt;
  &lt;h3 id=&quot;hDCr&quot;&gt;&lt;strong&gt;Попытка 1&lt;/strong&gt;&lt;/h3&gt;
  &lt;p id=&quot;SG4R&quot;&gt;Решил подойти к проблеме в лоб. Достал свой старый роутер (ASUS RT-AC68U), связал его с Huawei как ретранслятор. Asus разместил рядом с рабочим местом, назначил ему такой же SSID с постфиксом rep. Ожидаемо, качество сигнала стало отличным -роутер же рядом. Но вот интернет остался плохим. Asus мог работать как ретранслятор только в диапазоне 2,4 ГГц. К тому же у него мощнее покрытие, из-за чего сеть не терялась даже в ближайшей к Huawei комнате. Получалось комбо: сильный сигнал и слабый интернет. Так прошёл ещё месяц, после чего я перешёл к следующей итерации.&lt;/p&gt;
  &lt;h3 id=&quot;4pMV&quot;&gt;&lt;strong&gt;Попытка 2&lt;/strong&gt;&lt;/h3&gt;
  &lt;p id=&quot;46nJ&quot;&gt;Раз у Asus лучше покрытие, значит можно поставить его в удачное место и наслаждаться связью, которая ловит даже на парковке. Я соединил оба роутера витой парой, настроил у Asus SSID, а у Huawei вообще выключил Wi-Fi, чтобы не создавать интерференцию. Качество сигнала на рабочем месте поднялось до 30% - уже лучше. Но 5 ГГц деградировал до 2,4 ГГц и не выдавал больше 15 Мб/сек. В целом стало лучше, чем было, но на созвонах и лекциях картинка всё ещё фризилась. Да и загрузка контента часто подвисала. Так прошло ещё полгода.&lt;/p&gt;
  &lt;h3 id=&quot;WqnJ&quot;&gt;&lt;strong&gt;Попытка 3&lt;/strong&gt;&lt;/h3&gt;
  &lt;p id=&quot;Wv6K&quot;&gt;Всё-таки дискомфорт перевесил мою лень. Я решил поискать более системное решение вопроса и сдуть пыль с диплома инженера по сетям.&lt;/p&gt;
  &lt;p id=&quot;8c3b&quot;&gt;Какие варианты есть у современного мира?&lt;/p&gt;
  &lt;p id=&quot;kRrG&quot;&gt;Самый очевидный - проложить витую пару до середины квартиры. Тогда сигнал будет &lt;s&gt;всратый&lt;/s&gt; равномерный для всех. Решение хорошее, но, так как чистовая отделка уже сделана, а я не люблю висящие и валяющиеся провода, вариант был отложен до лучших времён.&lt;/p&gt;
  &lt;ol id=&quot;5BHy&quot;&gt;
    &lt;li id=&quot;2Wfn&quot;&gt;Powerline-адаптеры (интернет через розетку). По витой паре передаются электрические импульсы, их можно пустить и через электросеть. Но, во-первых, от топологии проводки будет зависеть, насколько деградирует сигнал, а во-вторых, у меня комнаты разделены по автоматам, и нет уверенности, что это вообще заведётся.&lt;/li&gt;
    &lt;li id=&quot;ctlK&quot;&gt;Использовать специализированный повторитель (усилитель). Я с этим мало работал, но почитал и понял, что это создаст ещё одну сеть, плюс съест скорость, и результат будет как в первой попытке.&lt;/li&gt;
    &lt;li id=&quot;jIDf&quot;&gt;Mesh-система. Это беспроводная ячеистая сеть (WMN) с бесшовным роумингом. Она может работать как по проводу, так и по воздуху. В отличие от обычных репитеров, здесь используется выделенный транспортный канал (Backhaul), чтобы отделить служебный трафик. Это позволяет сохранить производительность. При этом у меня всегда один SSID, а подключение происходит к ближайшему роутеру. Управлять всей сетью можно как единым целым из любой точки квартиры.&lt;/li&gt;
    &lt;li id=&quot;0mqB&quot;&gt;Можно было сделать два роутера с одним SSID и настроить расстояние, при котором роутер отменяет сигнал.&lt;/li&gt;
    &lt;li id=&quot;yq2K&quot;&gt;Использовать WDS. Но я почитал - слишком сложно, хотя и похоже на рабочий вариант. Всё равно будет резать скорость.&lt;/li&gt;
  &lt;/ol&gt;
  &lt;p id=&quot;Rd3n&quot;&gt;В итоге я остановился на Mesh-системе с Wireless Backhaul. Правда, мои старые роутеры меш не поддерживали, поэтому пришлось раскошелиться на лакшери Keenetic. Настройка очень простая: на ведомом роутере надо переключить тумблер режима, связать главное устройство с ведомым и всё. Дальше магия WMN на канальном и сетевом уровнях модели OSI.&lt;/p&gt;
  &lt;p id=&quot;nxVj&quot;&gt;В результате получилась топология:&lt;/p&gt;
  &lt;p id=&quot;lHRa&quot;&gt;Huawei (как адаптер PON-LAN) → Keenetic Voyager Pro KN-3510 → Keenetic Speedster KN-3013.&lt;/p&gt;
  &lt;h3 id=&quot;dNuS&quot;&gt;&lt;strong&gt;Итог&lt;/strong&gt;: &lt;/h3&gt;
  &lt;p id=&quot;fVBV&quot;&gt;один SSID для 2,4 и 5 ГГц на всех роутерах внутри сети. На рабочем месте сигнал 75% и скорость 200 Мб/сек. Потеря скорости составила ~55%. Не идеально, но лучшее из всего, что было. Из минусов - требуется перенастройка умных устройств.&lt;/p&gt;
  &lt;p id=&quot;oHZ3&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;fAac&quot;&gt;&lt;a href=&quot;https://t.me/ask_for_oleg&quot; target=&quot;_blank&quot;&gt;Давайте оставаться на связи!&lt;/a&gt;&lt;/p&gt;

</content></entry><entry><id>mifleo:feature-flags</id><link rel="alternate" type="text/html" href="https://teletype.in/@mifleo/feature-flags?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=mifleo"></link><title>🚩Какая польза от Feature Flags 🚩</title><published>2025-05-26T17:42:08.617Z</published><updated>2025-05-26T17:42:47.880Z</updated><summary type="html">Что самое мощное в этом инструменте? Включение какого-то функционала? Я уверен, что не только.</summary><content type="html">
  &lt;p id=&quot;r7hm&quot;&gt;Что самое мощное в этом инструменте? Включение какого-то функционала? Я уверен, что не только.&lt;/p&gt;
  &lt;p id=&quot;Hmlc&quot;&gt;Вообще, давно хочу раскрыть такую тему как Фича-флаги (или feature flags, feature toggles). Тема хоть и старая, но актуальная и интересная. Они бывают &lt;em&gt;статические&lt;/em&gt;, т.е. устанавливаются на этапе сборки проекта, и &lt;em&gt;динамические&lt;/em&gt;, это когда можно менять прямо в рантайме поведение приложения.&lt;/p&gt;
  &lt;p id=&quot;m6NJ&quot;&gt;Соответственно, реализация может быть&lt;/p&gt;
  &lt;p id=&quot;q2ar&quot;&gt;🧩 через конфиг (env, обычный файлик с конфигами)&lt;/p&gt;
  &lt;p id=&quot;PQwp&quot;&gt;🧩 через штатную базу данных проекта (обычно это реляционная СУБД)&lt;/p&gt;
  &lt;p id=&quot;4tph&quot;&gt;🧩 через in-memory DB (redis, memcache etc.)&lt;/p&gt;
  &lt;p id=&quot;Vofg&quot;&gt;🧩 через 3d-party приложение (LaunchDarkly, Flagsmith и т.д.). В Gitlab (правда в Premium) тоже можно, кстати.&lt;/p&gt;
  &lt;p id=&quot;3xjG&quot;&gt;🧩 своё решение 🧠 - тут уже кто на что способен.&lt;/p&gt;
  &lt;p id=&quot;c4C5&quot;&gt;Соответственно, статические это быстро и дёшево, но не гибко. Нужно включить - передеплоивай приложение. Ну это ладно, это пол беды. Чтобы закатить что-то обратно - тоже нужно передеплоить. Это уже больнее.&lt;/p&gt;
  &lt;p id=&quot;DhnM&quot;&gt;С динамическими этот процесс проще - поменял стейт флага, что-то включилось, поменял обратно - выключилось. И никаких передеплоев. Красота. Включить или выключить функционал сильно проще, чем запускать сборку, деплой и прочее. Особенно, если у вас целый пайплайн из процессов для того чтобы выкатить одну строчку. Особенно, если нужно отключить фичу.&lt;/p&gt;
  &lt;h2 id=&quot;kgNs&quot;&gt;🎢 &lt;em&gt;Плавный rollout&lt;/em&gt;.&lt;/h2&gt;
  &lt;p id=&quot;2p8a&quot;&gt;Это одно из моих самых любимых.&lt;/p&gt;
  &lt;p id=&quot;mS4X&quot;&gt;Фича флаги можно отлично применить для плавной раскатки. Включаем на процент пользователей, смотрим на графики: ошибки, загруженность ресурсов, алерты в Sentry. Если есть проблема она быстро себя проявит на небольшом числе пользователей, это поможет быстро её идентифицировать и пофиксить.&lt;/p&gt;
  &lt;p id=&quot;BWlV&quot;&gt;В коде можно что-то типа такого сделать&lt;/p&gt;
  &lt;pre id=&quot;L5SM&quot; data-lang=&quot;php&quot;&gt;&amp;lt;?php

final class FeatureRollout
{
    public function __construct(
        private readonly int $percentage // 0–100
    ) {
        if ($this-&amp;gt;percentage &amp;lt; 0 || $this-&amp;gt;percentage &amp;gt; 100) {
            throw new InvalidArgumentException(&amp;#x27;Percentage must be between 0 and 100.&amp;#x27;);
        }
    }

    public function isFeatureEnabledFor(string $userIdentifier): bool
    {
        $hash = crc32($userIdentifier);
        $bucket = $hash % 100;

        return $bucket &amp;lt; $this-&amp;gt;percentage;
    }
}

$rollout = new FeatureRollout(30); // включено для 30% пользователей

$userId = &amp;#x27;some-uuid&amp;#x27;;

if ($rollout-&amp;gt;isFeatureEnabledFor($userId)) {
    echo &amp;#x27;Показать новую фичу&amp;#x27;;
} else {
    echo &amp;#x27;Оставить старое поведение&amp;#x27;;
}&lt;/pre&gt;
  &lt;p id=&quot;fNfc&quot;&gt;тогда конфигурироваться должно и состояние флага, и процент раскатки.&lt;br /&gt;&lt;/p&gt;
  &lt;h2 id=&quot;6gkS&quot;&gt;По категориям&lt;/h2&gt;
  &lt;p id=&quot;zaUN&quot;&gt;🔥 &lt;strong&gt;Release Toggles&lt;/strong&gt; - позволяют интегрировать незавершённые функции в основную ветку кода, отключая их до готовности к релизу.&lt;br /&gt;&lt;br /&gt;Постоянно пользуюсь. Когда нужно делать крупные задачи, то разделить их на мелкие части и доставлять понемногу проще. Так не накапливается огромная куча непроревьювленного кода, который обрастает конфликтами. Минус такого подхода, что всё нужно будет перетестировать в конце.&lt;br /&gt;&lt;/p&gt;
  &lt;p id=&quot;AaDM&quot;&gt;🔥 &lt;strong&gt;Experiment Toggles&lt;/strong&gt; - используются для A/B-тестирования и оценки пользовательских реакций на различные варианты функциональности.&lt;/p&gt;
  &lt;p id=&quot;FQvh&quot;&gt;🔥 &lt;strong&gt;Ops Toggles&lt;/strong&gt; - предоставляют возможность оперативного включения или отключения функций в зависимости от условий эксплуатации или производительности системы.&lt;br /&gt;&lt;br /&gt;Ну тут всё понятно. Самый кайф.&lt;/p&gt;
  &lt;p id=&quot;6vkY&quot;&gt;🔥 &lt;strong&gt;Permissioning Toggles&lt;/strong&gt; - ограничивают доступ к определённым функциям для конкретных групп пользователей, например, только для премиум-клиентов.&lt;/p&gt;
  &lt;h2 id=&quot;KPSF&quot;&gt;&lt;br /&gt;По стратегии использования&lt;/h2&gt;
  &lt;p id=&quot;ppaT&quot;&gt;🧩 Краткосрочные - для временного использования и должны быть удалены после завершения эксперимента (расктаки).&lt;/p&gt;
  &lt;p id=&quot;PLe4&quot;&gt;🧩 Долгосрочные - требуют постоянного управления и документирования. Они с нами надолго.&lt;br /&gt;&lt;/p&gt;
  &lt;h2 id=&quot;19f7&quot;&gt;Как обычно, у любого инструмента есть своя цена&lt;/h2&gt;
  &lt;p id=&quot;2bjV&quot;&gt;⚠️ Сложность тестирования - yвуличие множества флагов может привести к комбинаторному взрыву вариантов для тестирования. Необходимо тщательно планировать стратегии тестирования.&lt;/p&gt;
  &lt;p id=&quot;Vdg3&quot;&gt;⚠️ Технический долг - неудалённые или устаревшие флаги увеличивают сложность системы и могут привести к ошибкам. Важно регулярно проводить ревизию и удалять неиспользуемые флаги. В одной компании у нас через какое-то время после раскатки приходил бот и ставил задачу на выпиливание флага.&lt;/p&gt;
  &lt;p id=&quot;iE4A&quot;&gt;⚠️ Если используем внешнюю систему, то нужно позаботится о кешировании флагом и об инвалидации кеша. Либо как хранить флаги поближе к коду. Ходить по http - не вариант.&lt;br /&gt;&lt;br /&gt;-&lt;br /&gt;&lt;br /&gt;&lt;a href=&quot;https://t.me/ask_for_oleg&quot; target=&quot;_blank&quot;&gt;Давайте оставаться на связи&lt;/a&gt; &lt;/p&gt;

</content></entry><entry><id>mifleo:nwWFwyYdM37</id><link rel="alternate" type="text/html" href="https://teletype.in/@mifleo/nwWFwyYdM37?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=mifleo"></link><title>Мьютексные блокировки в php фреймворках</title><published>2025-04-09T09:51:00.018Z</published><updated>2025-05-13T06:04:42.221Z</updated><summary type="html">Решил я разобраться как устроены мьютексы в различных фреймворках. А началось всё с того, что произошёл у меня спор с коллегой по поводу того, как отработает код в Yii2:</summary><content type="html">
  &lt;p id=&quot;T2iB&quot;&gt;Решил я разобраться как устроены мьютексы в различных фреймворках. А началось всё с того, что произошёл у меня спор с коллегой по поводу того, как отработает код в Yii2:&lt;/p&gt;
  &lt;pre id=&quot;Lxn8&quot;&gt;$mutex-&amp;gt;acquire(&amp;#x27;my-lock&amp;#x27;, 10);
&lt;/pre&gt;
  &lt;p id=&quot;pFXy&quot;&gt;Поскольку в нашем проекте мьютексы хранятся в Redis, я был уверен, что число &lt;code&gt;10&lt;/code&gt; в этом контексте обозначает TTL (Time To Live) блокировки, то есть время, через которое блокировка будет автоматически снята. Это предположение основывалось на том, что в команде Redis для установки блокировки используется следующий синтаксис:&lt;/p&gt;
  &lt;pre id=&quot;a2bl&quot;&gt;SET lock_name unique_value NX PX 5000
&lt;/pre&gt;
  &lt;p id=&quot;HUgH&quot;&gt;Здесь создаётся блокировка с именем &lt;code&gt;lock_name&lt;/code&gt;, где &amp;quot;владельцем&amp;quot; является &lt;code&gt;unique_value&lt;/code&gt; (снять блокировку можно только, указав это значение) на 5000 миллисекунд, если такого ключа у &amp;quot;владельца&amp;quot; нет.&lt;/p&gt;
  &lt;p id=&quot;NqXx&quot;&gt;Кроме того, мой опыт работы с Symfony подсказывал, что при создании мьютекса можно передать только TTL. Однако, как оказалось, в случае с Yii2 я ошибался. Изучив код, я понял, что параметр &lt;code&gt;10&lt;/code&gt; в методе &lt;code&gt;acquire&lt;/code&gt; обозначает не TTL, а таймаут — время, в течение которого процесс будет ожидать освобождения мьютекса.&lt;/p&gt;
  &lt;p id=&quot;N7xv&quot;&gt;&lt;strong&gt;Как устроены мьютексы в PHP-фреймворках?&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;CJFu&quot;&gt;Рассмотрим интерфейсы и их реализацию в различных фреймворках, поскольку на глубоком уровне они работают схожим образом, особенно при использовании Redis.&lt;/p&gt;
  &lt;h3 id=&quot;Yii2&quot;&gt;Yii2&lt;/h3&gt;
  &lt;pre id=&quot;Tj0v&quot;&gt;if (!$mutex-&amp;gt;acquire($mutexName)) {
    return;
}
try {
    // критическая секция
} finally {
    $mutex-&amp;gt;release();
}
&lt;/pre&gt;
  &lt;p id=&quot;B4bK&quot;&gt;При таком использовании метод &lt;code&gt;acquire&lt;/code&gt; сразу вернёт &lt;code&gt;false&lt;/code&gt;, если блокировка не может быть установлена, и ожидание не произойдёт. Если вызвать &lt;code&gt;$mutex-&amp;gt;acquire($mutexName, 10)&lt;/code&gt;, процесс будет ожидать до 10 секунд, пока мьютекс не освободится. Однако TTL также можно задать.&lt;/p&gt;
  &lt;h3 id=&quot;Yii3&quot;&gt;Yii3&lt;/h3&gt;
  &lt;p id=&quot;mQzD&quot;&gt;В Yii3 добавлена обёртка &lt;code&gt;\Yiisoft\Mutex\Synchronizer&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;nf5H&quot;&gt;$synchronizer = new Synchronizer($mutexFactory);

$result = $synchronizer-&amp;gt;execute(&amp;#x27;my-lock&amp;#x27;, function () {
    // критическая секция
}, 10); // таймаут 10 секунд
&lt;/pre&gt;
  &lt;p id=&quot;J9Rs&quot;&gt;Здесь освобождение мьютекса происходит внутри &lt;code&gt;Synchronizer&lt;/code&gt;, что удобнее и безопаснее. Это избавляет от необходимости писать собственные обёртки. Для тех, кто предпочитает полный контроль, мьютексы можно использовать так же, как в Yii2.&lt;/p&gt;
  &lt;h3 id=&quot;Laravel&quot;&gt;Laravel&lt;/h3&gt;
  &lt;p id=&quot;QD1E&quot;&gt;В Laravel мьютексы являются частью пакета кеширования:&lt;/p&gt;
  &lt;pre id=&quot;kkFc&quot;&gt;Cache::lock(&amp;#x27;my-critical-section&amp;#x27;, 10)-&amp;gt;block(5, function () {
    // критическая секция
});
&lt;/pre&gt;
  &lt;p id=&quot;Bf7c&quot;&gt;Здесь создаётся мьютекс с TTL 10 секунд и таймаутом 5 секунд. Освобождение происходит автоматически. Можно также выполнить всё вручную:&lt;/p&gt;
  &lt;pre id=&quot;m17u&quot;&gt;$lock = Cache::lock(&amp;#x27;my-task&amp;#x27;, 10);

if ($lock-&amp;gt;get()) {
    try {
        // критическая секция
    } finally {
        $lock-&amp;gt;release();
    }
}
&lt;/pre&gt;
  &lt;p id=&quot;EZ8t&quot;&gt;Мьютексные блокировки часто применяются для предотвращения параллельного запуска фоновых задач. В Laravel существует middleware &lt;code&gt;WithoutOverlapping&lt;/code&gt; в пакете очередей, который использует тот же мьютекс, предоставляя удобную обёртку для типовых задач.&lt;/p&gt;
  &lt;h3 id=&quot;Symfony&quot;&gt;Symfony&lt;/h3&gt;
  &lt;p id=&quot;jkPp&quot;&gt;Symfony предоставляет, пожалуй, самый гибкий и полный пакет для работы с мьютексами:&lt;/p&gt;
  &lt;pre id=&quot;byfb&quot;&gt;$lock = $factory-&amp;gt;createLock(&amp;#x27;pdf-creation&amp;#x27;, ttl: 30);
if (!$lock-&amp;gt;acquire()) {
    return;
}
try {
    // выполнение задачи менее 30 секунд
} finally {
    $lock-&amp;gt;release();
}
&lt;/pre&gt;
  &lt;p id=&quot;m31U&quot;&gt;Здесь блокировка создаётся в неблокирующем режиме. Можно создать и в блокирующем, используя &lt;code&gt;acquire(true)&lt;/code&gt;. Также можно обновлять TTL мьютекса через &lt;code&gt;$lock-&amp;gt;refresh()&lt;/code&gt;, если становится понятно, что первоначального TTL недостаточно.&lt;/p&gt;
  &lt;p id=&quot;6foT&quot;&gt;Особое внимание в этом пакете привлекают блокировки с разделением доступа. Поддержка этой функции зависит от используемого адаптера; для Redis она доступна. В то время как &lt;code&gt;acquire&lt;/code&gt; захватывает исключительную блокировку, после которой никакой другой процесс не сможет ни читать, ни писать в защищённый ресурс, &lt;code&gt;acquireRead&lt;/code&gt; (разделяемая блокировка) позволяет нескольким процессам захватывать блокировку в режиме чтения, пока кто-то не выполнит исключительную блокировку.&lt;/p&gt;
  &lt;p id=&quot;W23W&quot;&gt;&lt;strong&gt;Заключение&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;BVdy&quot;&gt;Подобные библиотеки делают использование мьютексов простым и удобным, скрывая тонкости реализации. Например, последовательное выполнение команд &lt;code&gt;GET&lt;/code&gt; (проверка наличия ключа) и &lt;code&gt;DEL&lt;/code&gt; (удаление ключа) для снятия мьютекса не гарантирует атомарность операции, поскольку между выполнением этих команд значение ключа может измениться другим процессом. Поэтому требуется выполнение Lua-скрипта:&lt;/p&gt;
  &lt;pre id=&quot;vFl0&quot;&gt;if redis.call(&amp;quot;GET&amp;quot;, KEYS[1]) == ARGV[1] then
    return redis.call(&amp;quot;DEL&amp;quot;, KEYS[1])
else
    return 0
end
&lt;/pre&gt;
  &lt;p id=&quot;D4GH&quot;&gt;Этот скрипт обеспечивает атомарность операции удаления мьютекса, проверяя значение ключа перед удалением.&lt;/p&gt;
  &lt;p id=&quot;ecaQ&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;4Q6N&quot;&gt;Подписывайся на &lt;a href=&quot;http://t.me/ask_for_oleg&quot; target=&quot;_blank&quot;&gt;канал 🤘&lt;/a&gt;&lt;/p&gt;

</content></entry></feed>