Сериализация сообщения с помощью protobuf для актора akka, который содержит сериализуемые данные

У меня есть постоянный актер, который может получать команду одного типа Persist(event), где событие имеет тип trait Event (существует множество его реализаций). И в случае успеха это возвращает Persisted(event) отправителю.

Само событие является сериализуемым, поскольку это данные, которые мы храним в постоянном хранилище, а сериализация реализуется с помощью настраиваемого сериализатора, который внутренне использует классы, созданные из файлов google protobuf .proto. И этот настраиваемый сериализатор настроен в application.conf и привязан к базовому признаку Event. Это уже нормально работает.

Примечание. Реализации Event - это не классы, генерируемые protobuf. Это обычные классы scala, и у них тоже есть их эквивалент в protobuf, но он отображается через настраиваемый сериализатор, привязанный к базовому типу событий. Это было сделано моими предшественниками для управления версиями (что, вероятно, не требуется, потому что с этим можно справиться с помощью простых классов protobuf + настраиваемая комбинация сериализации, но это другое дело), ​​и я не хочу менять этот банкомат.

Сейчас мы пытаемся реализовать сегментирование кластера для этого актора, что также означает, что мои команды (а именно Persist и Persisted) также должны быть сериализуемыми, поскольку они могут быть перенаправлены на другие узлы.

Это модель предметной области:

sealed trait PersistenceCommand {
  def event: Event
}

final case class Persisted(event: Event) extends PersistenceCommand
final case class Persist(event: Event) extends PersistenceCommand

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

Подход 1. Определите новый файл прототипа для Persist и Persisted, но что мне использовать в качестве типа данных для event? Я не нашел способа определить что-то вроде этого:

  message Persist {
   "com.example.Event" event = 1 // this doesn't work
   }

Таким образом, я могу использовать существующую черту Scala Event в качестве типа данных. Если это сработает, я думаю (хотя это и надумано) я мог бы привязать сгенерированный код (после компиляции этого прото-файла) к встроенному сериализатору akka для google protobuf, и это может сработать. Приведенное выше примечание объясняет, почему я не могу использовать конструкцию oneof в моем прото-файле.

Подход 2. Это то, что я реализовал, и он работает (но мне это не нравится)

По сути, я написал новый сериализатор для команд и делегировал сераизализацию и десериализацию event части команды существующему сериализатору.

class PersistenceCommandSerializer extends SerializerWithStringManifest {
  val eventSerializer: ManifestAwareEventSerializer = new ManifestAwareEventSerializer()

  val PersistManifest   = Persist.getClass.getName
  val PersistedManifest = Persisted.getClass.getName
  val Separator         = "~"

  override def identifier: Int = 808653986

  override def manifest(o: AnyRef): String = o match {
    case Persist(event)   => s"$PersistManifest$Separator${eventSerializer.manifest(event)}"
    case Persisted(event) => s"$PersistedManifest$Separator${eventSerializer.manifest(event)}"
  }

  override def toBinary(o: AnyRef): Array[Byte] = o match {
    case command: PersistenceCommand => eventSerializer.toBinary(command.event)
  }

  override def fromBinary(bytes: Array[Byte], manifest: String): AnyRef = {
    val (commandManifest, dataManifest) = splitIntoCommandAndDataManifests(manifest)
    val event                           = eventSerializer.fromBinary(bytes, dataManifest).asInstanceOf[Event]
    commandManifest match {
      case PersistManifest =>
        Persist(event)
      case PersistedManifest =>
        Persisted(event)
    }
  }

  private def splitIntoCommandAndDataManifests(manifest: String) = {
    val commandAndDataManifests = manifest.split(Separator)
    (commandAndDataManifests(0), commandAndDataManifests(1))
  }
}

Проблема с этим подходом - это то, что я делаю в def manifest и def fromBinary. Я должен был убедиться, что у меня есть манифест команды, а также манифест события при сериализации и десериализации. Следовательно, мне пришлось использовать ~ в качестве разделителя - своего рода мой собственный метод сериализации для информации манифеста.

Есть ли лучший или, возможно, правильный способ реализовать это?

Для контекста: я использую ScalaPB для создания классов scala из файлов .proto и классических актеров akka.

Мы очень ценим любое руководство!


person Niks    schedule 12.02.2020    source источник
comment
Что касается Approach 1, пробовали ли вы oneof protobuf, например, protobuf message Persists { oneof event { EventTypeX = 1; EventTypeY = 2; ... } }   -  person Sava Vranešević    schedule 13.02.2020
comment
Привет, @ SavaVranešević. Я не могу этого сделать, потому что реализации типов событий не являются классами, генерируемыми protobuf. Это обычные классы scala, и у них тоже есть их эквивалент в protobuf, но он отображается через настраиваемый сериализатор, привязанный к базовому типу Event. Это было сделано моими предшественниками для управления версиями (что, вероятно, не требуется, потому что с этим можно справиться с помощью простых классов protobuf + настраиваемая комбинация сериализации, но это другое дело), ​​и я не хочу менять этот банкомат.   -  person Niks    schedule 13.02.2020


Ответы (1)


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

Вы можете увидеть, как это делается внутри Akka для наших собственных форматов проводов, здесь: https://github.com/akka/akka/blob/6bf20f4117a8c27f8bd412228424caafe76a89eb/akka-remote/src/main/protobatsuf/WireFats/.proto#L48

person johanandren    schedule 18.02.2020