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

io.reactors.protocol.backpressure-protocols.scala Maven / Gradle / Ivy

The newest version!
package io.reactors
package protocol



import io.reactors.common.IndexedSet
import io.reactors.common.concurrent.UidGenerator
import io.reactors.container.RHashMap
import scala.collection._



/** Communication patterns based on backpressure.
 *
 *  Backpressure ensures that the backpressure server has a bound on the number of
 *  events in its event queue, at all times. This is achieved by preventing the clients
 *  from sending too many events.
 *
 *  Clients must ask the backpressure server for a backpressure link. When a client
 *  receives a link, it must check if it has sufficient budget to send events to
 *  the server, and potentially wait before sending an event. The budget is spent each
 *  time that the client sends an event, and replenished when the server sends a token.
 *
 *  There are several kinds of backpressure exposed by this module:
 *
 *  - The for-all backpressure policy maintains a fixed number of tokens across all
 *    backpressure links. The advantage is that the server cannot be overwhelmed,
 *    regardless of the number of clients. The disadvantage is that some clients can
 *    fail while holding some of the tokens, in which case tokens are lost. If failures
 *    are possible in the system, such scenarios can ultimately starve the protocol.
 *  - The per-client backpressure policy maintains a fixed number of tokens per each
 *    backpressure link. The advantage is that the failure of any single client only
 *    obliviates the tokens from the client's own backpressure links, so other clients
 *    cannot be starved. The disadvantage is that the total number of clients may be
 *    unbounded, which can overwhelm the backpressure server.
 */
trait BackpressureProtocols {
  self: ServerProtocols =>

  /** Augments reactor systems with operations used to create backpressure reactors.
   */
  implicit class BackpressureSystemOps(val system: ReactorSystem) {
    /** Creates and starts a for-all backpressure server reactor.
     *
     *  @see [[io.reactors.protocol.BackpressureProtocols]]
     *
     *  @tparam T       type of the events send over the backpressure link
     *  @param budget   the total number of events that can be in the queue
     *  @return         a backpressure server channel of the new reactor
     */
    def backpressureForAll[T: Arrayable](budget: Long)(
      f: Events[T] => Unit
    ): Backpressure.Server[T] =
      system.spawn(Reactor[Backpressure.Req[T]] { self =>
        self.main.extra[Backpressure.ChannelInfo[T]](
          new Backpressure.ChannelInfo(implicitly[Arrayable[T]]))
        f(self.main.pressureForAll(budget))
      })

    /** Creates and starts a per-client backpressure server reactor.
     *
     *  @see [[io.reactors.protocol.BackpressureProtocols]]
     *
     *  @tparam T       type of the events send over the backpressure link
     *  @param budget   the total number of events that can be in the queue
     *  @return         a backpressure server channel of the new reactor
     */
    def backpressurePerClient[T: Arrayable](budget: Long)(
      f: Events[T] => Unit
    ): Backpressure.Server[T] =
      system.spawn(Reactor[Backpressure.Req[T]] { self =>
        self.main.extra[Backpressure.ChannelInfo[T]](
          new Backpressure.ChannelInfo(implicitly[Arrayable[T]]))
        f(self.main.pressurePerClient(budget))
      })
  }

  /** Methods for creating backpressure channels.
   */
  implicit class BackpressureChannelBuilderOps(val builder: ChannelBuilder) {
    /** Creates a channel with the backpressure server type.
     *
     *  The channel itself does not have backpressure logic. To start the backpressure
     *  protocol, additional methods must be called on the resulting connector.
     *
     *  @tparam T      the type of events sent over the backpressure channel
     *  @return        a connector of the backpressure request type
     */
    def backpressure[T: Arrayable]: Connector[Backpressure.Req[T]] = {
      val info = new Backpressure.ChannelInfo(implicitly[Arrayable[T]])
      builder.extra(info).open[Backpressure.Req[T]]
    }
  }

  /** Methods for starting backpressure protocols on backpressure channels.
   */
  implicit class BackpressureConnectorOps[T](val conn: Connector[Backpressure.Req[T]]) {
    /** Starts the for-all protocol on the backpressure channel.
     *
     *  @see [[io.reactors.protocol.BackpressureProtocols]]
     */
    def pressureForAll(initialBudget: Long): Events[T] = {
      implicit val a = conn.extra[Backpressure.ChannelInfo[T]].arrayable
      val system = Reactor.self.system
      val uidGen = new UidGenerator(1)
      val input = system.channels.daemon.open[T]
      val links = new IndexedSet[Channel[Long]]
      val linkstate = new RHashMap[Backpressure.Uid, Backpressure.LinkState]
      val allTokens = system.channels.daemon.shortcut.router[Long]
        .route(Router.roundRobin(links))
      var budget = initialBudget
      input.events on {
        allTokens.channel ! 1L + budget
      }
      conn.events onMatch {
        case (Backpressure.Open(tokens), response) =>
          val uid = uidGen.generate()
          val trackedTokens = tokens.inject { num =>
            val s = linkstate.applyOrNil(uid)
            if (s != null) s.budget += num
          }
          links += trackedTokens
          linkstate(uid) = new Backpressure.LinkState(trackedTokens)
          if (budget > 0) {
            allTokens.channel ! budget
            budget = 0
          }
          response ! (uid, input.channel)
        case (Backpressure.Seal(uid), _) =>
          val s = linkstate.applyOrNil(uid)
          if (s != null) {
            linkstate.remove(uid)
            links -= s.tokens
            budget += s.budget
            if (budget > 0 && links.nonEmpty) {
              allTokens.channel ! budget
              budget = 0
            }
          }
      }
      input.events
    }

    /** Starts the per-client protocol on the backpressure channel.
     *
     *  @see [[io.reactors.protocol.BackpressureProtocols]]
     */
    def pressurePerClient(initialBudget: Long): Events[T] = {
      implicit val a = conn.extra[Backpressure.ChannelInfo[T]].arrayable
      val system = Reactor.self.system
      val input = system.channels.daemon.open[T]
      conn.events onMatch {
        case (Backpressure.Open(tokens), response) =>
          val clientInput = system.channels.daemon.open[T]
          clientInput.events.pipe(input.channel)
          clientInput.events.on(tokens ! 1L)
          tokens ! initialBudget
          response ! (0L, clientInput.channel)
        case (Backpressure.Seal(_), _) =>
          // Safe to ignore the seal operation, as budget is not shared.
      }
      input.events
    }
  }

  implicit class BackpressureServerOps[T](val server: Backpressure.Server[T]) {
    /** Obtains the backpressure link from the backpressure server.
     *
     *  This method is called from the clients that can access the backpressure server.
     *  The methods returns a single-assignment variable through which the server
     *  responds with a backpressure link. Once obtained, the backpressure link can be
     *  used to send events to the server.
     *
     *  @see [[io.reactors.protocol.BackpressureProtocols]]
     */
    def link: IVar[Backpressure.Link[T]] = {
      val system = Reactor.self.system
      val tokens = system.channels.daemon.open[Long]
      val budget = RCell(0L)
      tokens.events.onEvent(budget := budget() + _)
      (server ? Backpressure.Open(tokens.channel)).map {
        case (uid, ch) =>
          Reactor.self.sysEvents onMatch {
            case ReactorTerminated => server ?! Backpressure.Seal(uid)
          }
          new Backpressure.Link(uid, ch, budget)
      }.toIVar
    }
  }
}


/** Contains backpressure types and auxiliary classes.
 */
object Backpressure {
  /** Unique identifier for established backpressure links.
   */
  type Uid = Long

  /** Type of the backpressure server channel.
   */
  type Server[T] = io.reactors.protocol.Server[Payload, (Uid, Channel[T])]

  /** Type of the backpressure request.
   */
  type Req[T] = io.reactors.protocol.Server.Req[Payload, (Uid, Channel[T])]

  /** Represents the state of the link between the client and the backpressure server.
   */
  class Link[T](
    private val uid: Long,
    private val channel: Channel[T],
    private val budget: RCell[Long]
  ) extends Serializable {
    /** A signal denoting whether event sends are available.
     *
     *  Clients can subscribe to this signal to execute operations once event sends
     *  become possible.
     */
    val available: Signal[Boolean] = budget.map(_ > 0).toSignal(false)

    /** Attempts to send an event to the server.
     *
     *  If successful, returns `true`. Otherwise, returns `false`.
     */
    def trySend(x: T): Boolean = {
      if (budget() == 0) false else {
        budget := budget() - 1
        channel ! x
        true
      }
    }
  }

  /** Holds extra info about that backpressure server channel.
   */
  class ChannelInfo[T](val arrayable: Arrayable[T])

  /** Holds state of the link.
   */
  class LinkState(val tokens: Channel[Long]) {
    var budget = 0L
  }

  /** Payload for backpressure requests.
   */
  sealed trait Payload

  /** Payload for opening a backpressure link.
   */
  case class Open(ch: Channel[Long]) extends Payload

  /** Payload for closing an existing backpressure link.
   */
  case class Seal(uid: Uid) extends Payload
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy