diff --git a/src/main/scala/com/github/mogproject/redismock/MockHashOperations.scala b/src/main/scala/com/github/mogproject/redismock/MockHashOperations.scala index f1c1d67..6e0830c 100644 --- a/src/main/scala/com/github/mogproject/redismock/MockHashOperations.scala +++ b/src/main/scala/com/github/mogproject/redismock/MockHashOperations.scala @@ -2,6 +2,7 @@ package com.github.mogproject.redismock import com.github.mogproject.redismock.entity.{Bytes, HASH, Key, HashValue} import com.github.mogproject.redismock.storage.Storage +import com.github.mogproject.redismock.util.ops._ import com.redis.{HashOperations, Redis} import com.redis.serialization._ import com.redis.serialization.Parse.parseDefault @@ -12,80 +13,67 @@ import scala.util.Try trait MockHashOperations extends HashOperations with MockOperations with Storage { self: Redis => - private[this] def setRaw(key: Any, rawValue: Map[Bytes, Bytes])(implicit format: Format): Unit = { + private def setRaw(key: Any, rawValue: Map[Bytes, Bytes])(implicit format: Format): Unit = { currentDB.update(Key(key), HashValue(rawValue)) } - private[this] def getRaw(key: Any)(implicit format: Format): Option[HASH.DataType] = - currentDB.get(Key(format.apply(key))).map(_.as(HASH)) + private def getRaw(key: Any)(implicit format: Format): Option[HASH.DataType] = + currentDB.get(Key(key)).map(_.as(HASH)) - private[this] def getRawOrEmpty(key: Any)(implicit format: Format): HASH.DataType = + private def getRawOrEmpty(key: Any)(implicit format: Format): HASH.DataType = getRaw(key).getOrElse(Map.empty[Bytes, Bytes]) + private def getLong(key: Any, field: Any)(implicit format: Format): Option[Long] = hget(key, field).map { v => + Try(v.toLong).getOrElse(throw new RuntimeException("ERR hash value is not an integer or out of range")) + } + + private def getLongOrZero(key: Any, field: Any)(implicit format: Format): Long = getLong(key, field).getOrElse(0L) + + private def getFloat(key: Any, field: Any)(implicit format: Format): Option[Float] = hget(key, field).map { v => + Try(v.toFloat).getOrElse(throw new RuntimeException("ERR hash value is not a valid float")) + } + + private def getFloatOrZero(key: Any, field: Any)(implicit format: Format): Float = + getFloat(key, field).getOrElse(0.0f) + - override def hset(key: Any, field: Any, value: Any)(implicit format: Format): Boolean = currentDB.synchronized { - val m = getRawOrEmpty(key) - setRaw(key, m.updated(Bytes(field), Bytes(value))) - m.isEmpty + override def hset(key: Any, field: Any, value: Any)(implicit format: Format): Boolean = withDB { + getRawOrEmpty(key) <| { h => setRaw(key, h.updated(Bytes(field), Bytes(value)))} |> {_.isEmpty} } - override def hsetnx(key: Any, field: Any, value: Any)(implicit format: Format): Boolean = currentDB.synchronized { - getRaw(key) match { - case Some(_) => false - case None => - setRaw(key, Map(Bytes(field) -> Bytes(value))) - true - } + override def hsetnx(key: Any, field: Any, value: Any)(implicit format: Format): Boolean = withDB { + !exists(key) <| { _ => setRaw(key, Map(Bytes(field) -> Bytes(value)))} } override def hget[A](key: Any, field: Any)(implicit format: Format, parse: Parse[A]): Option[A] = getRaw(key).flatMap(_.get(Bytes(field))).map(_.parse(parse)) - override def hmset(key: Any, map: Iterable[Product2[Any, Any]])(implicit format: Format): Boolean = - currentDB.synchronized { - val m = getRawOrEmpty(key) - setRaw(key, m ++ map.map { case (k, v) => Bytes(k) -> Bytes(v) }.toMap) - m.isEmpty - } + override def hmset(key: Any, map: Iterable[Product2[Any, Any]])(implicit format: Format): Boolean = withDB { + val m = map.map { case (k: Any, v: Any) => Bytes(k) -> Bytes(v)}.toMap + getRawOrEmpty(key) ++ m <| {setRaw(key, _)} |> {_.isEmpty} + } override def hmget[K, V](key: Any, fields: K*)(implicit format: Format, parseV: Parse[V]): Option[Map[K, V]] = getRaw(key).map(m => fields.flatMap(f => m.get(Bytes(f)).map(_.parse(parseV)).map(f -> _)).toMap) - override def hincrby(key: Any, field: Any, value: Int)(implicit format: Format): Option[Long] = - currentDB.synchronized { - val n = (for { - m <- getRaw(key) - v <- m.get(Bytes(field)) - } yield { - Try(v.parse(parseDefault).toLong).getOrElse(throw new RuntimeException("ERR hash value is not an integer or out of range")) - }).getOrElse(0L) + value - hset(key, field, n) - Some(n) - } - - override def hincrbyfloat(key: Any, field: Any, value: Float)(implicit format: Format): Option[Float] = - currentDB.synchronized { - val n = (for { - m <- getRaw(key) - v <- m.get(Bytes(field)) - } yield { - Try(v.parse(parseDefault).toFloat).getOrElse(throw new RuntimeException("ERR hash value is not a valid float")) - }).getOrElse(0.0f) + value - hset(key, field, n) - Some(n) - } + override def hincrby(key: Any, field: Any, value: Int)(implicit format: Format): Option[Long] = withDB { + getLongOrZero(key, field) + value <| { x => hset(key, field, x)} |> Some.apply + } + + override def hincrbyfloat(key: Any, field: Any, value: Float)(implicit format: Format): Option[Float] = withDB { + getFloatOrZero(key, field) + value <| { x => hset(key, field, x)} |> Some.apply + } override def hexists(key: Any, field: Any)(implicit format: Format): Boolean = getRawOrEmpty(key).contains(Bytes(field)) - override def hdel(key: Any, field: Any, fields: Any*)(implicit format: Format): Option[Long] = - currentDB.synchronized { - val fs = (field :: fields.toList).map(Bytes.apply) - val x = getRawOrEmpty(key) - val y = x.filterKeys(!fs.contains(_)) - setRaw(key, y) - Some(x.size - y.size) - } + override def hdel(key: Any, field: Any, fields: Any*)(implicit format: Format): Option[Long] = withDB { + val fs = (field :: fields.toList).map(Bytes.apply) + val x = getRawOrEmpty(key) + val y = x.filterKeys(!fs.contains(_)) + setRaw(key, y) + Some(x.size - y.size) + } override def hlen(key: Any)(implicit format: Format): Option[Long] = getRaw(key).map(_.size) @@ -96,7 +84,7 @@ trait MockHashOperations extends HashOperations with MockOperations with Storage getRaw(key).map(_.values.toList.map(_.parse(parse))) override def hgetall[K, V](key: Any)(implicit format: Format, parseK: Parse[K], parseV: Parse[V]): Option[Map[K, V]] = - getRaw(key).map(_.map { case (k, v) => k.parse(parseK) -> v.parse(parseV) }) + getRaw(key).map(_.map { case (k, v) => k.parse(parseK) -> v.parse(parseV)}) // HSCAN // Incrementally iterate hash fields and associated values (since 2.8) diff --git a/src/main/scala/com/github/mogproject/redismock/MockListOperations.scala b/src/main/scala/com/github/mogproject/redismock/MockListOperations.scala index b361ea6..68c31de 100644 --- a/src/main/scala/com/github/mogproject/redismock/MockListOperations.scala +++ b/src/main/scala/com/github/mogproject/redismock/MockListOperations.scala @@ -4,6 +4,7 @@ import com.github.mogproject.redismock.entity.{Bytes, LIST, Key, ListValue} import com.github.mogproject.redismock.storage.Storage import com.redis.{Redis, ListOperations} import com.redis.serialization._ +import com.github.mogproject.redismock.util.ops._ import com.github.mogproject.redismock.util.Implicits._ import scala.annotation.tailrec @@ -14,9 +15,8 @@ trait MockListOperations extends ListOperations with MockOperations with Storage // helper functions // - private def setRaw(key: Any, rawValue: Traversable[Bytes])(implicit format: Format): Unit = { + private def setRaw(key: Any, rawValue: Traversable[Bytes])(implicit format: Format): Unit = currentDB.update(Key(key), ListValue(rawValue.toVector)) - } private def getRaw(key: Any)(implicit format: Format): Option[LIST.DataType] = currentDB.get(Key(key)).map(_.as(LIST)) @@ -26,9 +26,7 @@ trait MockListOperations extends ListOperations with MockOperations with Storage // LPUSH (Variadic: >= 2.4) // add values to the head of the list stored at key override def lpush(key: Any, value: Any, values: Any*)(implicit format: Format): Option[Long] = withDB { - val v = (value :: values.toList).map(Bytes.apply).toVector ++ getRawOrEmpty(key) - setRaw(key, v) - Some(v.size) + (value :: values.toList).map(Bytes.apply) ++ getRawOrEmpty(key) <| (setRaw(key, _)) |> { v => Some(v.size)} } // LPUSHX (Variadic: >= 2.4) @@ -38,9 +36,7 @@ trait MockListOperations extends ListOperations with MockOperations with Storage // RPUSH (Variadic: >= 2.4) // add values to the tail of the list stored at key override def rpush(key: Any, value: Any, values: Any*)(implicit format: Format): Option[Long] = withDB { - val v = getRawOrEmpty(key) ++ (value :: values.toList).map(Bytes.apply).toVector - setRaw(key, v) - Some(v.size) + getRawOrEmpty(key) ++ (value :: values.toList).map(Bytes.apply) <| (setRaw(key, _)) |> { v => Some(v.size)} } // RPUSHX (Variadic: >= 2.4) @@ -56,7 +52,8 @@ trait MockListOperations extends ListOperations with MockOperations with Storage // LRANGE // return the specified elements of the list stored at the specified key. // Start and end are zero-based indexes. - override def lrange[A](key: Any, start: Int, end: Int)(implicit format: Format, parse: Parse[A]): Option[List[Option[A]]] = { + override def lrange[A](key: Any, start: Int, end: Int) + (implicit format: Format, parse: Parse[A]): Option[List[Option[A]]] = { getRaw(key) map { _.sliceFromTo(start, end).map(_.parseOption(parse)).toList } @@ -148,65 +145,38 @@ trait MockListOperations extends ListOperations with MockOperations with Storage } } - override def brpoplpush[A](srcKey: Any, dstKey: Any, timeoutInSeconds: Int)(implicit format: Format, parse: Parse[A]): Option[A] = { - @tailrec - def loop(limit: Long): Option[A] = { - if (limit != 0 && limit <= System.currentTimeMillis()) { - None - } else { - getRaw(srcKey) match { - case Some(bs) if bs.nonEmpty => rpoplpush(srcKey, dstKey) // TODO: reduce # of getRaw (to be once) and be atomic - case _ => Thread.sleep(500L); loop(limit) - } - } - } + override def brpoplpush[A](srcKey: Any, dstKey: Any, timeoutInSeconds: Int) + (implicit format: Format, parse: Parse[A]): Option[A] = + loopUntilFound(List(srcKey))(rpoplpush(_, dstKey))(getTimeLimit(timeoutInSeconds)) - loop(if (timeoutInSeconds == 0) 0 else System.currentTimeMillis() + timeoutInSeconds * 1000L) + override def blpop[K, V](timeoutInSeconds: Int, key: K, keys: K*) + (implicit format: Format, parseK: Parse[K], parseV: Parse[V]): Option[(K, V)] = { + loopUntilFound(key :: keys.toList){ k => lpop(k)(format, parseV).map((k, _))}(getTimeLimit(timeoutInSeconds)) } - override def blpop[K, V](timeoutInSeconds: Int, key: K, keys: K*)(implicit format: Format, parseK: Parse[K], parseV: Parse[V]): Option[(K, V)] = { - val ks = key #:: keys.toStream - - @tailrec - def loop(limit: Long): Option[(K, V)] = { - if (limit != 0 && limit <= System.currentTimeMillis()) { - None - } else { - val results = ks.map(k => (k, getRawOrEmpty(k))) - results.find(_._2.nonEmpty) match { - case Some((k, _)) => - // TODO: refactor and be atomic - Some((k, lpop(k)(format, parseV).get)) - case _ => - Thread.sleep(500L) - loop(limit) - } - } - } - - loop(if (timeoutInSeconds == 0) 0 else System.currentTimeMillis() + timeoutInSeconds * 1000L) + override def brpop[K, V](timeoutInSeconds: Int, key: K, keys: K*) + (implicit format: Format, parseK: Parse[K], parseV: Parse[V]): Option[(K, V)] = { + loopUntilFound(key :: keys.toList){ k => rpop(k)(format, parseV).map((k, _))}(getTimeLimit(timeoutInSeconds)) } - override def brpop[K, V](timeoutInSeconds: Int, key: K, keys: K*)(implicit format: Format, parseK: Parse[K], parseV: Parse[V]): Option[(K, V)] = { - val ks = key #:: keys.toStream - - @tailrec - def loop(limit: Long): Option[(K, V)] = { - if (limit != 0 && limit <= System.currentTimeMillis()) { - None - } else { - val results = ks.map(k => (k, getRawOrEmpty(k))) - results.find(_._2.nonEmpty) match { - case Some((k, _)) => - // TODO: refactor and be atomic - Some((k, rpop(k)(format, parseV).get)) - case _ => - Thread.sleep(500L) - loop(limit) - } + private def findFirstNonEmpty[K](keys: Seq[K])(implicit format: Format): Option[K] = + keys.find(getRawOrEmpty(_).nonEmpty) + + @tailrec + private def loopUntilFound[K, A](keys: Seq[K])(task: K => Option[A])(limit: Long): Option[A] = { + if (limit != 0 && limit <= System.currentTimeMillis()) { + None + } else { + val result = withDB {findFirstNonEmpty(keys).flatMap(task)} + result match { + case Some(_) => result + case None => + Thread.sleep(500L) + loopUntilFound(keys)(task)(limit) } } - - loop(if (timeoutInSeconds == 0) 0 else System.currentTimeMillis() + timeoutInSeconds * 1000L) } + + private def getTimeLimit(timeoutInSeconds: Int): Long = + if (timeoutInSeconds == 0) 0 else System.currentTimeMillis() + timeoutInSeconds * 1000L } diff --git a/src/main/scala/com/github/mogproject/redismock/MockOperations.scala b/src/main/scala/com/github/mogproject/redismock/MockOperations.scala index 1482b6a..31a83ca 100644 --- a/src/main/scala/com/github/mogproject/redismock/MockOperations.scala +++ b/src/main/scala/com/github/mogproject/redismock/MockOperations.scala @@ -9,35 +9,36 @@ import com.redis.serialization.{Format, Parse} import scala.util.Random -trait MockOperations extends Operations with Storage { self: Redis => +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, - limit:Option[(Int, Int)] = None, - desc:Boolean = false, - alpha:Boolean = false, - by:Option[String] = None, - get:List[String] = Nil)(implicit format:Format, parse:Parse[A]):Option[List[Option[A]]] = ??? + override def sort[A](key: String, + limit: Option[(Int, Int)] = None, + desc: Boolean = false, + alpha: Boolean = false, + by: Option[String] = None, + get: List[String] = Nil)(implicit format: Format, parse: Parse[A]): Option[List[Option[A]]] = ??? // SORT with STORE // sort keys in a set, and store result in the supplied key - override def sortNStore[A](key:String, - limit:Option[(Int, Int)] = None, - desc:Boolean = false, - alpha:Boolean = false, - by:Option[String] = None, - get:List[String] = Nil, - storeAt: String)(implicit format:Format, parse:Parse[A]):Option[Long] = ??? + override def sortNStore[A](key: String, + limit: Option[(Int, Int)] = None, + desc: Boolean = false, + alpha: Boolean = false, + by: Option[String] = None, + get: List[String] = Nil, + storeAt: String)(implicit format: Format, parse: Parse[A]): Option[Long] = ??? // 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]]] = { val r = StringUtil.globToRegex(pattern.toString) - println(r) - Some(currentDB.keys.withFilter(k => k.k.parse().matches(r)).map(k => k.k.parseOption(parse)).toList) + currentDB.keys.view.map(_.k).filter(_.parse(Parse.parseStringSafe).matches(r)) + .map(_.parseOption(parse)).toList |> Some.apply } // RANDKEY @@ -54,12 +55,13 @@ trait MockOperations extends Operations with Storage { self: Redis => // RENAME (oldkey, newkey) // atomically renames the key oldkey to newkey. 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") } + 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 = withDB { !exists(newkey) && rename(oldkey, newkey) + } // DBSIZE // return the size of the db. @@ -67,20 +69,20 @@ trait MockOperations extends Operations with Storage { self: Redis => // EXISTS (key) // test if the specified key exists. - override def exists(key: Any)(implicit format: Format): Boolean = currentDB.contains(Key(format(key))) + override def exists(key: Any)(implicit format: Format): Boolean = Key(key) |> currentDB.contains // DELETE (key1 key2 ..) // deletes the specified keys. override def del(key: Any, keys: Any*)(implicit format: Format): Option[Long] = { val oldSize = currentDB.size - (Seq(key) ++ keys) foreach { k => currentDB.remove(Key(format(k))) } + (key :: keys.toList) foreach {Key(_) |> currentDB.remove} 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] = - Some(currentDB.get(Key(key)).map(_.valueType.toString.toLowerCase).getOrElse("none")) + (Key(key) |> currentDB.get).map(_.valueType.toString.toLowerCase).getOrElse("none") |> Some.apply // EXPIRE (key, expiry) // sets the expire time (in sec.) for the specified key. @@ -111,40 +113,29 @@ trait MockOperations extends Operations with Storage { self: Redis => // SELECT (index) // selects the DB to connect, defaults to 0 (zero). - override def select(index: Int): Boolean = (0 <= index) whenTrue { db = index } + override def select(index: Int): Boolean = (0 <= index) whenTrue {db = index} // FLUSHDB the DB // removes all the DB data. - override def flushdb: Boolean = { - currentDB.clear() - true - } + override def flushdb: Boolean = true whenTrue currentDB.clear() // FLUSHALL the DB's // removes data from all the DB's. - override def flushall: Boolean = { - currentNode.clear() - true - } + override def flushall: Boolean = true whenTrue currentNode.clear() // 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 = if (this.db == db) { - true - } else if (db < 0) { false - } else { - currentDB.synchronized { + } else withDB { + val k = Key(key) + currentDB.getWithExpireAt(k).map { case (v, t) => 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 + dst.update(k, v, t) + currentDB.remove(k) } - } + }.isDefined } // QUIT @@ -153,10 +144,7 @@ trait MockOperations extends Operations with Storage { self: Redis => // AUTH // auths with the server. - override def auth(secret: Any)(implicit format: Format): Boolean = { - // always returns true in the mock - true - } + override def auth(secret: Any)(implicit format: Format): Boolean = true // always returns true in the mock // PERSIST (key) // Remove the existing timeout on key, turning the key from volatile (a key with an expire set) @@ -165,5 +153,9 @@ trait MockOperations extends Operations with Storage { self: Redis => // SCAN // Incrementally iterate the keys space (since 2.8) - override def scan[A](cursor: Int, pattern: Any = "*", count: Int = 10)(implicit format: Format, parse: Parse[A]): Option[(Option[Int], Option[List[Option[A]]])] = ??? + override def scan[A](cursor: Int, pattern: Any = "*", count: Int = 10) + (implicit format: Format, parse: Parse[A]) + : Option[(Option[Int], Option[List[Option[A]]])] = + // TODO: implement + ??? } diff --git a/src/main/scala/com/github/mogproject/redismock/MockSetOperations.scala b/src/main/scala/com/github/mogproject/redismock/MockSetOperations.scala index 1677636..cc7bf57 100644 --- a/src/main/scala/com/github/mogproject/redismock/MockSetOperations.scala +++ b/src/main/scala/com/github/mogproject/redismock/MockSetOperations.scala @@ -56,9 +56,11 @@ trait MockSetOperations extends SetOperations with MockOperations with Storage { // Remove and return (pop) a random element from the Set value at key. override def spop[A](key: Any)(implicit format: Format, parse: Parse[A]): Option[A] = withDB { // TODO: random choice - val (h, t) = getRawOrEmpty(key).splitAt(1) - h.headOption.map { x => - setRaw(key, t) + for { + s <- getRaw(key) + x <- s.headOption + } yield { + setRaw(key, s.tail) x.parse(parse) } } diff --git a/src/main/scala/com/github/mogproject/redismock/storage/Storage.scala b/src/main/scala/com/github/mogproject/redismock/storage/Storage.scala index 4cd53db..386390b 100644 --- a/src/main/scala/com/github/mogproject/redismock/storage/Storage.scala +++ b/src/main/scala/com/github/mogproject/redismock/storage/Storage.scala @@ -19,7 +19,10 @@ trait Storage { /** * get specified database in the current node */ - def getDB(db: Int): Database = currentNode.getOrElseUpdate(db, new Database) + def getDB(db: Int): Database = { + require(db >= 0) + currentNode.getOrElseUpdate(db, new Database) + } /** * syntax sugar for executing atomic tasks with current DB diff --git a/src/main/scala/com/github/mogproject/redismock/util/TTLTrieMap.scala b/src/main/scala/com/github/mogproject/redismock/util/TTLTrieMap.scala index a82d501..5fed24b 100644 --- a/src/main/scala/com/github/mogproject/redismock/util/TTLTrieMap.scala +++ b/src/main/scala/com/github/mogproject/redismock/util/TTLTrieMap.scala @@ -9,6 +9,7 @@ import com.github.mogproject.redismock.util.ops._ class TTLTrieMap[K, V] { private[util] final val store = TrieMap.empty[K, V] private[util] final val expireAt = TrieMap.empty[K, Long] + private val neverExpire = -1L private[this] def now(): Long = System.currentTimeMillis() @@ -18,9 +19,7 @@ class TTLTrieMap[K, V] { * @param key key * @param value value */ - def update(key: K, value: V): Unit = { - store.update(key, value) - } + def update(key: K, value: V): Unit = store.update(key, value) /** * update key/value with ttl @@ -29,12 +28,18 @@ class TTLTrieMap[K, V] { * @param value value * @param ttl time to live in millis (None means infinite ttl) */ - def update(key: K, value: V, ttl: Option[Long]): Unit = { + def update(key: K, value: V, ttl: Option[Long]): Unit = update(key, value, ttl.map(now() + _).getOrElse(neverExpire)) + + /** + * update key/value with datetime to be expired + * + * @param key key + * @param value value + * @param timestamp timestamp to be expired in millis (-1: never expired) + */ + def update(key: K, value: V, timestamp: Long): Unit = synchronized { store.update(key, value) - ttl match { - case Some(t) => expireAt.update(key, now() + t) - case None => expireAt.remove(key) - } + if (timestamp == neverExpire) expireAt.remove(key) else expireAt.update(key, timestamp) } /** @@ -63,14 +68,17 @@ class TTLTrieMap[K, V] { * @return return None if the key doesn't exist or has expired, * otherwise return the value wrapped with Some */ - def get(key: K): Option[V] = { - if (expireAt.get(key).exists(_ <= now())) { - remove(key) - None - } else { - store.get(key) - } - } + def get(key: K): Option[V] = withTruncate() {store.get(key)} + + /** + * get the current value and the datetime to be expired in millis + * + * @param key key + * @return return None if the key doesn't exist or has expired, + * otherwise return 2-tuple of the value and the expired-at (-1: never expired) wrapped with Some + */ + def getWithExpireAt(key: K): Option[(V, Long)] = + withTruncate() {store.get(key).map { v => (v, expireAt.getOrElse(key, neverExpire))}} /** * get the datetime to be expired in millis @@ -78,8 +86,7 @@ class TTLTrieMap[K, V] { * @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 } + def getExpireAt(key: K): Option[Long] = getWithExpireAt(key).map(_._2) /** * get time to live @@ -97,24 +104,16 @@ class TTLTrieMap[K, V] { * @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 - } + (getWithExpireAt(oldKey) map { case (v, t) => + update(newKey, v, t) + remove(oldKey) + }).isDefined } /** * get the lazy list of all keys */ - def keys: Iterable[K] = store.keys + def keys: Iterable[K] = withTruncate()(store.keys) /** * clear all the keys/values @@ -151,7 +150,7 @@ class TTLTrieMap[K, V] { * @param key key * @return true if the key exists */ - def removeExpireAt(key: K): Boolean = withTruncate(){ + def removeExpireAt(key: K): Boolean = withTruncate() { expireAt.remove(key) contains(key) }