Как сделать с помощью Akka типобезопасные границы, безопасные для потоков? - PullRequest
0 голосов
/ 10 июля 2020

Какова общепринятая практика реализации границ модуля между группами актеров в типе akka?

TL / DR

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

Пример

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

Например: рассмотрим Алису, Боба и Чарл ie. Алисе нравится разговаривать с Бобом, а Чарл ie часто задается вопросом, как поживает Боб. Чарл ie не знает об Алисе (и не должен), и наоборот. Между каждой парой существует протокол, сообщения которого они могут получать друг от друга:

trait Protocol[ From, To ]

object Alice
{
    sealed trait BobToAlice extends Protocol[ Bob, Alice ]
    case object ApologizeToAlice extends BobToAlice
    case object LaughAtAlice extends BobToAlice
}

object Bob
{
    sealed trait AliceToBob extends Protocol[ Alice, Bob ]
    case object SingToBob extends AliceToBob
    case object ScoldBob extends AliceToBob

    sealed trait CharlieToBob extends Protocol[ Charlie, Bob ]
    case object HowYouDoinBob extends CharlieToBob
}

object Charlie
{
    sealed trait BobToCharlie extends Protocol[ Bob, Charlie ]
    case object CryToCharlie extends BobToCharlie
    case object LaughToCharlie extends BobToCharlie
}

Граница здесь - это два лица Боба: разговор с Алисой и разговор с Чарлом ie - это два разных протокола. Теперь каждый может поговорить с Бобом, не зная друг друга. Алиса, например, любит петь, но не смеется над ней, пока она это делает:

import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.scaladsl.Behaviors.same
import akka.actor.typed.{ ActorRef, Behavior }

class Alice( bob: ActorRef[ Protocol[ Alice, Bob ] ] )
{
    import Alice._
    import nl.papendorp.solipsism.protocol.Bob.{ ScoldBob, SingToBob }

    val talkToBob: Behavior[ BobToAlice ] = Behaviors.receiveMessage
    {
        case LaughAtAlice =>
            bob ! ScoldBob
            same

        case ApologizeToAlice =>
            bob ! SingToBob
            same
    }
}

Чарл ie, с другой стороны, озабочен только тем, как Боб чувствует себя в данный момент:


import akka.actor.typed.scaladsl.Behaviors.{ receiveMessage, same }
import akka.actor.typed.{ ActorRef, Behavior }

class Charlie(bob: ActorRef[Protocol[Charlie,Bob]])
{
    import Charlie._
    import nl.papendorp.solipsism.protocol.Bob.HowYouDoinBob
    
    val concerned: Behavior[BobToCharlie] = receiveMessage
    {
        case CryToCharlie =>
            bob ! HowYouDoinBob
            same

        case LaughToCharlie =>
            bob ! HowYouDoinBob
            same
    }
}

Однако эффект Алисы на настроение Боба влияет на то, как Боб разговаривает с Чарлом ie. Для этого нам нужно объединить два протокола через BobsPersonalLife, чтобы иметь возможность представлять их в рамках одного актера:

import akka.actor.typed.scaladsl.Behaviors._
import akka.actor.typed.{ ActorRef, Behavior }
import Alice.BobToAlice
import Charlie.BobToCharlie

object Bob
{
    private[ Bob ] sealed trait BobsPersonalLife

    sealed trait AliceToBob extends Protocol[Alice, Bob] with BobsPersonalLife
    case object SingToBob extends AliceToBob
    case object ScoldBob extends AliceToBob

    sealed trait CharlieToBob extends Protocol[Charlie, Bob] with BobsPersonalLife
    case object HowYouDoinBob extends CharlieToBob
}

class Bob( alice: ActorRef[BobToAlice], charlie: ActorRef[BobToCharlie] )
{
    import Alice._
    import Bob._
    import Charlie._
    
    private val happy: Behavior[ BobsPersonalLife ] = receiveMessage
    {
        case HowYouDoinBob =>
            charlie ! LaughToCharlie
            same

        case ScoldBob =>
            alice ! ApologizeToAlice
            sad

        case SingToBob =>
            alice ! LaughAtAlice
            same
    }

    val sad: Behavior[ BobsPersonalLife ] = receiveMessage
    {
        case HowYouDoinBob =>
            charlie ! CryToCharlie
            same

        case ScoldBob =>
            alice ! ApologizeToAlice
            same

        case SingToBob  =>
            alice ! LaughAtAlice
            happy
    }
}

Пока все хорошо. Мы можем создать экземпляр Алисы и Чарла ie, используя ActorRef.narrow[ _X_ToBob ]. Но как насчет Боба? Или, скорее, альтер-эго Бобов? Если мы хотим заменить Боба на Бориса, который жалуется не Чарлу ie, а Дорис, используя DorisToBob extends Protocol[ Doris, Bob ], мы больше не сможем получать сообщения от Алисы, так как нет общих супертрент AliceToBob и DorisToBob. . Внезапно BobsPersonalLife становится замком для каждого Боба, с которым может разговаривать Алиса.

Каким будет способ заменить Боба Борисом? Если бы мы использовали ActorRef.unsafeUpcast, мы потеряем безопасность типов. Если мы используем двух участников в общем состоянии, мы теряем безопасность потоков. Обертка _X_ToBob (например, Either[ AliceToBob, CharlieToBob ] или сокращенный тип объединения Дотти) также не работает, поскольку оболочка просто берет на себя роль BobsPersonalLife. когда мы просто позволяем DorisToBob унаследовать от BobsPersonalLife, мы получаем объединение всех возможных партнеров всех бобов alter-e go не может удалить ни одного из них, никогда.

Вопрос

Как мы можем добиться истинно безопасного разделения типов между Алисой и Чарлом ie внутри Боба?

1 Ответ

1 голос
/ 11 июля 2020

Я думаю, что это почти вопрос X: Y («как мне установить границы интерфейса в Akka» против «как мне достичь sh цели границ интерфейса в Akka»).

object Protocol {
  sealed trait Message

  sealed trait LaughReply extends Message
  sealed trait MoodReply extends Message
  case class Apology(from: ActorRef[Singing]) extends Message
  case class Singing(from: ActorRef[Laughing]) extends Message

  case class Laughing(from: ActorRef[LaughReply]) extends Message with MoodReply
  case class HowYouDoin(replyTo: ActorRef[MoodReply]) extends Message with LaughReply
  case class Scolding(from: ActorRef[Apology]) extends Message with LaughReply
  case class Crying(from: ActorRef[HowYouDoin]) extends Message with MoodReply
}

object Alice {
  val talkToBob: Behavior[Message] = Behaviors.receive { (context, msg) =>
    msg match {
      case Apology(from) =>
        from ! Singing(context.self)
        Behaviors.same
      case Laughing(from) =>
        Behaviors.same
        from ! Scolding(context.self)
      case _ =>  // Every other message is ignored by Alice
        Behaviors.same
    }
  }
}

object Charlie {
  val concerned: Behavior[Message] = Behaviors.receive { (context, msg) =>
    msg match {
      case Crying(from) =>
        from ! HowYouDoin(context.self)
        Behaviors.same
      case Laughing(from) =>
        from ! HowYouDoin(context.self)
        Behaviors.same
      case _ =>
        Behaviors.same
    }
  }
}

object Bob {
  val happy: Behavior[Message] = Behaviors.receive { (context, msg) =>
    msg match {
      case HowYouDoin(replyTo) =>
        replyTo ! Laughing(context.self)
        Behaviors.same
      case Scolding(from) =>
        from ! Apology(context.self)
        sad
      case Singing(from) =>
        from ! Laughing(context.self)
        Behaviors.same
      case _ =>
        Behaviors.same
    }
  }
  
  val sad: Behavior[Message] = Behaviors.receive { (context, msg) =>
    msg match {
      case HowYouDoin(replyTo) =>
        replyTo ! Crying(context.self)
        Behaviors.same
      case Scolding(from) =>
        from ! Apology(context.self)
        Behaviors.same
      case Singing(from) =>
        from ! Laughing(context.self)
        Behaviors.same
      case _ =>
        Behaviors.same
    }
  }
}

Уловка заключается в основном в декомпозиции протокола с помощью миксинов и кодировании состояния протокола (какие сообщения принимаются) в сообщениях. Пока никто не ссылается на ActorRef[Message] (ActorRef контравариантен, поэтому ActorRef[LaughReply] не ActorRef[Message]), нет способа отправить сообщение, которое цель не обязалась принять. Обратите внимание, что сохранение ActorRef в состоянии актера активно работает против этого: если вы собираетесь сохранить еще один ActorRef в состоянии своего актера, это довольно сильный признак, IMO, что вы вообще не заинтересованы в развязке их.

Альтернативой, а не всеобъемлющим протоколом, является наличие протоколов для каждого из Алисы / Боба / Чарли / и т.д. c. с командами и ответами, определенными только в контексте этого актора, и использованием, например, типизированного шаблона запроса для адаптации протокола ответа целевого актора к командному протоколу запрашивающего актора.

...