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

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

25 января 2023 г.

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

Imagine how simple it is to manage your features

Очень удобно иметь возможность в любой момент изменить поведение сервиса без перезагрузки серверов, отключить неработающий функционал или провести AB-тестирование. Для этого разработчики обычно используют переключатели функций. Если в вашем стартапе не гигант, а небольшая группа энтузиастов — вам придется решать эту проблему доступными инструментами. В этой статье я покажу вам простой способ реализации переключения функций с помощью общей базы данных SQL.

Описание типичной системы

Сколько бы усилий ни вкладывалось в разработку отказоустойчивых самодостаточных систем, ручное вмешательство неизбежно:

* исправление несоответствий данных, * запуск задач, * восстановление данных после сбоя, * заполнение или миграция, * отображение бизнес-операций и связанных данных.

Если проблему можно легко исправить, разработчик может настроить данные непосредственно в базе данных или написать bash/python/любой скрипт. Обычно мы избегаем прямого доступа к данным, но обстоятельства могут заставить вас это сделать. Но есть более надежный способ - приложение внутреннего пользования - Панель администрирования.

It could be your admin panel UI

Преимущества панели администратора:

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

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

Запачкайте руки кодом

Простой переключатель

Самый простой вариант использования — это двухпозиционный переключатель: либо включить для всех, либо отключить для всех.

private final FeatureToggleService featureToggleService;

public void businessLogic() {
   if (featureToggleService.isFeatureEnabled(IMPORTANT_FEATURE)) {
       doImportantFeatureStuff();
   } else {
       log.info("Feature disabled {}", IMPORTANT_FEATURE);
   }
}

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

public class FeatureToggleService {

   private static final String STATE_QUERY = "SELECT id FROM features WHERE name = (?)";

   private final JdbcOperations jdbcOperations;

   private final LoadingCache<String, Boolean> toggleStateCache = CacheBuilder.newBuilder()
           .expireAfterWrite(60, TimeUnit.SECONDS)
           .build(new CacheLoader<>() {
               public Boolean load(String featureName) {
                   return jdbcOperations.queryForList(STATE_QUERY, String.class, featureName)
                              .size() > 0;
               }
           });

   public boolean isFeatureEnabled(String featureName) {
       boolean state = toggleStateCache.getUnchecked(featureName);
       return Boolean.TRUE.equals(state);
   }
}

В примере выше я использовал кеш от Google Guava. Это простой локальный кеш, но в 99% случаев он наверняка удовлетворит ваши требования. Функции переключаются нечасто, нет смысла делать запросы часто. В качестве примера я выбрал 60 секунд, но это хороший компромисс, не слишком много запросов и не слишком долгое переключение.

Переключатель пользователя

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

которые считаются тестировщиками.

private final FeatureToggleService featureToggleService;

public void businessLogic(LoggedUser loggedUser) {
   if (featureToggleService.isFeatureEnabled(IMPORTANT_FEATURE, loggedUser.id())) {
       doImportantFeatureStuff();
   } else {
       log.info("Feature disabled {}", IMPORTANT_FEATURE);
   }
}

Появилось третье состояние фичи — тестирование. Итак, состояния:

  1. Включено для всех
  2. Отключено для всех
  3. Включено для тестировщиков

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

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

И реализация может быть:

public class FeatureToggleService {

   private static final String STATE_QUERY =
            "SELECT enabled_for FROM features WHERE name = (?)";
   private static final String TESTERS_QUERY = "SELECT id FROM test_users";
   private final JdbcOperations jdbcOperations;

   private final Supplier<Set<Long>> testUsersSupplier =
           memoizeWithExpiration(this::extractTesters, 60, TimeUnit.SECONDS);

   private final LoadingCache<String, String> toggleStateCache = CacheBuilder.newBuilder()
           .expireAfterWrite(60, TimeUnit.SECONDS)
           .build(new CacheLoader<>() {
               public String load(String featureName) {
                   return jdbcOperations.queryForList(STATE_QUERY, String.class, featureName)
                           .stream().findFirst().orElse("NONE");
               }
           });

   public boolean isFeatureEnabled(String featureName, long userId) {
       String state = toggleStateCache.getUnchecked(featureName);
       return "ALL".equals(state) ||
               ("TEST".equals(state) && testUsersSupplier.get().contains(userId));
   }

   private Set<Long> extractTesters() {
       return Set.copyOf(jdbcOperations.queryForList(TESTERS_QUERY, Long.class));
   }
}

Сначала мы получаем состояние фичи по ее имени (либо из базы данных, либо из кеша), затем, если она ALL, считаем ее включенной для всех, или если она TEST — рассматриваем только ее включен для тестировщиков. Если это так, мы получаем список тестировщиков (либо из базы данных, либо из кеша) и проверяем, есть ли текущий пользователь в списке. Если ничего из вышеперечисленного — считайте, что функция отключена для всех.

Что может пойти не так в приведенных выше реализациях

Есть одна неочевидная проблема с локальным кешем, о которой вам следует знать:

  1. 09:45:27 функция включена для всех
  2. 09:45:47 запрос 1 поступил на сервер 1, и функция работала как включенная
  3. 09:45:51 запрос 2 поступил на сервер 2, и функция не работала, так как отключена

Почему это так? Локальные кэши ничего не знают о других серверах, в большинстве случаев это вообще не может быть проблемой. В противном случае вам следует выполнить одно из следующих действий:

  • продолжить использовать локальный кеш, но удалить его с помощью уведомления (используя подсистему публикации или широковещательные сообщения);
  • использовать распределенный кеш (eCache или аналогичный) и удалять его при каждом изменении;
  • использовать базы данных в оперативной памяти (Redis, Zookeeper или аналогичные) для хранения функций и предоставления недорогого доступа к ним;
  • воспользоваться платным сервисом, предоставляющим этот функционал (их несколько, здесь нет дополнений и настроек);
  • придумайте свою гениальную идею, их может быть много :)

Заключение

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


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE