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

от автора

Бороздя просторы космоса Хабра, рабочих репозиториев и не только, в сегменте 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, поэтому чтобы один пользователь не всегда попадал в одну и ту же группу на всех флагах придется искать подход, или спрашивать у нейронки умных людей.

ссылка на оригинал статьи https://habr.com/ru/articles/1029024/