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

com.twitter.finagle.memcached.Interpreter.scala Maven / Gradle / Ivy

There is a newer version: 21.2.0
Show newest version
package com.twitter.finagle.memcached

import com.google.common.hash.Hashing
import com.twitter.finagle.Service
import com.twitter.finagle.memcached.protocol._
import com.twitter.finagle.memcached.util.{AtomicMap, ParserUtils}
import com.twitter.io.Buf
import com.twitter.util.{Future, Time}

/**
 * Evaluates a given Memcached operation and returns the result.
 */
class Interpreter(map: AtomicMap[Buf, Entry]) {

  import ParserUtils._
  import Interpreter._

  def apply(command: Command): Response = {
    command match {
      case Set(key, flags, expiry, value) =>
        map.lock(key) { data =>
          data(key) = Entry(value, expiry)
          Stored()
        }
      case Add(key, flags, expiry, value) =>
        map.lock(key) { data =>
          val existing = data.get(key)
          existing match {
            case Some(entry) if entry.valid =>
              NotStored()
            case _ =>
              data(key) = Entry(value, expiry)
              Stored()
          }
        }
      case Replace(key, flags, expiry, value) =>
        map.lock(key) { data =>
          val existing = data.get(key)
          existing match {
            case Some(entry) if entry.valid =>
              data(key) = Entry(value, expiry)
              Stored()
            case Some(_) =>
              data.remove(key) // expired
              NotStored()
            case _ =>
              NotStored()
          }
        }
      case Append(key, flags, expiry, value: Buf) =>
        map.lock(key) { data =>
          val existing = data.get(key)
          existing match {
            case Some(entry) if entry.valid =>
              data(key) = Entry(entry.value.concat(value), expiry)
              Stored()
            case Some(_) =>
              data.remove(key) // expired
              NotStored()
            case _ =>
              NotStored()
          }
        }
      case Cas(key, flags, expiry, value, casUnique) =>
        map.lock(key) { data =>
          val existing = data.get(key)
          existing match {
            case Some(entry) if entry.valid =>
              val currentValue = entry.value
              if (casUnique.equals(generateCasUnique(currentValue))) {
                data(key) = Entry(value, expiry)
                Stored()
              } else {
                NotStored()
              }
            case _ =>
              NotStored()
          }
        }
      case Prepend(key, flags, expiry, value) =>
        map.lock(key) { data =>
          val existing = data.get(key)
          existing match {
            case Some(entry) if entry.valid =>
              data(key) = Entry(value.concat(entry.value), expiry)
              Stored()
            case Some(_) =>
              data.remove(key) // expired
              NotStored()
            case _ =>
              NotStored()
          }
        }
      case Get(keys) =>
        Values(
          keys.flatMap { key =>
            map.lock(key) { data =>
              data.get(key) filter { entry =>
                if (!entry.valid)
                  data.remove(key) // expired
                entry.valid
              } map { entry =>
                Value(key, entry.value)
              }
            }
          }
        )
      case Gets(keys) =>
        getByKeys(keys)
      case Delete(key) =>
        map.lock(key) { data =>
          if (data.remove(key).isDefined)
            Deleted()
          else
            NotFound()
        }
      case Incr(key, delta) =>
        map.lock(key) { data =>
          val existing = data.get(key)
          existing match {
            case Some(entry) if entry.valid =>
              val Buf.Utf8(existingString) = entry.value
              if (!existingString.isEmpty && !DigitsPattern.matcher(existingString).matches())
                throw new ClientError("cannot increment or decrement non-numeric value")

              val existingValue: Long =
                if (existingString.isEmpty) 0L
                else existingString.toLong

              val result: Long = existingValue + delta
              data(key) = Entry(Buf.Utf8(result.toString), entry.expiry)

              Number(result)
            case Some(_) =>
              data.remove(key) // expired
              NotFound()
            case _ =>
              NotFound()
          }
        }
      case Decr(key, value) =>
        map.lock(key) { data =>
          apply(Incr(key, -value))
        }
      case Quit() =>
        NoOp()
    }
  }

  private def getByKeys(keys: Seq[Buf]): Values = {
    Values(
      keys.flatMap { key =>
        map.lock(key) { data =>
          data.get(key).filter { entry =>
            entry.valid
          }.map { entry =>
            val value = entry.value
            Value(key, value, Some(generateCasUnique(value)))
          }
        }
      }
    )
  }

}

private[memcached] object Interpreter {
  /*
  * Using non-cryptographic goodFastHash Hashing Algorithm
  * for we only care about speed for testing.
  *
  * The real memcached uses uint64_t for cas tokens,
  * so we convert the hash to a String
  * representation of an unsigned Long so it can be
  * used as a cas token.
  */
  private[memcached] def generateCasUnique(value: Buf): Buf = {
    val hashAsUnsignedLong = Hashing.goodFastHash(32)
      .newHasher(value.length)
      .putBytes(Buf.ByteArray.Owned.extract(value))
      .hash()
      .padToLong
      .abs
    Buf.Utf8(hashAsUnsignedLong.toString)
  }
}

case class Entry(value: Buf, expiry: Time) {
  /**
   * Whether or not the cache entry has expired
   */
  def valid: Boolean = expiry == Time.epoch || Time.now < expiry
}

class InterpreterService(interpreter: Interpreter) extends Service[Command, Response] {
  def apply(command: Command) = Future(interpreter(command))
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy