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

com.twitter.finagle.memcached.protocol.text.Framer.scala Maven / Gradle / Ivy

The newest version!
package com.twitter.finagle.memcached.protocol.text

import com.twitter.finagle.framer.{Framer => FinagleFramer}
import com.twitter.finagle.memcached.util.ParserUtils
import com.twitter.io.Buf
import scala.collection.mutable.ArrayBuffer

private[memcached] object Framer {

  private sealed trait State
  private case object AwaitingTextFrame extends State
  private case class AwaitingDataFrame(bytesNeeded: Int) extends State

  private val EmptySeq = IndexedSeq.empty[Buf]

  private val TokenDelimiter: Byte = ' '
}

/**
 * Frames Bufs into Memcached frames. Memcached frames are one of two types;
 * text frames and data frames. Text frames are delimited by `\r\n`. If a text
 * frame starts with the token `VALUE`, a data frame will follow. The length of the
 * data frame is given by the string representation of the third token in the
 * text frame. The data frame also ends with `\r\n`.
 *
 * For more information, see https://github.com/memcached/memcached/blob/master/doc/protocol.txt.
 *
 * To simplify the decoding logic, we have decoupled framing and decoding; however, because of the
 * complex framing logic, we must partially decode messages during framing to frame correctly.
 *
 * @note Class contains mutable state. Not thread-safe.
 */
private[memcached] trait Framer extends FinagleFramer {
  import Framer._

  private[this] var accum: Buf = Buf.Empty

  private[this] var state: State = AwaitingTextFrame

  protected val byteArrayForBuf2Int: Array[Byte] = ParserUtils.newByteArrayForBuf2Int()

  /**
   * Return the number of bytes before `\r\n` (newline), or -1 if no newlines found
   */
  private[this] def bytesBeforeLineEnd(bytes: Array[Byte]): Int = {
    var i = 0
    while (i < bytes.length - 1) {
      if (bytes(i) == '\r' && bytes(i + 1) == '\n') {
        return i
      }
      i += 1
    }
    -1
  }

  /**
   * Using the current accumulation of Bufs, read the next frame. If no frame can be read,
   * return null.
   */
  private def extractFrame(): Buf =
    state match {
      case AwaitingTextFrame =>
        val frameLength = bytesBeforeLineEnd(Buf.ByteArray.Owned.extract(accum))
        if (frameLength < 0) {
          null
        } else {

          // We have received a text frame. Extract the frame.
          val frameBuf: Buf = accum.slice(0, frameLength)

          // Remove the extracted frame from the accumulator, stripping the newline (2 chars)
          accum = accum.slice(frameLength + 2, accum.length)

          val tokens = ParserUtils.split(Buf.ByteArray.Owned.extract(frameBuf), TokenDelimiter)

          val bytesNeeded = dataLength(tokens)

          // If the frame starts with "VALUE", we expect a data frame to follow,
          // of length `bytesNeeded`.
          if (bytesNeeded != -1) state = AwaitingDataFrame(bytesNeeded)
          frameBuf
        }
      case AwaitingDataFrame(bytesNeeded) =>

        // A data frame ends with `\r\n', so we must wait for `bytesNeeded + 2` bytes.
        if (accum.length >= bytesNeeded + 2) {

          // Extract the data frame
          val frameBuf: Buf = accum.slice(0, bytesNeeded)

          // Remove the extracted frame from the accumulator, stripping the newline (2 chars)
          accum = accum.slice(bytesNeeded + 2 , accum.length)
          state = AwaitingTextFrame
          frameBuf
        } else {
          null
        }
    }

  /**
   * Frame a Buf and any accumulated partial frames into as many Memcached frames as possible.
   */
  def apply(buf: Buf): IndexedSeq[Buf] = {
    accum = accum.concat(buf)
    var frame = extractFrame()

    if (frame != null) {
      // The average Gizmoduck memcached pipeline has 0-1 requests pending, and the average server
      // response is split into 2 memcached protocol frames, so we chose 2 as the initial array
      // size.
      val frames = new ArrayBuffer[Buf](2)
      do {
        frames += frame
        frame = extractFrame()
      } while (frame != null)

      frames.toIndexedSeq
    } else {
      EmptySeq
    }
  }

  /**
   * Given a sequence of Buf tokens that comprise a Memcached frame,
   * return the length of data expected in the next frame, or -1
   * if the length cannot be extracted.
   */
  def dataLength(tokens: IndexedSeq[Buf]): Int
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy