Статьи

Функциональный вызов параллелизма Scala

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

Автоматический выключатель защищает доступ к внешней службе, такой как база данных, сторонний API или микросервис. Как только слишком много запросов к сервису перестают работать, автоматический выключатель отключается и сразу же сбрасывает все запросы, пока автоматический выключатель не сможет сброситься.

Автоматические выключатели не только защищают внешние службы от перегрузки (давая им возможность восстановления после сбоя), но и помогают сохранить локальные ресурсы (такие как сокеты, потоки и т. П.), Которые в противном случае были бы потрачены впустую на потерю стоимости.

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

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

На семинаре ZIO изучение различных возможностей автоматических выключателей дало мне понять: я действительно не люблю автоматические выключатели. Я нахожу произвольную природу числа состояний и условий переключения глубоко беспокоящими.

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

Соревнование

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

Поток корректируется на основе наблюдаемых отказов, которые соответствуют требованиям (т. Е. Соответствуют определенному пользователем предикату).

Если вы хотите использовать ZIO для реализации Tap, то ваш API должен соответствовать следующему интерфейсу:

/**
 * A `Tap` adjusts the flow of tasks through 
 * an external service in response to observed
 * failures in the service, always trying to 
 * maximize flow while attempting to meet the 
 * user-defined upper bound on failures.
 */
trait Tap[-E1, +E2] {
  /**
   * Sends the task through the tap. The 
   * returned task may fail immediately with a
   * default error depending on the service 
   * being guarded by the tap.
   */
  def apply[R, E >: E2 <: E1, A](
    effect: ZIO[R, E, A]): ZIO[R, E, A]
}
object Tap {
  /**
   * Creates a tap that aims for the specified 
   * maximum error rate, using the specified 
   * function to qualify errors (unqualified 
   * errors are not treated as failures for 
   * purposes of the tap), and the specified 
   * default error used for rejecting tasks
   * submitted to the tap.
   */
  def make[E1, E2](
    errBound  : Percentage,
    qualified : E1 => Boolean, 
    rejected  : => E2): UIO[Tap[E1, E2]] = ???
}

Если вы хотите использовать Cats IO или Monix для реализации Tap, тогда ваш API должен соответствовать следующему интерфейсу (или его полиморфному эквиваленту):

trait Tap {
  def apply[A](effect: Task[A]): Task[A]
}
object Tap {
  def make(
    errBound  : Percentage,
    qualified : Throwable => Boolean, 
    rejected  : => Throwable): Task[Tap] = ???
}

Ваша реализация Tapдолжна удовлетворять следующему требованию:

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

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

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

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

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

Победители

Основной причиной работы над этой задачей является поиск решений для параллелизма в функциональном Scala. Это забавный маленький проект, который проведет вас в большом туре по современным, чисто функциональным системам эффектов в Scala.

Тем не менее, я хочу дать вам немного дополнительной мотивации для работы над этой проблемой!

Если вы разместите свой код в Gist, чтобы весь мир мог учиться на вашем решении, тогда я буду продвигать ваше решение и куплю вам напиток в следующий раз, когда мы окажемся в одном городе!

Наконец, если ваше решение входит в тройку лучших, которые я получу в течение следующих двух недель, я свяжусь с вами в Linkedin и напишу краткое, честное подтверждение ваших навыков в функциональной Scala.

Готов? На ваших отметках, получить набор, иди!