golang

Froggle — фича-флаги без боли

  • среда, 29 апреля 2026 г. в 00:00:20
https://habr.com/ru/articles/1029024/

Бороздя просторы космоса Хабра, рабочих репозиториев и не только, в сегменте Java разработчиков и других JVM динозавров, была обнаружена извественная проблема, большинство фич закрыты фича-флагами в виде простых переменных в коде (иногда чересчур замедруенными). И в этом хаосе родилась идея просто менеджера флагов для разных приложений.

В мире кубов и контейнеров JVM приложения чувствуют себя немного странного когда речь заходит о вопросах: кто сожрал все ресурсы в кластере? или как же мне вывернуть приложение чтобы не рестартить его? Со вторым вопросом предлагаю ознакомится ближе.
Механизм рефреша конфигураций есть:

  1. В спринге в виде /actuator/refresh, но он не гарантирует консистентность между всеми потоками приложения, риск race conditions и деградации приложения

  2. JMX — практически невозможен в продовой среде для наших целей

  3. Jolokia — риски безопасности, часто в read‑only в проде

Многие и многие менеджеры и продакты отдали бы многое за систему которая в рантайме изменяет поведение системы в один клик, да еще и с раскаткой на n% юзеров! (все же ведь любят канареек?)

Так о чем я, родилась идея легковесного хранилища/менеджера флагов для любых бэк-енд приложений, мобилок или фронт-энда через REST/GRPC взаимодействие.

Схема взаимодействия
Схема взаимодействия

Что есть что?
По своей сути система состоит из двух отдельных приложений:

1. evaltuation‑api
Отвечает за чтение состояния флагов, их правил и является элементом системы на которую подписываются клиенты ожидающие изменений во флагах. Хранит в кэше состояние флагов.
Подпись естественно в рамках GRPC (потому что так проще для начала)
2. core‑engine
Отвечает за создание, изменение, удаление флагов на условном дашборде или api. Является центровым в этой архитектуре, поскольку:
а) менеджит состояние
б) пушит через канал (pg‑notify so far) обновление напрямую в evaluation‑api

Структура флага проста как мир

create table flags
(
    id            uuid                     default gen_random_uuid() not null
        primary key,
    key           text                                               not null
        unique,
    description   text,
    enabled       boolean                  default false             not null,
    default_value boolean                  default false             not null,
    created_at    timestamp with time zone default now()             not null,
    updated_at    timestamp with time zone default now()             not null,
    status        text                     default 'draft'::text     not null
        constraint flags_status_check
            check (status = ANY
                   (ARRAY ['draft'::text, 'active'::text, 'inactive'::text, 'deprecated'::text, 'archived'::text]))
);
  • key — самое важное о чем спрашивают подписанные клиенты

  • status — простой статус для жизненным циклом флага

  • default_value — с каким статусом создался флаг

  • enabled — свойство включен ли флаг?

У флагов предусмотрены опциональные условия, которым должены удовлетворить параметры передаваемые клиентами

create table targeting_rules
(
    id           uuid                     default gen_random_uuid() not null
        primary key,
    flag_id      uuid                                               not null
        references flags
            on delete cascade,
    priority     smallint                                           not null,
    attribute    text                                               not null,
    operator     text                                               not null,
    value        jsonb                                              not null,
    return_value boolean                                            not null,
    created_at   timestamp with time zone default now()             not null,
    unique (flag_id, priority)
);

  • flag_id — с каким флагом связано правило
    attribute — название параметра сравнения

  • operator — eq, neq, in, not_in,

  • value - целевое значение аттрибута

  • return_value - что вернуть если правило совпало, пока boolean, возможны расширения до любого типа

В базовом виде вот и весь состав системы и флагов, клиент передает в evaluation‑api набор параметров, evaluation‑api получает данные о флаге, если их нет в кэше из бд, кеширует и отдает клиенту его значение. В случае обновления core‑engire через pg_notify пушит изменения флага в evaluation‑api и тот в свою очередь кеширует их, либо пушит изменения подписчикам GRPC.

На подходе rollout с реализацией консистентного хранения флагов для клиентов, нельзя же просто использовать random() < 0.1, поэтому чтобы один пользователь не всегда попадал в одну и ту же группу на всех флагах придется искать подход, или спрашивать у нейронки умных людей.