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

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

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

import com.twitter.finagle.memcached.protocol.ServerError
import com.twitter.finagle.memcached.protocol.text._
import com.twitter.finagle.memcached.util.ParserUtils
import com.twitter.io.Buf

/**
 * Decodes Buf-encoded Responses into Decodings. Used by the client.
 *
 * @note Class contains mutable state. Not thread-safe.
 */
private[memcached] object ClientDecoder {
  private val END: Buf = Buf.Utf8("END")
  private val ITEM: Buf = Buf.Utf8("ITEM")
  private val STAT: Buf = Buf.Utf8("STAT")
  private val VALUE: Buf = Buf.Utf8("VALUE")

  private val EmptyValueLines: ValueLines = ValueLines(Seq.empty)

  // Constant for the length of a byte array that will contain a String representation of an Int,
  // which is used in the Decoder class when converting a Buf to an Int
  private val MaxLengthOfIntString = Int.MinValue.toString.length

  private val NeedMoreData: Null = null

  private sealed trait State
  private case object AwaitingResponse extends State
  private case class AwaitingResponseOrEnd(valuesSoFar: Seq[TokensWithData]) extends State
  private case class AwaitingStatsOrEnd(valuesSoFar: Seq[Tokens]) extends State
  private case class AwaitingData(
      valuesSoFar: Seq[TokensWithData],
      tokens: Seq[Buf],
      bytesNeeded: Int) extends State
}

private[finagle] class ClientDecoder extends Decoder {
  import ClientDecoder._

  private[this] var state: State = AwaitingResponse

  private[this] val awaitingResponseContinue: Seq[Buf] => Decoding = { tokens =>
    if (isEnd(tokens)) {
      EmptyValueLines
    } else if (isStats(tokens)) {
      awaitStatsOrEnd(Seq(Tokens(tokens)))
      NeedMoreData
    } else {
      Tokens(tokens)
    }
  }

  def decode(buffer: Buf): Decoding = {
    state match {
      case AwaitingResponse =>
        decodeLine(buffer, needsData, awaitData)(awaitingResponseContinue)

      case AwaitingStatsOrEnd(linesSoFar) =>
        decodeLine(buffer, needsData, awaitData) { tokens =>
          state = AwaitingResponse
          if (isEnd(tokens)) {
            StatLines(linesSoFar)
          } else if (isStats(tokens)) {
            awaitStatsOrEnd(linesSoFar :+ Tokens(tokens))
            NeedMoreData
          } else {
            throw new ServerError("Invalid reply from STATS command")
          }
        }
      case AwaitingData(valuesSoFar, tokens, bytesNeeded) =>
        decodeData(bytesNeeded, buffer) { data =>
          awaitResponseOrEnd(
            valuesSoFar :+
              TokensWithData(tokens, data)
          )
          NeedMoreData
        }
      case AwaitingResponseOrEnd(valuesSoFar) =>
        decodeLine(buffer, needsData, awaitData) { tokens =>
          state = AwaitingResponse
          if (isEnd(tokens)) {
            ValueLines(valuesSoFar)
          } else NeedMoreData
        }
    }
  }

  private[this] def awaitData(tokens: Seq[Buf], bytesNeeded: Int): Unit = {
    state match {
      case AwaitingResponse =>
        awaitData(Nil, tokens, bytesNeeded)
      case AwaitingResponseOrEnd(valuesSoFar) =>
        awaitData(valuesSoFar, tokens, bytesNeeded)
    }
  }

  private[this] def awaitData(
    valuesSoFar: Seq[TokensWithData],
    tokens: Seq[Buf],
    bytesNeeded: Int
  ): Unit = {
    state = AwaitingData(valuesSoFar, tokens, bytesNeeded)
  }

  private[this] def awaitResponseOrEnd(valuesSoFar: Seq[TokensWithData]): Unit = {
    state = AwaitingResponseOrEnd(valuesSoFar)
  }

  private[this] def awaitStatsOrEnd(valuesSoFar: Seq[Tokens]): Unit = {
    state = AwaitingStatsOrEnd(valuesSoFar)
  }

  private[this] def isEnd(tokens: Seq[Buf]) =
    tokens.length == 1 && tokens.head == END

  private[this] def isStats(tokens: Seq[Buf]) =
    tokens.nonEmpty && (tokens.head == STAT || tokens.head == ITEM)

  private[this] val needsData: Seq[Buf] => Int = { tokens =>
    val responseName = tokens.head
    if (responseName == VALUE) {
      validateValueResponse(tokens)
      val dataLengthAsBuf = tokens(3)
      dataLengthAsBuf.write(byteArrayForBuf2Int, 0)
      ParserUtils.byteArrayStringToInt(byteArrayForBuf2Int, dataLengthAsBuf.length)
    } else -1
  }

  private[this] def validateValueResponse(args: Seq[Buf]): Unit = {
    if (args.length < 4) throw new ServerError("Too few arguments")
    if (args.length > 5) throw new ServerError("Too many arguments")
    if (args.length == 5 && !ParserUtils.isDigits(args(4))) throw new ServerError("CAS must be a number")
    if (!ParserUtils.isDigits(args(3))) throw new ServerError("Bytes must be number")
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy