Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,15 @@ redis-cli shutdown

|started|feature impl|complete|feature|
|:-:|:-:|:-:|:--|
|[x]|[ ]|[ ]|Keys|
|[x]|[x]|[ ]|Keys|
|[x]|[x]|[ ]|Strings|
|[x]|[x]|[ ]|Lists|
|[x]|[x]|[ ]|Sets|
|[ ]|[ ]|[ ]|Sorted Sets|
|[x]|[x]|[ ]|Hashes|
|[ ]|[ ]|[ ]|HyperLogLog|
|[ ]|[ ]|[ ]|sscan, hscan|
|[ ]|[ ]|[ ]|sort, sortNStore|

#### Features
|started|feature impl|complete|feature|
Expand Down
87 changes: 59 additions & 28 deletions src/main/scala/com/github/mogproject/redismock/MockOperations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ package com.github.mogproject.redismock

import com.github.mogproject.redismock.entity.Key
import com.github.mogproject.redismock.storage.Storage
import com.redis.{Operations, Redis, serialization}
import com.redis.serialization.Format
import com.redis.serialization.Parse
import serialization._
import com.github.mogproject.redismock.util.StringUtil
import com.github.mogproject.redismock.util.ops._
import com.redis.{Operations, Redis}
import com.redis.serialization.{Format, Parse}
import scala.util.Random


trait MockOperations extends Operations with Storage { self: Redis =>

lazy val random = new Random(12345L)

// SORT
// sort keys in a set, and optionally pull values for them
override def sort[A](key:String,
Expand All @@ -30,23 +34,32 @@ trait MockOperations extends Operations with Storage { self: Redis =>

// KEYS
// returns all the keys matching the glob-style pattern.
override def keys[A](pattern: Any = "*")(implicit format: Format, parse: Parse[A]): Option[List[Option[A]]] = ???
override def keys[A](pattern: Any = "*")(implicit format: Format, parse: Parse[A]): Option[List[Option[A]]] = {
val r = StringUtil.globToRegex(pattern.toString)
println(r)
Some(currentDB.keys.withFilter(k => k.k.toString.matches(r)).map(k => k.k.parseOption(parse)).toList)
}

// RANDKEY
// return a randomly selected key from the currently selected DB.
@deprecated("use randomkey", "2.8") override def randkey[A](implicit parse: Parse[A]): Option[A] = ???
@deprecated("use randomkey", "2.8") override def randkey[A](implicit parse: Parse[A]): Option[A] = randomkey[A]

// RANDOMKEY
// return a randomly selected key from the currently selected DB.
override def randomkey[A](implicit parse: Parse[A]): Option[A] = ???
override def randomkey[A](implicit parse: Parse[A]): Option[A] = {
// TODO: make random after implementing 'scan'
currentDB.keys.headOption.map(_.k.parse(parse))
}

// RENAME (oldkey, newkey)
// atomically renames the key oldkey to newkey.
override def rename(oldkey: Any, newkey: Any)(implicit format: Format): Boolean = ???
override def rename(oldkey: Any, newkey: Any)(implicit format: Format): Boolean =
currentDB.renameKey(Key(oldkey), Key(newkey)) whenFalse { throw new RuntimeException("ERR no such key") }

// RENAMENX (oldkey, newkey)
// rename oldkey into newkey but fails if the destination key newkey already exists.
override def renamenx(oldkey: Any, newkey: Any)(implicit format: Format): Boolean = ???
override def renamenx(oldkey: Any, newkey: Any)(implicit format: Format): Boolean =
!exists(newkey) && rename(oldkey, newkey)

// DBSIZE
// return the size of the db.
Expand All @@ -59,47 +72,46 @@ trait MockOperations extends Operations with Storage { self: Redis =>
// DELETE (key1 key2 ..)
// deletes the specified keys.
override def del(key: Any, keys: Any*)(implicit format: Format): Option[Long] = {
val ks = Seq(key) ++ keys
ks foreach { k => currentDB.remove(Key(format(k))) }
Some(ks.size)
val oldSize = currentDB.size
(Seq(key) ++ keys) foreach { k => currentDB.remove(Key(format(k))) }
Some(oldSize - currentDB.size)
}

// TYPE (key)
// return the type of the value stored at key in form of a string.
override def getType(key: Any)(implicit format: Format): Option[String] = ???
override def getType(key: Any)(implicit format: Format): Option[String] =
Some(currentDB.get(Key(key)).map(_.valueType.toString.toLowerCase).getOrElse("none"))

// EXPIRE (key, expiry)
// sets the expire time (in sec.) for the specified key.
override def expire(key: Any, ttl: Int)(implicit format: Format): Boolean = ???
override def expire(key: Any, ttl: Int)(implicit format: Format): Boolean = pexpire(key, ttl * 1000)

// PEXPIRE (key, expiry)
// sets the expire time (in milli sec.) for the specified key.
override def pexpire(key: Any, ttlInMillis: Int)(implicit format: Format): Boolean = ???
override def pexpire(key: Any, ttlInMillis: Int)(implicit format: Format): Boolean =
currentDB.updateTTL(Key(key), ttlInMillis)

// EXPIREAT (key, unix timestamp)
// sets the expire time for the specified key.
override def expireat(key: Any, timestamp: Long)(implicit format: Format): Boolean = ???
override def expireat(key: Any, timestamp: Long)(implicit format: Format): Boolean = pexpireat(key, timestamp * 1000L)

// PEXPIREAT (key, unix timestamp)
// sets the expire timestamp in millis for the specified key.
override def pexpireat(key: Any, timestampInMillis: Long)(implicit format: Format): Boolean = ???
override def pexpireat(key: Any, timestampInMillis: Long)(implicit format: Format): Boolean =
currentDB.updateExpireAt(Key(key), timestampInMillis)

// TTL (key)
// returns the remaining time to live of a key that has a timeout
override def ttl(key: Any)(implicit format: Format): Option[Long] = ???
override def ttl(key: Any)(implicit format: Format): Option[Long] =
pttl(key).map(x => if (x < 0L) -1L else math.round(x / 1000.0))

// PTTL (key)
// returns the remaining time to live of a key that has a timeout in millis
override def pttl(key: Any)(implicit format: Format): Option[Long] = ???
override def pttl(key: Any)(implicit format: Format): Option[Long] = currentDB.getTTL(Key(key))

// SELECT (index)
// selects the DB to connect, defaults to 0 (zero).
override def select(index: Int): Boolean = if (0 <= index) {
db = index
true
} else {
false
}
override def select(index: Int): Boolean = (0 <= index) whenTrue { db = index }

// FLUSHDB the DB
// removes all the DB data.
Expand All @@ -117,20 +129,39 @@ trait MockOperations extends Operations with Storage { self: Redis =>

// MOVE
// Move the specified key from the currently selected DB to the specified destination DB.
override def move(key: Any, db: Int)(implicit format: Format): Boolean = ???
override def move(key: Any, db: Int)(implicit format: Format): Boolean = if (this.db == db) {
true
} else if (db < 0) {
false
} else {
currentDB.synchronized {
val dst = getDB(db)
dst.synchronized {
val k = Key(key)
currentDB.get(k).map { v =>
dst.update(k, v)
currentDB.getExpireAt(k) map { dst.updateExpireAt(k, _) } getOrElse dst.removeExpireAt(k)
currentDB.remove(k)
}.isDefined
}
}
}

// QUIT
// exits the server.
override def quit: Boolean = disconnect

// AUTH
// auths with the server.
override def auth(secret: Any)(implicit format: Format): Boolean = ???
override def auth(secret: Any)(implicit format: Format): Boolean = {
// always returns true in the mock
true
}

// PERSIST (key)
// Remove the existing timeout on key, turning the key from volatile (a key with an expire set)
// to persistent (a key that will never expire as no timeout is associated).
override def persist(key: Any)(implicit format: Format): Boolean = ???
override def persist(key: Any)(implicit format: Format): Boolean = currentDB.removeExpireAt(Key(key))

// SCAN
// Incrementally iterate the keys space (since 2.8)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import com.redis.serialization.Format
* key data should be stored as vector to calculate accurate hash code
* @param k key binary
*/
case class Key(k: Vector[Byte]) {
case class Key(k: Bytes) {
}

object Key {
def apply(k: Array[Byte]) = new Key(k.toVector)
def apply(k: Array[Byte]) = new Key(Bytes(k))

def apply(k: Any)(implicit format: Format): Key = apply(format(k))
def apply(k: Any)(implicit format: Format): Key = new Key(Bytes(k))
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ trait Storage {
def currentDB: Database = currentNode.getOrElseUpdate(db, new Database)

def currentNode: Node = Storage.index.getOrElseUpdate((host, port), new Node)

/**
* get specified database in the current node
*/
def getDB(db: Int): Database = currentNode.getOrElseUpdate(db, new Database)
}

object Storage {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.github.mogproject.redismock.util

import scala.util.Try

//implicit def seqToEnhancedSeq

/**
* Enhanced and safer indexed sequence class
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.github.mogproject.redismock.util


object StringUtil {
/**
* convert glob-style string to regex string
* @param glob glob-style string
* @return regex string (not Regex instance)
*/
def globToRegex(glob: String): String = {
val b = new StringBuilder
glob.foreach {
case c if "\\/$^+.()=!|.,".contains(c) =>
b += '\\'
b += c
case '?' =>
b += '.'
case '*' =>
b ++= ".*"
case c =>
b += c
}
b.result()
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.github.mogproject.redismock.util

import scala.collection.concurrent.TrieMap
import com.github.mogproject.redismock.util.ops._

/**
* Thread-safe key-value store with the time-to-live attribute
Expand Down Expand Up @@ -36,6 +37,25 @@ class TTLTrieMap[K, V] {
}
}

/**
* update datetime to be expired for the specified key
* @param key key
* @param timestamp timestamp to be expired in millis
* @return true if the key exists
*/
def updateExpireAt(key: K, timestamp: Long): Boolean = withTransaction { t =>
t.contains(key) whenTrue t.expireAt.update(key, timestamp)
}

/**
* update key's ttl
*
* @param key key
* @param ttl time to live in millis
* @return true if the key exists
*/
def updateTTL(key: K, ttl: Long): Boolean = updateExpireAt(key, now() + ttl)

/**
* get the current value from the key
*
Expand All @@ -52,10 +72,54 @@ class TTLTrieMap[K, V] {
}
}

/**
* get the datetime to be expired in millis
*
* @param key key
* @return if the key exists and the ttl is not set, then return Some(-1)
*/
def getExpireAt(key: K): Option[Long] =
withTruncate(){ if (contains(key)) Some(expireAt.getOrElse(key, -1L)) else None }

/**
* get time to live
*
* @param key key
* @return time to live in millis wrapped by Option
*/
def getTTL(key: K): Option[Long] = getExpireAt(key).map(_ - now())

/**
* rename the key
*
* @param oldKey old key
* @param newKey new key
* @return true when the old key exists
*/
def renameKey(oldKey: K, newKey: K): Boolean = synchronized {
get(oldKey) match {
case Some(v) =>
store.update(newKey, v)
expireAt.get(oldKey) foreach { t =>
expireAt.update(newKey, t)
expireAt.remove(oldKey)
}
store.remove(oldKey)
true
case None =>
false
}
}

/**
* get the lazy list of all keys
*/
def keys: Iterable[K] = store.keys

/**
* clear all the keys/values
*/
def clear(): Unit = {
def clear(): Unit = synchronized {
store.clear()
expireAt.clear()
}
Expand All @@ -75,20 +139,36 @@ class TTLTrieMap[K, V] {
* remove specified key with value
* @param key key
*/
def remove(key: K): Unit = {
def remove(key: K): Unit = synchronized {
store.remove(key)
expireAt.remove(key)
}

/**
* remove the timestamp to be expired
* This means the key will be alive forever.
*
* @param key key
* @return true if the key exists
*/
def removeExpireAt(key: K): Boolean = withTruncate(){
expireAt.remove(key)
contains(key)
}

private def truncate(time: Long): Unit = {
expireAt.foreach {
case (k, t) if t <= time => remove(k)
case _ =>
}
}

private[util] def withTruncate[A](time: Long = now())(f: => A): A = { truncate(time); f }
private[util] def withTruncate[A](time: Long = now())(f: => A): A = synchronized {
truncate(time)
f
}

def withTransaction[A](thunk: TTLTrieMap[K, V] => A): A = synchronized(thunk(this))

override def equals(other: Any): Boolean = other match {
case that: TTLTrieMap[K, V] => withTruncate()(this.store == that.store && this.expireAt == that.expireAt)
Expand Down
Loading