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

sttp.tapir.server.vertx.streams.Pipe.scala Maven / Gradle / Ivy

The newest version!
package sttp.tapir.server.vertx.streams

import io.vertx.core.buffer.Buffer
import io.vertx.core.http.{ServerWebSocket, WebSocketFrame => VertxWebSocketFrame}
import io.vertx.core.streams.{ReadStream, WriteStream}
import sttp.ws.WebSocketFrame
import sttp.ws.WebSocketFrame._

import java.util.concurrent.atomic.AtomicReference
import scala.annotation.tailrec

object Pipe {
  private sealed trait Action
  private case object Skip extends Action
  private case object Stop extends Action

  private sealed trait Command extends Action
  private case object Pause extends Command
  private case object Resume extends Command

  @tailrec
  def modify[A, B](ref: AtomicReference[A], f: A => (A, B)): B = {
    val oldA = ref.get
    val (newA, b) = f(oldA)
    if (ref.compareAndSet(oldA, newA)) {
      b
    } else {
      modify(ref, f)
    }
  }

  // Resume and Pause actions can be received simultanuously. This class is used in order to
  // process these actions sequentially.
  private case class BackpressureState(
      // Shows whether Resume or Pause is processed right now.
      inProgress: Boolean,
      // Counts difference between received Resume and Pause events.
      // Sometimes two Resume events can come in row. In this case second event can be ignored.
      // For example, if active stream receives Resume and then Pause. Resume must be ignored
      // because stream is already active. But Pause event also must be ignored because it
      // compensates previous Resume event.
      // This means that we should resume stream only if count is zero and pause stream only
      // if count is one.
      count: Int,
      // Queue with unprocessed actions
      queue: List[Command]
  )

  @tailrec
  private def applyBackpressureCommands[T](ref: AtomicReference[BackpressureState], request: ReadStream[T]): Unit =
    modify[BackpressureState, Action](
      ref,
      {
        case state @ (BackpressureState(false, _, Nil) | BackpressureState(true, _, _)) =>
          (state, Stop)

        case BackpressureState(false, 0, Resume :: tail) =>
          (BackpressureState(inProgress = true, 1, tail), Resume)
        case BackpressureState(false, i, Resume :: tail) =>
          (BackpressureState(inProgress = true, i + 1, tail), Skip)

        case BackpressureState(false, 1, Pause :: tail) =>
          (BackpressureState(inProgress = true, 0, tail), Pause)
        case BackpressureState(false, i, Pause :: tail) =>
          (BackpressureState(inProgress = true, i - 1, tail), Skip)
      }
    ) match {
      case Stop =>
        ()
      case act =>
        if (act == Resume) request.resume() else if (act == Pause) request.pause() else ()
        ref updateAndGet {
          case BackpressureState(true, i, commands) => BackpressureState(inProgress = false, i, commands)
          case unexpected                           => throw new Exception(s"Unexpected state $unexpected")
        }
        applyBackpressureCommands(ref, request)
    }

  // End handler can be called before all buffers are sent through data handler.
  // If endHandler unconditionally ends writeStream then part of data can be lost.
  // AtomicReference with ProgressState allows to delay ending writeStream until
  // last buffer.
  private case class ProgressState(inProgress: Int, completed: Boolean)

  def apply(request: ReadStream[Buffer], writeStream: WriteStream[Buffer]): Unit = {
    val progress = new AtomicReference(ProgressState(0, completed = false))
    val backpressure = new AtomicReference(BackpressureState(inProgress = false, 1, Nil))

    writeStream.drainHandler { _ =>
      backpressure.updateAndGet(state => state.copy(queue = state.queue :+ Resume))
      applyBackpressureCommands(backpressure, request)
    }

    request.handler((data: Buffer) => {
      progress.getAndUpdate(s => s.copy(s.inProgress + 1))
      writeStream.write(
        data,
        _ => {
          val state = progress.updateAndGet(s => s.copy(s.inProgress - 1))
          if (state.inProgress == 0 && state.completed) writeStream.end()
          ()
        }
      )
      if (writeStream.writeQueueFull()) {
        backpressure.updateAndGet(state => state.copy(queue = state.queue :+ Pause))
        applyBackpressureCommands(backpressure, request)
      }
      ()
    })
    request.endHandler { _ =>
      val state = progress.updateAndGet(_.copy(completed = true))
      if (state.inProgress == 0) writeStream.end()
      ()
    }
    request.exceptionHandler { _ =>
      writeStream.end()
      ()
    }

    request.resume()
    ()
  }

  def apply(request: ReadStream[WebSocketFrame], socket: ServerWebSocket): Unit = {
    val progress = new AtomicReference(ProgressState(0, completed = false))
    val backpressure = new AtomicReference(BackpressureState(inProgress = false, 1, Nil))

    socket.drainHandler { _ =>
      backpressure.updateAndGet(state => state.copy(queue = state.queue :+ Resume))
      applyBackpressureCommands(backpressure, request)
    }

    def writeFrame(frame: VertxWebSocketFrame): Unit =
      socket.writeFrame(
        frame,
        _ => {
          val state = progress.updateAndGet(s => s.copy(s.inProgress - 1))
          if (state.inProgress == 0 && state.completed) socket.end()
          ()
        }
      ): Unit

    request.handler((sttpFrame: WebSocketFrame) => {
      progress.getAndUpdate(s => s.copy(s.inProgress + 1))

      sttpFrame match {
        case Text(payload, finalFragment, _) =>
          writeFrame(VertxWebSocketFrame.textFrame(payload, finalFragment))
        case Binary(payload, finalFragment, _) =>
          writeFrame(VertxWebSocketFrame.binaryFrame(Buffer.buffer(payload), finalFragment))
        case Ping(payload) =>
          writeFrame(VertxWebSocketFrame.pingFrame(Buffer.buffer(payload)))
        case Pong(payload) =>
          writeFrame(VertxWebSocketFrame.pongFrame(Buffer.buffer(payload)))
        case Close(statusCode, _) =>
          socket.close(statusCode.toShort, { _ => () })
      }

      if (!socket.isClosed && socket.writeQueueFull()) {
        backpressure.updateAndGet(state => state.copy(queue = state.queue :+ Pause))
        applyBackpressureCommands(backpressure, request)
      }
      ()
    })
    request.endHandler { _ =>
      val state = progress.updateAndGet(_.copy(completed = true))
      if (state.inProgress == 0) socket.end()
      ()
    }
    request.exceptionHandler { _ =>
      socket.end()
      ()
    }

    request.resume()
    ()
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy