backend · intermediate · 6d
Сервис фич-флагов
Собери небольшой сервис флагов с правилами таргетинга, процентными раскатками и типизированным SDK, который вычисляет флаги на клиенте из закешированного набора правил.
Результат
API, отдающее версионированный набор правил флагов (ETag + 304), и типизированный SDK, где flagOn('x', user) вычисляется в процессе без I/O на вызов, с детерминированным солёным бакетингом и kill-switch, распространяющимся за секунды.
Этапы
0/5 · 0%- 01Модель набора правил + ревалидация ETag
Смоделируй флаги и правила таргетинга типизированной валидируемой схемой, затем отдай набор правил так, чтобы SDK могли дёшево его кешировать. У каждого флага — ключ, дефолтное значение, упорядоченные правила таргетинга (например, `email endsWith @company.com`, `country == DE`, `userId in allowlist`) и конфиг процентной раскатки. Схема валидируется zod при записи — некорректное правило отклоняется на создании, а не при оценке, когда оно бы молча неверно сработало. Отдавай GET /flags с сильным ETag (хеш версии сериализованного набора правил); SDK хранят ETag и ревалидируют через If-None-Match, получая 304 без тела при отсутствии изменений (только ~200 байт заголовков). Это делает частый поллинг дешёвым и позволяет тысячам экземпляров SDK ревалидироваться одновременно без пиков пропускной способности — правильный дефолт до стриминга.
Критерии готовности- GET /flags возвращает полный набор правил с сильным ETag; совпавший If-None-Match отдаёт 304 без тела и с корректными заголовками Cache-Control/ETag.
- Схема флагов и правил типизирована zod и валидируется при записи; некорректное правило (плохой оператор, отсутствующее поле) отклоняется с 400 и никогда не сохраняется.
Опирается наСамопроверка
Покажи отклонение некорректного правила при записи и ревалидацию 304 с заголовками. Senior-ревьюер проверяет, что ETag — хеш версии набора правил (не timestamp) и что валидация происходит до сохранения.
- 02Детерминированная солёная раскатка
Реализуй процентную раскатку так, чтобы один пользователь всегда получал один ответ, а включённая доля была ~X% при X% раскатки. Примитив — `hash(userId + flagKey) mod 100 < rolloutPercent`: примешивание flagKey критично — хеширование только userId помещает одни и те же 10% пользователей в первый дециль по каждому флагу, систематическое смещение, искажающее любой A/B-эксперимент, опирающийся на независимые назначения. Монотонное расширение корзин тоже важно: увеличение раскатки с 10% до 20% никогда не должно выключать уже включённых пользователей (ты расширяешь порог, а не перетасовываешь корзины). Докажи корректность на смоделированной популяции (например, 10k пользователей): при 10% включены ~1k, при 20% ~2k и исходные 1k всё ещё внутри, хи-квадрат тест показывает равномерное распределение корзин. Задокументируй, почему `Math.random()` на запрос здесь был бы неправильным.
Критерии готовности- hash(userId + flagKey) детерминированно раскладывает по корзинам; при X% раскатки ~X% из 10k смоделированных включены, один пользователь никогда не мерцает.
- Увеличение раскатки 10%→20%→50% сохраняет всех ранее включённых включёнными (монотонно), хи-квадрат или подсчёт корзин показывает равномерное распределение, а не кластеризацию.
Опирается наСамопроверка
Покажи гистограмму корзин на 10k пользователей и тест монотонного расширения (10%→20% сохраняет исходную когорту). Senior-ревьюер проверяет, что хеш солёный flagKey (не только userId), и просит объяснить A/B-смещение, если бы не был.
- 03Типизированный SDK с оценкой в процессе
Собери SDK, делающий оценку флага локальным типизированным вызовом без сети. `flagOn('flagKey', user)` вычисляется по кешированному набору правил в процессе: проход по правилам таргетинга в порядке приоритета, первое совпадение побеждает, переход к солёной процентной раскатке, затем к дефолту. Никакого fetch на вызов — набор правил закеширован через ETag и ревалидируется фоновым интервалом, так что горячий путь — чистая функция `ruleset × user → boolean` без I/O. Типобезопасность важна: SDK дженерик по ключу флага, так что `flagOn('nonexistent', ...)` — ошибка компиляции, а тип возврата сужается по флагу (boolean vs string vs JSON). Докажи бенчмарком: 100k оценок в процессе против наивного 'fetch набора на вызов' и покажи разницу латентности (микросекунды против миллисекунд). Задокументируй порядок оценки и что происходит, когда пользователь попадает под несколько правил.
Критерии готовности- SDK flagOn вычисляется в процессе из кешированного набора правил без сетевого I/O на вызов; правила таргетинга упорядочены по приоритету с детерминированным переходом к раскатке → дефолту.
- SDK типизирован, так что неизвестный ключ флага — ошибка компиляции; бенчмарк показывает оценку в процессе за микросекунды против миллисекунд за fetch-на-вызов, числа зафиксированы.
Опирается наСамопроверка
Покажи бенчмарк (100k оценок в процессе vs fetch-на-вызов) и ошибку компиляции для неизвестного ключа флага. Senior-ревьюер проверяет, что оценка упорядочена по приоритету с явным переходом и что на горячем пути нет I/O.
- 04SLO kill-switch и стриминг распространения
Сделай kill-switch реальным: выключение флага должно распространиться на каждый экземпляр SDK достаточно быстро, чтобы быть полезным при инциденте безопасности. При одном поллинге и TTL 60с выключенный флаг остаётся включённым до 60с в каждом SDK — окно воздействия, скрытый SLO сервиса флагов. Добавь стриминговый канал обновлений (SSE): сервер пушит событие 'доступна версия набора правил N+1', SDK немедленно ревалидируются, и распространение падает с TTL до ~секунд (латентность SSE + одна ревалидация). Оставь поллинг как фолбэк для клиентов, пропустивших стрим. Измерь оба пути: только с поллингом переключи флаг и зафиксируй наихудшую задержку распространения (до TTL); с SSE зафиксируй p50/p95 латентности push-to-eval. Задокументируй компромисс: SSE устраняет окно ценой постоянного соединения на экземпляр SDK (стоимость fan-out); поллинг без соединений, но с ограниченным окном устаревания. Укажи, когда расщепление оценки (часть SDK на старом наборе, часть на новом) приемлемо (постепенная раскатка) и когда нет (kill-switch безопасности).
Критерии готовности- SSE-канал пушит версии набора правил в SDK; переключение kill-switch распространяется на все подключённые SDK за секунды (p95 push-to-eval зафиксирован), с поллингом как фолбэком при обрыве SSE.
- SLO распространения задокументирован числами: наихудший случай только поллинга (TTL) против p95 SSE, приемлемость расщепления оценки указана для постепенной раскатки против kill-switch безопасности.
Опирается наСамопроверка
Переключи флаг и покажи латентность распространения только поллингом vs SSE (TTL vs секунды). Senior-ревьюер проверяет, что ты можешь назвать окно воздействия kill-switch, стоимость fan-out SSE и когда расщепление оценки неприемлемо.
- 05Нагрузи, наблюдай и отработай инцидент
Докажи под прод-подобной нагрузкой и сделай наблюдаемым, когда оно ведёт себя плохо. Нагрузи сервис флагов множеством экземпляров SDK (например, 50 конкурентных оценщиков, смесь ключей флагов, флаги 10% раскатки, переключения kill-switch на середине теста) и найди QPS, где узким местом становится ревалидация набора правил, а не оценка (304 дёшевы, но всё равно стоят round trip). Снимай RED-метрики (rate запросов, rate выборки набора правил, доля 304, rate оценок, число SSE-соединений) и трейс-span на выборку набора правил, чтобы собственная латентность системы флагов была видна в водопаде. Затем отработай инцидент: переключи правило таргетинга флага на середине нагрузки и наблюдай расщепление оценки — часть SDK на старом наборе, часть на новом — даёт несогласованные результаты flagOn для одного пользователя. Обнаружь это по метрикам (падение доли 304 + несогласованность оценки), смягчи (принудительная ревалидация / SSE push) и напиши пост-мортем на 5 строк, чья превенция — не 'снизь TTL до 1с'.
Критерии готовности- Устойчивый нагрузочный тест (≥50 конкурентных SDK, смесь флагов, переключение флага на середине) сообщает пропускную способность, долю 304, rate оценок и число SSE-соединений на дашборде с видимым трейс-span выборки набора правил.
- Ты воспроизвёл инцидент расщепления оценки (переключение правила на середине нагрузки), обнаружил его по метрикам, смягчил и написал пост-мортем с корневой причиной и превенцией, которая не 'снизь TTL до 1с'.
Опирается наСамопроверка
Вставь дашборд и пост-мортем. Senior-ревьюер проверяет, что узкое место отнесено к ревалидации набора правил (не оценке), трейс его локализовал, а превенция адресует распространение (SSE/поллинг), а не просто 'понизь TTL'.
Стартер
fallowlone/skein-projects
projects/feature-flags-service
- README.md
- src/flags.ts
- test/flags.test.ts
npx degit fallowlone/skein-projects/projects/feature-flags-service feature-flags-service Реализуй заглушки, затем гоняй тесты, пока не позеленеют: bun test
Форкни репозиторий и запушь свою работу — workflow grade прогонит тесты и статические проверки на твоих раннерах.
Рубрика
| Джуниор | Миддл | Сеньор | |
|---|---|---|---|
| Детерминированная раскатка | Процентная раскатка использует Math.random() на каждый запрос; один пользователь получает разные ответы на последовательных вызовах, а включённая доля лишь приближённа. | hash(userId + flagKey) mod 100 детерминированно раскладывает пользователя по корзинам: один пользователь всегда попадает в одну корзину, при X% включены ~X%, и монотонное расширение корзин означает, что увеличение раскатки никогда не выключает уже включённых пользователей. | Ты рассуждаешь о столкновениях корзин между флагами: если хешировать только userId (а не userId+flagKey), пользователи в корзине 0–10% — всегда одни и те же люди для каждого флага, систематическое смещение, искажающее результаты A/B. Примешивание flagKey разрывает корреляцию. Ты демонстрируешь это тестом распределения хи-квадрат на смоделированной популяции пользователей. |
| Кеширование набора правил и латентность распространения | SDK запрашивает полный набор правил из API при каждом вызове оценки; высоконагруженный сервис добавляет сетевой round trip к каждому запросу. | Набор правил отдаётся с ETag; SDK-клиенты кешируют его локально и ревалидируют через If-None-Match, получая 304 (без передачи тела) при отсутствии изменений — оценка в процессе без сетевого I/O на каждый вызов. | Ты рассуждаешь о латентности распространения kill-switch: TTL кеша 60с означает, что флаг, выключенный при инциденте безопасности, остаётся включённым до 60с в каждом SDK. Стриминг (SSE push) устраняет это окно, но добавляет постоянное соединение на каждый экземпляр SDK. Ты документируешь выбранный компромисс и наихудшую задержку распространения при своём интервале поллинга. |
| Согласованность оценки и корректность таргетинга | Правила таргетинга применяются в произвольном порядке; пользователь, попадающий под несколько правил, получает недетерминированный результат в зависимости от порядка оценки. | Правила оцениваются в порядке приоритета с явным переходом к процентной раскатке; одна и та же модель правил и один пользователь всегда дают один boolean, а схема типизирована и валидируется, так что некорректное правило отклоняется при записи, а не при оценке. | Ты рассматриваешь окно устаревшего набора правил: SDK-клиент, оценивающий кешированный набор правил, пока сервер уже изменил правило, производит расщепление оценки — одни пользователи видят старое поведение, другие — новое. Ты документируешь, когда это приемлемо (постепенная раскатка) и когда нет (kill-switch безопасности, требующий распространения за секунды), и показываешь, как стриминговый канал закрывает разрыв. |
Эталонный разбор (спойлер)
Почему hash(userId + flagKey): хеширование только userId создаёт систематическую корреляцию корзин — одни и те же 10% пользователей всегда попадают в первый дециль по каждому флагу. Это смещает любой A/B-эксперимент, опирающийся на независимые назначения лечения. Примешивание flagKey разрывает корреляцию корзин между флагами; примешивание experiment-id разрывает её между перезапусками одного флага.
Латентность распространения kill-switch — скрытый SLO: сервис флагов обычно второстепенен, пока инцидент безопасности не требует немедленного выключения чего-либо. SDK на основе поллинга добавляют до одного TTL воздействия после переключения kill-switch. SSE стриминг устраняет это окно ценой постоянного соединения на экземпляр SDK — компромисс: количество соединений (fan-out) против времени распространения.
ETag-кеширование для раздачи набора правил: отдача версионного ETag с набором правил и принятие If-None-Match означает, что неизменённые наборы правил передают только 200 байт заголовков, а не полный payload. Это делает частый поллинг дешёвым и позволяет многим экземплярам SDK ревалидироваться одновременно без пиков пропускной способности — правильный дефолт для SDK на основе поллинга до добавления стриминга.
Сделай по-сеньорски
- Добавь назначение на эксперимент по процентам с липким бакетингом, чтобы пользователь оставался в том же варианте даже при перезапуске эксперимента — и докажи тестом распределения.
- Добавь пререквизиты флагов (флаг B вычисляется только если флаг A включён) и докажи отсутствие циклов в DAG с short-circuit оценкой.
- Добавь аудит-логирование и диффинг истории флагов, чтобы каждое изменение правила атрибутировалось и было обратимо, и покажи откат, восстанавливающий предыдущую версию набора правил.