Русский
Русский
English
Статистика
Реклама

Feature flags

Feature Flags и фабрика ПО

20.02.2021 14:06:35 | Автор: admin

Наши команды практикуют подход Trunk Based Development новый код сразу добавляется в мастер-ветку, сторонние ветки живут максимум несколько дней. А чтобы коммиты не мешали друг другу, разработчики используют фича-флаги (Feature Flags) переключатели в коде, которые запускают и останавливают работу его компонентов.

Рассмотрим обычную итерацию разработки: планирование, уточнение требований, создание задач в трекере, разработка. По мере готовности задачи разворачиваются на тестовом окружении для проверки, релизная ветка стабилизируется. Потом наконец наступает выпуск, и команда может наконец получить реальную обратную связь от пользователей.

Если вы стремитесь сокращать Time-to-Market, это недопустимо долго. Чем раньше вы получите обратную связь от пользователей, тем скорее вы исправите ошибки, тем меньше времени вы тратите на неудачные идеи, тем больше ресурсов можете уделить идеям удачным.

Чтобы обновления быстрее доезжали до прода, одна итерация должна включать одну фичу. Именно поэтому нужно сокращать срок жизни веток.

Проблемы долгоживущих веток

  • Конфликты между коммитами (Merge hell). Откладывание релиза, интеграция с внешней системой, прочие внешние факторы могут привести кол в нерабочее состояние.

  • Трудности с шарингом кода. Другим членам команды может потребоваться код из новой ветки, если фичи зависят друг от друга. Приходится стартовать ещё одну ветку только ради доступа к этому коду.

  • Проблемы тестового окружения. Если тестовый сервер один, а фиче-веток много, параллельно тестировать задачи не получится.

  • Тяжело откатить изменения. Проблемы после релиза обычное дело, и если из-за новой фиче-ветки начинаются сбои, разработчикам приходится выбирать между хотфиксом и ревертом, лезть в исходный код, заново выкладывать решение.

Как Trunk Based Development решает эти проблемы

Trunk Based Development (от англ. trunk ствол дерева) метод разработки кода на основе одной главной ветки. В отличие от подхода Gitflow, TBD позволяет разработчикам добавлять новые модули сразу в master. Второстепенные feature-ветки также могут создаваться, но они имеют короткий срок жизни.

Trunk Based Development предполагает только одну ветку для разработки, которая называется trunk. В любой момент эту ветку можно развернуть её на проде, а разработка, как и прежде, идёт в отдельных фича-ветках. Только теперь эти ветки живут не более двух дней.

Все изменения в trunk вливаются через пул-реквесты. Изменения небольшие, поэтому процесс ревью не затягивается. Попадание нового кода в trunk запускает процессы автоматического билда, тестов и развёртывания на необходимые окружения.

Но как вести разработку в одной ветке, если какие-то фичи ещё не готовы, а релиз завтра? Тут нам на помощь приходят Feature Flags.

Как работают Feature Flags

По сути своей, это IF-блок, который запускает кусок кода при выполнении некого условия. Самое простое разработчик сам прописывает, включать или выключать код. Могут быть параметры посложнее: например, по расписанию или только для пользователей с такими-то уровнем доступа. Или наоборот фича отключается, если нагрузка на систему превышает заданный порог.

В точке переключения мы обращаемся к переключателю (toggle router), который определяет состояние фичи. Чтобы понять, какой нужен флаг, роутер обращается к его конфигурации и контексту. Первая определяет общую стратегию включения флага (основные условия для его работы), второй включает любую дополнительную информацию (например, имя пользователя, который отправил запрос, текущее время и т.д.).

Как использовать Feature Flags

Технически фиче-флаги работают одинаково, а по способу применения их можно разделить на следующие категории:

  • Релизные (release toggles): скрывают неготовые фичи, уменьшают количество веток, открепляют запуск фичи от даты деплоя. Основной тип флагов.

  • Экспериментальные (experiment toggles): используются для A/B тестирования, позволяют таргетировать функции на разные группы пользователей. Таким образом вы можете развернуть новый сервис на Х% аудитории, чтобы оценить нагрузку или собрать первые отзывы.

  • Разрешающие (permission toggles): открывают доступ к платным фичам или закрытым функциям администратора. Такие флаги могут жить очень долго, быть очень динамичными и менять своё состояние при каждом запросе.

  • Операционные (ops toggles): отключают ресурсоёмкие функции. Например, так можно регулировать работу приложения на слабых смартфонах или застраховаться от падения производительности при запуске новой функциональности флаг отключит модуль до того, как тот вызовет критический сбой.

### Что дают Feature Flags

  • Непрерывная доставка фич со стабильным качеством возможность отключить нерабочий код снижает риски в релизной версии.

  • Тестирование новых фич в боевых условиях фиче-флаги позволяют постепенно внедрять сервисы, контролируя риски при релизе на реальную аудиторию.

  • Возможность развивать несколько версий ПО параллельно TBD и фичефлаги позволяют предлагать разные функции разным группам пользователей, при этом поддерживать все эти версии ПО может всё та же одна команда.

Как управлять флагами

  • Проприетарные решения: LaunchDarkly, Bullet-Train, Unleash. Каждый продукт предлагает какие-то свои преимущества, каждый по-своему удобный. Но за лицензию придётся платить, а гибкость настройки зависит от разработчика системы.

  • Open source решения: Moggles, Esquilo. Это бесплатные решения, но чтобы они заработали у вас, потребуется над ними поколдовать. Кроме того, придётся подбирать продукт с таким набором функций, который вас устроит.

  • Собственная система управления: вариант, которым пользуемся мы. Это единственное в своём роде решение, которое целиком нас устраивает. В будущих постах расскажем подробнее.

Как флаги работают у нас

  • Feature FlagsPortal(FF-Portal): Web + REST API приложение для манипулирования состоянием флагов. Напрямую работает с хранилищем флагов.

  • Feature FlagsStorage(FF-Storage): персистентное хранилище с настройками флагов и их статусами.

  • KubernetesConfigMap(FF-configmap): k8s ConfigMap ресурс, который собирается на основе данных, которые хранятся в FF-Storage в удобном формате для конечного приложения. Изменение данных флагов через FF-Portal также влечёт к изменению FF-configmap.

  • Microservice(MS): Микросервис, который использует FF-configmap как источник конфигурации при старте приложения. При изменений FF-configmap, микросервис делает перезагрузку своей конфигурации.

Приложение считывает конфигурацию флагов с FF-ConfigMap, который монтируется к Pod-у как файл. При изменении ConfigMap, k8s обновит и файл, далее приложение среагирует на изменение файла и перегрузит конфигурацию флагов.

Изменение флагов происходит через портал, который отправляет интеграционное сообщение в шину при обновлении статуса. Компонент Config Updater обновляет значения флагов в FF-ConfigMap через K8S API.

Напоследок о тестировании

Возникает вопрос, как тестировать продукт с фиче-флагами? На первый взгляд, флаги усложняют в этот процесс если переключателей становится много, то и количество всевозможных состояний резко растёт.

Но не всегда флаги зависят друг от друга. Поэтому мы для релиза тестируем два предельных случая: 1) все новые флаги выключены и 2) все флаги включены.

Практика показывает, что обычно этого достаточно.

Подробнее..

Как портал Feature Flags помогает бизнесу управлять ИТ-продуктом

05.03.2021 10:06:07 | Автор: admin

Продолжаем рассказывать про feature flags (FF) переключатели в коде, которые запускают и деактивируют функции продукта. На этот раз хотим вам рассказать про наше решение портал фиче-флагов, который позволяет бизнес-заказчикам управлять состоянием FF, а значит функциональностью продукта.

В нашей первой статье про Feature Flags мы говорили, как этот инструмент помогает ускорить запуск новых фич, повысить конкурентоспособность продукта и в целом упростить процессы в команде. Сейчас мы запустили в опытно-промышленную эксплуатацию портал для управления фиче-флагами. И хотим рассказать вам об этом решении.

Feature Flag - это IF-блок, который запускает часть кода при выполнении некого условия. Самое простое разработчик сам прописывает, включать или выключать код. Могут быть параметры посложнее: например, по расписанию или только для пользователей с такими-то уровнем доступа. Или наоборот фича отключается, если нагрузка на систему превышает заданный порог.

Идея портала для фича-флагов состоит в том, чтобы владельцы продукта могли самостоятельно вводить в бой или же выключать функции, не привлекая к этому команду разработки. На портале заказчик видит только функции, готовые к приемке и внедрению. Для него это руководство к действию протестировать, либо включить функциональность. И в нужный момент он самостоятельно переключает её флаг, и функция начинает работать в продукте.

Команде разработки такая механика помогает менять стиль работы: переходить к микрорелизам и выносить за его пределы согласования поставки с заказчиком. Задача выкладывается на прод сразу по факту готовности. Заказчик же управляет приемкой и включением фич на проде. Все делают свою работу, не блокируя друг друга.

Базовый набор функций позволяет переключать флаги и централизованно контролировать состояние всех фич. В дальнейшем мы планируем добавить возможность удалять неактуальные флаги из списка, группировать их по разным фильтрам, выводить аналитику. Фичи можно будет запускать с настраиваемыми параметрами например, указывать дату, когда она должна включиться или отключиться.

Как портал помогает бизнес-заказчику

A/B-исследования и бета-тесты. В логику переключателей можно заложить стратегию деления пользователей часть аудитории увидит новую функциональность, а остальные будут работать с основной версией.

Разграничение доступа к функциям. Переключатели могут автоматически открывать и блокировать возможности для разных пользователей. Например, подключать платные функции или запускать пробный режим для новых клиентов.

Защита от сбоев. Системные переключатели позволяют включать и выключать целые компоненты продукта. Например, если нагрузка превышает некий критический уровень или проблемы начинаются во внешнем сервисе, который подключен к вашему продукту.

В конечном счете переключатели приближают управление продуктом к нуждам бизнеса. Имея под рукой простой переключатель функций, заказчик обеспечивает себе оперативный контроль над состоянием продукта.

Архитектура портала

MVP-версия нашего портала представляла собой простую веб-страницу, где отображались статусы функций продукта. Настройки хранились в простом конфигурационном файле.

Когда идея прижилась, MVP вырос до компактного продукта из четырёх модулей:

  • Микросервис для управления переключателями содержит в себе всю логику их обработки. Микросервисы получают конфигурацию флагов при старте из configMap для своего namespace и сохраняют в своей локальной памяти. Если происходит включение или включение флагов на страничке портала, то агент меняет все configMaps, а микросервисы через обратную связь обновляют свою локальную конфигурацию.

  • Фронтенд-модуль обеспечивает механику включения и выключения переключателей.

  • Агент обеспечивает консистентность данных в локальном хранилище, которое заменило конфиг-файл с настройками.

  • Стартер (опциональный компонент) позволяет проверить работоспособность каждого отдельного переключателя перед изменением состояния.

Панель управления:

Самое главное, наш портал это Cloud Native продукт, который изначально разрабатывался для использования в микросервисных приложениях в инфраструктуре Kubernetes. Так что он может работать с любыми продуктами, вне зависимости от платформы, на которой он написан.

Хотите посмотреть вживую обращайтесь

У нас есть готовые SDK для .NET и Java, с которыми наши команды могут быстро запустить логику работы с фича-флагами в своих продуктах. Это включает в себя не только механизм их переключения, но и верхнеуровневый контекст использования. Например, чтобы состояние фичи не переключилось в тот момент, когда с ней работает пользователь, и не произошёл сбой процесса. А нашим заказчикам эти SDK открывают возможность поэкспериментировать с порталом в своём продукте.

Подробнее..

Trunk Based Development и Spring Boot, или ветвись оно все по абстракции

24.12.2020 20:04:36 | Автор: admin

Всем привет!

Закончилась осень, зима вступила в свои законные права, листья уже давно опали и перепутанные ветви кустарников наталкивают меня на мысли о моём рабочем Git репозитории Но вот начался новый проект: новая команда, чистый, как только что выпавший снег репозиторий. "Тут все будет по другому" - думаю я и начинаю "гуглить" про Trunk Based Development.

Если у вас никак не получается поддерживать git flow, вам надоели кучи этих непонятных веток и правил для них, если в вашем проекте появляются ветки вида "develop/ivanov", то добро пожаловать в под кат! Там я пробегусь по основным моментам Trunk Based Development и расскажу о том, как реализовать такой подход, используя Spring Boot.

Введение

Trunk Based Development (TBD) это подход, при котором вся разработка ведется на основе единственной ветки trunk (ствол). Чтобы воплотить такой подход в жизнь, нам нужно следовать трем основным правилам:

1) Любые коммиты в trunk не должны ломать сборку.

2) Любые коммиты в trunk должны быть маленькими, на столько, что review нового кода не должно занимать более 10 минут.

3) Релиз выпускается только на основе trunk.

Договорились? Теперь давайте разбираться на примере.

Начало разработки

Initial commit

Я не придумал ни чего лучше как написать приложения "оповещатель", REST сервис которому мы передаем оповещение в виде json, а он уже оповещает кого написано. Для начала собираем наш проект на spring initializr. Я сделал Maven Project, язык Java версия 8, Spring Boot 2.4.0. Зависимости нам понадобятся следующие:

Зависимости

Название

Тип

Описание

Spring Configuration Processor

DEVELOPER TOOLS

Generate metadata for developers to offer contextual help and "code completion" when working with custom configuration keys (ex.application.properties/.yml files).

Validation

I/O

JSR-303 validation with Hibernate validator.

Spring Web

WEB

Build web, including RESTful, applications using Spring MVC. Uses Apache Tomcat as the default embedded container.

Lombok

DEVELOPER TOOLS

Java annotation library which helps to reduce boilerplate code.

Инициализируем git репозиторий и пушим на GitHub или куда вам больше нравится. Основную ветку можно назвать по своему усмотрению: main, master или даже так и назвать - trunk, чтобы всем сразу было понятно чем вы тут занимаетесь. Все. Посадили деревце. Теперь будем бережено его выращивать.

Первая фича

Напишем первую реализацию которая будет отправлять сообщение на почту. Для начала опишем свойства нашего сервиса в виде ConfigurationProperties. У приложения пока будут только два свойства: sender-email - почтовый адрес отправителя и email-subject - тема письма в оповещении.

NotificationProperties
 @Getter @Setter @Component @Validated //говорим, что свойства должны проверяться @ConfigurationProperties(prefix = "notification") public class NotificationProperties {     @Email //проверяем что это почта     @NotBlank //проверяем что поле заполнено     private String senderEmail;     @NotBlank     private String emailSubject; }

Теперь сделаем компонент который будет отправлять оповещения на почту, делаем просто заглушу, так как скорее всего в реальность этот компонент предоставлялся бы библиотекой.

Собственно реализация для данного примера нам вообще не понадобится.

EmailSender
@Slf4j@Componentpublic class EmailSender {    /**     * Отправляет сообщение на почту понарошку     */    public void sendEmail(String from, String to, String subject, String text){        log.info("Send email\nfrom: {}\nto: {}\nwith subject: {}\nwith\n text: {}", from, to, subject, text);    }}


Напишем простую модельку для оповещения:

Notification
@Getter@Setter@Builder@AllArgsConstructorpublic class Notification {    private String text;    private String recipient;}

Сервис оповещения:

NotificationService
@Service@RequiredArgsConstructorpublic class NotificationService {    private final EmailSender emailSender;    private final NotificationProperties notificationProperties;    public void notify(Notification notification){        String from = notificationProperties.getNotificationSenderEmail();        String to = notification.getRecipient();        String subject = notificationProperties.getNotificationEmailSubject();        String text = notification.getText();        emailSender.sendEmail(from, to, subject, text);    }}

И наконец контроллер:

NotificationController
@RestController@RequiredArgsConstructorpublic class NotificationController {    private final NotificationService notificationService;    @PostMapping("/notification/notify")    public void notify(Notification notification){        notificationService.sendNotification(notification);    }}

Ещё нам конечно понадобится тесты, без них TBD не получится. Напишем тест для NotificationService:

NotificationServiceTest
@SpringBootTestclass NotificationServiceTest {    @Autowired    NotificationService notificationService;    @Autowired    NotificationProperties properties;    @MockBean    EmailSender emailSender;    @Test    void emailNotification() {        Notification notification = Notification.builder()                .recipient("test@email.com")                .text("some text")                .build();        notificationService.notify(notification);        ArgumentCaptor<string> emailCapture = ArgumentCaptor.forClass(String.class);        verify(emailSender, times(1))                .sendEmail(emailCapture.capture(),emailCapture.capture(),emailCapture.capture(),emailCapture.capture());        assertThat(emailCapture.getAllValues())                .containsExactly(properties.getSenderEmail(),                                notification.getRecipient(),                                properties.getEmailSubject(),                                notification.getText()                );    }}

И для NotificationController

NotificationControllerTest
@WebMvcTest(controllers = NotificationController.class)class NotificationControllerTest {    @Autowired    MockMvc mockMvc;    @Autowired    ObjectMapper objectMapper;    @MockBean    NotificationService notificationService;    @SneakyThrows    @Test    void testNotify() {        ArgumentCaptor<notification> notificationArgumentCaptor = ArgumentCaptor.forClass(Notification.class);        Notification notification = Notification.builder()                .recipient("test@email.com")                .text("some text")                .build();        mockMvc.perform(post("/notification/notify")                .contentType(MediaType.APPLICATION_JSON)                .content(objectMapper.writeValueAsString(notification)))                .andExpect(status().isOk());        verify(notificationService, times(1)).notify(notificationArgumentCaptor.capture());        assertThat(notificationArgumentCaptor.getValue())                .usingRecursiveComparison()                .isEqualTo(notification);    }}

Написали, сделали rebase, прогнали сборку с тестами и запушили в trunk - эта последовательность должна войти в привычку и делаться как можно чаще.

Для настоящих проектов я очень рекомендую делать первый коммит именно таким, чтобы он был как можно меньше и удовлетворял нашему второму правилу - code review меньше чем за 10 минут.

Профили

Начнем с самого простого и наверное для большинства известного приема - использование профилей. Для начала вам нужно включить все ваше воображение и представить, что нам вдруг понадобилось оповещать кого-то по расписанию. Ну что же сделаем отдельный класс под эту задачу.

NotificationTask
@Component@EnableScheduling@RequiredArgsConstructorpublic class NotificationTask {    private final NotificationService notificationService;    private final NotificationProperties notificationProperties;    @Scheduled(fixedDelay = 1000)    public void notifySubscriber(){        notificationService.notify(Notification.builder()                .recipient(notificationProperties.getSubscriberEmail())                .text("Notification is worked")                .build());    }}

Теперь прогоним наши тесты и получим исключение для теста сервиса:

"org.mockito.exceptions.verification.TooManyActualInvocations".

Конечно, ведь в нашем тесте ожидался один вызов метода sendEmail, а получилось больше, так как теперь этот же метод вызывается в задаче.

Не порядок. Можно конечно выставить задаче initialDelay, что бы тест успел запустится раньше чем задача, но это будет костыль. Вместо этого как вы уже наверное догадались мы применим профиль. Вынесем аннотацию @EnableScheduling в отдельную конфигурацию и добавим аннотацию @Profile где скажем, что нужно запускать задачи всегда, кроме как в профиле "test".

SchedulingConfig
@Profile("!test")@Configuration@EnableSchedulingpublic class SchedulingConfig {}

В тестовых ресурсах, в application.yaml добавим включение профиля:

application.yaml
spring:  profiles:    active: testnotification:  email-subject: Auto notification  sender-email: robot@somecompany.com

Теперь все должно заработать, в тестах, задачи по расписанию больше не запускаются, но если просто запустить приложение из main метода, то задачи будут исправно тикать.

В своей работе я в основном использую профили именно для тестирования, но ни кто вам не запретит использовать их для своих целей, главное, как мне кажется с ними не мельчить и не создавать их много.

Используйте профили тогда, когда вам нужно включать или выключать целый слой какой-либо логики, причем на постоянной основе, т.е. вы не планируете выкинуть эту возможность когда-нибудь потом. Примерами могут служить: безопасность, мониторинг или как в нашем случае задачи по расписанию.

Для более точечного управления функциями приложения лучше использовать Feature flags, но этот способ мы рассмотри уже после нашего первого релиза. Сделали rebase, прогнали сборку с тестами и запушили в trunk.

Первый релиз

Давайте немного отвлечемся от кодирования и посмотрим, что делать с релизами. В TBD описано два способа выпускать релизы: первый из релизной ветки, второй прямо из trunk. Здесь я разберу первый способ.

Первым делом нам нужно взять коммит из которого мы будем делать релиз, это может быть как последний коммит в trunk, так и коммит который вы сделали в прошлом, все зависит от того, из какой ревизии кода вы хотите сделать релиз.

Для git выкачать прошлый коммит можно так:

git checkout <hash>

Теперь создаем новую релизную ветку, обязательно ставим метку c версией, и пушим в удаленный репозиторий.

git checkout -b Release_1.0.0git tag 1.0.0git push -u origin Release_1.0.0git push origin 1.0.0

Готово! Можно разворачивать код из этой ветке в staging, а затем и в production.

Теперь мы добавим ещё парочку правил, которые будем соблюдать при работе с релизными ветками:

1) Разработчики не ведут в релизной ветки какие-либо работы

2) Релизная ветка не сливается с trunk

3) Если нужен Hotfix, делаем Cherry-pick из trunk и добавляем метку с минорной версией

Таким образом релизная ветка как бы "замораживается" и нужна только для того, чтобы выпустить из неё релиз и хранить в себе код соответствующей версии приложения. Релизные ветки можно и даже нужно удалять как только они становятся не актуальным.

Feature flags

Для второй версии нашего приложения у нас стоит задача добавить немного аудита в нашу систему: теперь каждое оповещение должно сохраняться в базу данных. Только вот проблема в том, что не известно когда на production её развернут и подготовят. Тогда, чтобы ни кого не ждать и не откладывать, то, что можно сделать сейчас, мы обернем данную функциональность в feature flag.

Если кратко, этот прием позволит нам внедрить новую фичу уже в следующем релизе, а вот включить её можно будет как только появится возможность это сделать, а в случае, если, что-то пойдет не так, фичу можно будет снова выключить.

Добавляем зависимости для взаимодействия с базой данных. БД на production у нас будет например oracle (это не особо важно для примера), а для тестов будем использовать h2.

Зависимости (БД)
    <dependency>        <groupid>org.springframework.boot</groupid>        <artifactid>spring-boot-starter-data-jpa</artifactid>    </dependency>    <dependency>        <groupid>com.oracle.ojdbc</groupid>        <artifactid>ojdbc10</artifactid>    </dependency>    <dependency>        <groupid>com.h2database</groupid>        <artifactid>h2</artifactid>    </dependency>

Теперь добавим отдельный класс, где будем описывать только свойства для включения разных фич. Примем конвенцию, что все свойства в этом классе должны быть boolean. Добавим туда флаг "persistence", который будет включать и выключать сохранение оповещений в базу.

FeatureProperties
@Getter@Setter@Component@ConfigurationProperties(prefix = "features.active")public class FeatureProperties {    boolean persistence;}

Сразу запишем в application.yaml в тестовых ресурсах features.active.persistence: on (spring сам поймет, что on==true).

Только не забудьте сначала скомпилировать проект, чтобы включилось автодополнение в свойствах приложения.

Нашу модель переделываем в Entity.

Осторожно много аннотаций!

Notification (Entity)
@Entity@Getter@Setter@Builder@NoArgsConstructor@AllArgsConstructorpublic class Notification {    @Id    @GeneratedValue    private Long id;    private String text;    private String recipient;    @CreationTimestamp    private LocalDateTime time;}

Добавляем репозиторий

NotificationRepository
public interface NotificationRepository extends CrudRepository<notification, long=""> {}

В NotificationService добавим NotificationRepository и FeatureProperties как зависимости, в конце метода notify вызовем метод репозитория save, обернув его в обычный if.

NotificationService (Feature flag)
@Service@RequiredArgsConstructorpublic class NotificationService {    private final EmailSender emailSender;    private final NotificationProperties notificationProperties;    private final FeatureProperties featureProperties;    @Nullable    private final NotificationRepository notificationRepository;    public void notify(Notification notification){        String from = notificationProperties.getSenderEmail();        String to = notification.getRecipient();        String subject = notificationProperties.getEmailSubject();        String text = notification.getText();        emailSender.sendEmail(from, to, subject, text);        if(featureProperties.isPersistence()){            notificationRepository.save(notification);        }    }}

Забегая немного вперед, аннотация @Nullable для поля NotificationRepository нам нужна, чтобы Spring не падал с ошибкой UnsatisfiedDependencyException, если не найдет такой бин у себя в контексте.

Теперь можно запустить тесты, и увидеть, что все они прошли, но если мы запустим наше приложение оно будет требовать указать url для базы данных и не будет запускаться.

Исправлять будем примерно так же как и для задач по расписанию. Создадим отдельную конфигурацию где укажем, что автоконфигурация для базы данных должна быть исключена, если флаг features.active.persistence: off (spring сам поймет, что off==false).

DataJpaConfig
@Configuration@ConditionalOnProperty(prefix = "features.active", name = "persistence",                        havingValue = "false", matchIfMissing = true)@EnableAutoConfiguration(exclude = {        DataSourceAutoConfiguration.class,        DataSourceTransactionManagerAutoConfiguration.class,        HibernateJpaAutoConfiguration.class})public class DataJpaConfig {}

Запускаем приложение с флагом features.active.persistence: off в свойствах. Приложение стартует, но не создает ни каких бинов связанных с работой базы данных.

Для того чтобы управлять флагами в среде развертывания, можно указать spring через командную строку параметр для дополнительных свойств, например:

--spring.config.additional-location=file:/etc/config/features.yaml

Или передать с помощью аргументов VM, например:

-Dfeatures.active.persistence=true

Правил с флагами будет два:

1) После того как функциональность полностью протестирована и стабильно работает, флаг этой функции нужно удалить

2) Мест в коде, где идет ветвление по одному и тому же feature флагу, должно быть минимальное количество

По второму правилу поясню подробнее. Если, ваша новая функциональность которую вы хотите обернуть в feature флаг, заставляет вас писать код вида: "if (flag) {}" в нескольких местах сразу, то вам стоит задуматься либо над дизайном вашей системы, либо о приеме "ветвления по абстракции", который как раз сейчас и разберем.

Branch by Abstraction

В третьей версии, настало время расширять функциональность оповещений.

Теперь с клиентской части нашего приложения в сообщениях, будет приходить тип оповещения: EMAIL, SMS или PUSH. Следовательно нам необходимо реализовать два дополнительных "отправщика" сообщений, а ещё логику в самом сервисе уведомлений которая будет определять реализацию.

Это довольно серьезная доработка, поэтому мы не хотим с ней торопится, мы хотим тщательно проработать архитектуру решения, да так чтобы в его развитие принимало как можно больше разработчиков. Поэтому мы не будет делать в Git отдельную ветку, в которой можно было бы хранить нестабильный код, а сделаем ветку внутри программы с помощью ветвления по абстракции.

Рецепт от Мартина Фаулера прост:

1) Выделить интерфейс для заменяемой функциональности

2) Заменить прямой вызов реализации в клиенте на обращение к интерфейсу

3) Создать новую реализацию которая реализует интерфейс

4) Подменить реализацию на новую

5) Удалить старую реализацию

Первым делом нам нужно сделать интерфейс NotificationService вместо класса, а сам класс переименовать в EmailNotificationService. В Inellij IDEA это можно провернуть с помощью рефакторинга:

1) Правой кнопкой по классу, выбрать Refactor/Extract interface

2) Выбрать опцию "Rename original class and use interface where possible"

3) В поле "Rename implementation class to" вписываем "EmailNotificationService"

4) В "Members to from interface" нажать галочку напротив метода "notify"

5) Нажать кнопку "Refactor"

После этого все классы должны ссылаться на интерфейс NotificationService, а рядом в пакете появится EmailNotificationService где будет старая реализация.

Сделали rebase, прогнали сборку с тестами и запушили в trunk.

После этого можно спокойно продолжать работу уже над новой реализацией. Добавим в модель поле с типом оповещения, пусть это просто Enum.

NotificationType
public enum NotificationType {    EMAIL, SMS, PUSH, UNKNOWN}

Так же нам нужно будет добавить два новых компонента "отправителя":

SmsSender и PushSender.

Senders
@Slf4j@Componentpublic class SmsSender {    /**     * Отправляет сообщение на телефон     */    public void sendSms(String phoneNumber, String text){        log.info("Send sms {}\nto: {}\nwith text: {}", phoneNumber, text);    }    }@Slf4j@Componentpublic class PushSender {        /**     * Отправляет push уведомления     */    public void push(String id, String text){        log.info("Push {}\nto: {}\nwith text: {}", id, text);    }}

Новую реализацию сервиса назовем MultipleNotificationService, и для начала напишем "в лоб".

MultipleNotificationService - switch case
@Service@RequiredArgsConstructorpublic class MultipleNotificationService implements NotificationService {    private final EmailSender emailSender;    private final PushSender pushSender;    private final SmsSender smsSender;    private final NotificationProperties notificationProperties;    private final NotificationRepository notificationRepository;    @Override    public void notify(Notification notification) {        String from = notificationProperties.getSenderEmail();        String to = notification.getRecipient();        String subject = notificationProperties.getEmailSubject();        String text = notification.getText();        NotificationType notificationType = notification.getNotificationType();        switch (notificationType!=null ? notificationType : NotificationType.UNKNOWN) {            case PUSH:                pushSender.push(to, text);                break;            case SMS:                smsSender.sendSms(to, text);                break;            case EMAIL:                emailSender.sendEmail(from, to, subject, text);                break;            default:                throw new UnsupportedOperationException("Unknown notification type: " + notification.getNotificationType());         }        notificationRepository.save(notification);    }}

Запустив тесты, обнаружим, что NotificationServiceTest стал падать с ошибкой:

"expected single matching bean but found 2: emailNotificationService, multipleNotificationService".

Вылечить проблему можно например добавлением аннотации @Primary над старой реализацией сервиса - EmailNotificationService.

@Primary - сделает бин приоритетным для инъекции, но в тоже время бины с тем же типом все равно создадутся в контексте и мы сможем внедрить новую реализацию в тест.

Другой вариант - просто убрать аннотацию @Service из новой реализации тем самым исключив её из контекста, а для теста написать отдельную конфигурацию или вообще не писать Spring тест, а написать простой unit тест, где создавать компоненты самим через "new".

Я воспользуюсь первым вариантом и напишу отдельный Spring тест для новой реализации.

MultipleNotificationServiceTest
@SpringBootTestclass MultipleNotificationServiceTest {    @Autowired    MultipleNotificationService multipleNotificationService;    @Autowired    NotificationProperties properties;    @MockBean    EmailSender emailSender;    @MockBean    PushSender pushSender;    @MockBean    SmsSender smsSender;    @Test    void emailNotification() {        Notification notification = Notification.builder()                .recipient("test@email.com")                .text("some text")                .notificationType(NotificationType.EMAIL)                .build();        multipleNotificationService.notify(notification);        ArgumentCaptor<string> emailCapture = ArgumentCaptor.forClass(String.class);        verify(emailSender, times(1))                .sendEmail(emailCapture.capture(),emailCapture.capture(),emailCapture.capture(),emailCapture.capture());        assertThat(emailCapture.getAllValues())                .containsExactly(properties.getSenderEmail(),                        notification.getRecipient(),                        properties.getEmailSubject(),                        notification.getText()                );    }    @Test    void pushNotification() {        Notification notification = Notification.builder()                .recipient("id:1171110")                .text("some text")                .notificationType(NotificationType.PUSH)                .build();        multipleNotificationService.notify(notification);        ArgumentCaptor<string> captor = ArgumentCaptor.forClass(String.class);        verify(pushSender, times(1))                .push(captor.capture(),captor.capture());        assertThat(captor.getAllValues())                .containsExactly(notification.getRecipient(),  notification.getText());    }    @Test    void smsNotification() {        Notification notification = Notification.builder()                .recipient("+79157775522")                .text("some text")                .notificationType(NotificationType.SMS)                .build();        multipleNotificationService.notify(notification);        ArgumentCaptor<string> captor = ArgumentCaptor.forClass(String.class);        verify(smsSender, times(1))                .sendSms(captor.capture(),captor.capture());        assertThat(captor.getAllValues())                .containsExactly(notification.getRecipient(),  notification.getText());    }    @Test    void unsupportedNotification() {        Notification notification = Notification.builder()                .recipient("+79157775522")                .text("some text")                .build();        assertThrows(UnsupportedOperationException.class, () -> {            multipleNotificationService.notify(notification);        });    }}

Сделали rebase, прогнали сборку с тестами, запушили в trunk, получили от команды по шапке за switch-case.

Реализовать логику красиво позволит шаблон "Стратегия", но тут есть проблема в том, что у всех компонент "отправителей" разные интерфейсы, собственно как скорее всего и будет в реальности, ведь обычно такие компоненты предоставляются внешними библиотекам. Решить проблему с разными интерфейсами можно с помощью шаблона "Адаптер". Расписывать подробно здесь не буду, статья всё таки о другом, но код вы можете посмотреть у меня на GitHub.

После того как сделали все красиво, пробуем ещё раз: rebase, прогнали сборку с тестами, запушили в trunk.

На этот раз код не вызвал ни у кого негатива, и его можно включать в программу. Делать будем это с помощью все того же feature флага.

В класс с флагами добавляем новый:

  boolean multipleSenders;

Над классом EmailNotificationService добавляем аннотацию с условием (ни в коем случае не удалять @Primary):

"Выключить, только, если флаг features.active.multiple-senders установлен (matchIfMissing) и равен false"

@ConditionalOnProperty(prefix = "features.active",                        name = "multiple-senders",                        havingValue = "false",                        matchIfMissing = true)

Над MultipleNotificationService нужно добавить аннотацию с "зеркальным" условием:

"Включить, только, если флаг features.active.multiple-senders не установлен (matchIfMissing) или равен true"

@ConditionalOnProperty(prefix = "features.active",                        name = "multiple-senders",                        havingValue = "true",                       matchIfMissing = true)

Таким образом в тестах у нас окажутся обе реализации, а вот при запуске приложения будет работать только одна.

После того как новая версия будет обкатана, вместе с feature флагом нужно будет удалить и старую реализацию, а вот выделенный интерфейс лучше все таки оставить.

И снова rebase, прогнали сборку с тестами, запушили в trunk. Вся команда проверила ваш код и благополучно забыла, что у вас ещё есть задача по расписанию, которой не сказали с каким типом ей отправлять оповещения.

На production заметили, что оповещения по расписанию больше не запускаются, но благодаря feature флагу все сразу же откатили обратно.

Команде разработки выдали логи, она принялась за исправление, и параллельно начала обдумывать как правильно сделать Hotfix, улучшить code review и тестирование проекта, чтобы более не сталкиваться с подобными проблемами но это уже совсем другая история, а нам пора подводить итоги.

Итоги

Trunk Based Development - отличная модель ветвления, которая наконец-то поможет вам избавится от кошмара слияния веток, позволит получить больше контроля над кодом, а команду сделать более дисциплинированной, превратит "теневое внедрение" из просто интересного приема в обыденность.

Trunk Based Development - очень гибкая методология, у неё есть несколько вариаций из которых вы сможете выбрать наиболее подходящий вариант, и конечно для её применения не обязательно использовать Spring Boot, но надеюсь я смог показать, что с ним это просто и удобно.

На этом всё, внизу будут все ссылки из статьи, спасибо за внимание!

Ссылки

Мой код на GitHub

TBD

spring initializr

ConfigurationProperties

Spring profiles

Feature flags

релизная ветка

релиз из trunk

Branch by abstraction

Стратегия

Адаптер

Подробнее..
Категории: Git , Devops , Java , Spring , Spring-boot , Trunk , Branching , Feature flags

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru