All Downloads are FREE. Search and download functionalities are using the official Maven repository.

replication.DeltaDissemination.scala Maven / Gradle / Ivy

The newest version!
package replication

import channels.*
import com.github.plokhotnyuk.jsoniter_scala.core.{JsonValueCodec, readFromArray, writeToArray}
import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker
import de.rmgk.delay.{Callback, syntax}
import rdts.base.Lattice.optionLattice
import rdts.base.{Bottom, Lattice, LocalUid, Uid}
import rdts.time.Dots
import replication.JsoniterCodecs.given
import replication.ProtocolMessage.*

import scala.util.{Failure, Success, Try}

trait Aead {
  def encrypt(plain: Array[Byte], associated: Array[Byte]): Array[Byte]
  def decrypt(cypher: Array[Byte], associated: Array[Byte]): Try[Array[Byte]]
}

object DeltaDissemination {
  def jsoniterMessages[T: JsonValueCodec](conn: LatentConnection[MessageBuffer])
      : LatentConnection[ProtocolMessage[T]] = {

    given JsonValueCodec[ProtocolMessage[T]] = JsonCodecMaker.make

    LatentConnection.adapt(
      (mb: MessageBuffer) => readFromArray[ProtocolMessage[T]](mb.asArray),
      (pm: ProtocolMessage[T]) => ArrayMessageBuffer(writeToArray(pm))
    )(conn)
  }
}

class DeltaDissemination[State](
    val replicaId: LocalUid,
    receiveCallback: State => Unit,
    crypto: Option[Aead] = None,
    immediateForward: Boolean = false,
) {

  given LocalUid = replicaId

  type ConnectionContext = Connection[ProtocolMessage[State]]

  val globalAbort = Abort()

  var connections: List[ConnectionContext] = Nil

  def debugCallbackAndRemoveCon(con: ConnectionContext): Callback[Any] =
    case Success(value) => ()
    case Failure(exception) =>
      lock.synchronized {
        connections = connections.filter(cc => cc != con)
      }
      exception.printStackTrace()

  def requestData(): Unit = {
    connections.foreach: con =>
      con.send(Request(replicaId.uid, selfContext)).run(using ())(debugCallbackAndRemoveCon(con))
  }

  def pingAll(): Unit = {
    connections.foreach { conn =>
      conn.send(Ping(System.nanoTime())).run(debugCallbackAndRemoveCon(conn))
    }
  }

  def addLatentConnection(latentConnection: LatentConnection[MessageBuffer])(using
      JsonValueCodec[State]
  ): Unit = {
    addLatentConnection(DeltaDissemination.jsoniterMessages(latentConnection))
  }

  def addLatentConnection(latentConnection: LatentConnection[ProtocolMessage[State]]): Unit = {
    val preparedConnection = latentConnection.prepare { from =>
      {
        case Success(msg)   => handleMessage(msg, from)
        case Failure(error) => error.printStackTrace()
      }
    }
    preparedConnection.run(using globalAbort) {
      case Success(conn) =>
        lock.synchronized {
          connections = conn :: connections
        }
        conn.send(Request(replicaId.uid, selfContext)).run(using ())(debugCallbackAndRemoveCon(conn))
      case Failure(ex) =>
        println(s"exception during connection activation")
        ex.printStackTrace()
    }
  }

  // note that deltas are not guaranteed to be ordered the same in the buffers
  val lock: AnyRef                               = new {}
  private var pastPayloads: List[Payload[State]] = Nil

  def allPayloads: List[Payload[State]] = pastPayloads
  private def rememberPayload(payload: Payload[State]): Unit =
    pastPayloads = payload :: pastPayloads

  private var contexts: Map[Uid, Dots] = Map.empty

  def selfContext: Dots = contexts.getOrElse(replicaId.uid, Dots.empty)

  def applyDelta(delta: State): Unit =
    val payload = lock.synchronized {
      val nextDot = selfContext.nextDot(replicaId.uid)
      val payload = Payload(replicaId.uid, Dots.single(nextDot), delta)
      updateContext(replicaId.uid, payload.dots)
      rememberPayload(payload)
      payload
    }
    disseminate(payload)

  def updateContext(rr: Uid, dots: Dots): Unit = lock.synchronized {
    contexts = contexts.updatedWith(rr)(curr => curr `merge` Some(dots))
  }

  def handleMessage(msg: ProtocolMessage[State], from: ConnectionContext): Unit = {
    msg match
      case Ping(time) =>
        from.send(Pong(time)).run(debugCallbackAndRemoveCon(from))
      case Pong(time) =>
        println(s"ping took ${(System.nanoTime() - time.toLong).doubleValue / 1000_000}ms")
      case Request(uid, knows) =>
        val relevant = lock.synchronized(pastPayloads).filterNot { dt => dt.dots <= knows }
        relevant.foreach: msg =>
          from.send(msg.addSender(replicaId.uid)).run(using ())(debugCallbackAndRemoveCon(from))
        updateContext(uid, selfContext `merge` knows)
      case payload @ Payload(uid, context, data) =>
        if context <= selfContext then return
        lock.synchronized {
          uid.foreach { uid =>
            updateContext(uid, context)
          }
          updateContext(replicaId.uid, context)
          rememberPayload(payload)
        }
        receiveCallback(data)
        if immediateForward then
          disseminate(payload, Set(from))

  }

  def disseminate(payload: Payload[State], except: Set[ConnectionContext] = Set.empty): Unit = {
    connections.filterNot(con => except.contains(con)).foreach: con =>
      con.send(payload).run(using ())(debugCallbackAndRemoveCon(con))
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy