Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support GETEX and GETDEL #340

Merged
merged 10 commits into from
Apr 13, 2021
Merged
Show file tree
Hide file tree
Changes from 4 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
26 changes: 26 additions & 0 deletions redis/src/main/scala/zio/redis/Input.scala
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,32 @@ object Input {
def encode(data: Update): Chunk[RespValue.BulkString] = Chunk.single(encodeString(data.stringify))
}

case object GetExPersistInput extends Input[(String, Boolean)] {
override private[redis] def encode(data: (String, Boolean)): Chunk[RespValue.BulkString] =
if (data._2) Chunk(encodeString(data._1), encodeString("PERSIST")) else Chunk(encodeString(data._1))
}
case object GetExInput extends Input[(String, Expire, Duration)] {
override private[redis] def encode(data: (String, Expire, Duration)): Chunk[RespValue.BulkString] =
data match {
case (key, Expire.SetExpireSeconds, duration) =>
Chunk(encodeString(key), encodeString("EX")) ++ DurationSecondsInput.encode(duration)
case (key, Expire.SetExpireMilliseconds, duration) =>
Chunk(encodeString(key), encodeString("PX")) ++ DurationMillisecondsInput.encode(duration)
case _ => Chunk(encodeString(data._1))
}
}

case object GetExAtInput extends Input[(String, ExpiredAt, Instant)] {
override private[redis] def encode(data: (String, ExpiredAt, Instant)): Chunk[RespValue.BulkString] =
data match {
case (key, ExpiredAt.SetExpireAtSeconds, instant) =>
Chunk(encodeString(key), encodeString("EXAT")) ++ TimeSecondsInput.encode(instant)
case (key, ExpiredAt.SetExpireAtMilliseconds, instant) =>
Chunk(encodeString(key), encodeString("PXAT")) ++ TimeMillisecondsInput.encode(instant)
case _ => Chunk(encodeString(data._1))
}
}

final case class Varargs[-A](input: Input[A]) extends Input[Iterable[A]] {
def encode(data: Iterable[A]): Chunk[RespValue.BulkString] =
data.foldLeft(Chunk.empty: Chunk[RespValue.BulkString])((acc, a) => acc ++ input.encode(a))
Expand Down
60 changes: 60 additions & 0 deletions redis/src/main/scala/zio/redis/api/Strings.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package zio.redis.api

import java.time.Instant

import zio.duration._
import zio.redis.Input._
import zio.redis.Output._
Expand Down Expand Up @@ -123,6 +125,51 @@ trait Strings {
final def getSet(key: String, value: String): ZIO[RedisExecutor, RedisError, Option[String]] =
GetSet.run((key, value))

/**
* Get the string value of a key and delete it on success (if and only if the key's value type is a string)
*
* @param key Key to get the value of
* @return Returns the value of the string or None if it did not previously have a value
*/
final def getDel(key: String): ZIO[RedisExecutor, RedisError, Option[String]] =
GetDel.run(key)

/**
* Get the value of key and set its expiration
*
* @param key Key to get the value of
* @param expire The option which can modify command behavior. e.g. use `Expire.SetExpireSeconds` set the specified expire time in seconds
* @param expireTime Time in seconds/milliseconds until the string should expire
* @return Returns the value of the string or None if it did not previously have a value
*/
final def getEx(key: String, expire: Expire, expireTime: Duration): ZIO[RedisExecutor, RedisError, Option[String]] =
GetEx.run((key, expire, expireTime))

/**
* Get the value of key and set its expiration
*
* @param key Key to get the value of
* @param expiredAt The option which can modify command behavior. e.g. use `Expire.SetExpireAtSeconds` set the specified Unix time at which the key will expire in seconds
* @param timestamp an absolute Unix timestamp (seconds/milliseconds since January 1, 1970)
* @return Returns the value of the string or None if it did not previously have a value
*/
final def getExAt(
key: String,
expiredAt: ExpiredAt,
timestamp: Instant
): ZIO[RedisExecutor, RedisError, Option[String]] =
GetExAt.run((key, expiredAt, timestamp))

/**
* Get the value of key and remove the time to live associated with the key
*
* @param key Key to get the value of
* @param persist if true, remove the time to live associated with the key, otherwise not
* @return Returns the value of the string or None if it did not previously have a value
*/
final def getEx(key: String, persist: Boolean): ZIO[RedisExecutor, RedisError, Option[String]] =
GetExPersist.run((key, persist))

/**
* Increment the integer value of a key by one
*
Expand Down Expand Up @@ -263,6 +310,7 @@ trait Strings {
}

private[redis] object Strings {

final val Append: RedisCommand[(String, String), Long] =
RedisCommand("APPEND", Tuple2(StringInput, StringInput), LongOutput)

Expand Down Expand Up @@ -299,6 +347,18 @@ private[redis] object Strings {
final val GetSet: RedisCommand[(String, String), Option[String]] =
RedisCommand("GETSET", Tuple2(StringInput, StringInput), OptionalOutput(MultiStringOutput))

final val GetDel: RedisCommand[String, Option[String]] =
RedisCommand("GETDEL", StringInput, OptionalOutput(MultiStringOutput))

final val GetEx: RedisCommand[(String, Expire, Duration), Option[String]] =
RedisCommand("GETEX", GetExInput, OptionalOutput(MultiStringOutput))

final val GetExAt: RedisCommand[(String, ExpiredAt, Instant), Option[String]] =
RedisCommand("GETEX", GetExAtInput, OptionalOutput(MultiStringOutput))

final val GetExPersist: RedisCommand[(String, Boolean), Option[String]] =
RedisCommand("GETEX", GetExPersistInput, OptionalOutput(MultiStringOutput))

final val Incr: RedisCommand[String, Long] = RedisCommand("INCR", StringInput, LongOutput)

final val IncrBy: RedisCommand[(String, Long), Long] =
Expand Down
25 changes: 25 additions & 0 deletions redis/src/main/scala/zio/redis/options/Shared.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package zio.redis.options

trait Shared {

sealed trait Update { self =>
private[redis] final def stringify: String =
self match {
Expand All @@ -18,6 +19,30 @@ trait Shared {
case object SetGreaterThan extends Update
}

sealed trait Expire { self =>
private[redis] final def stringify: String =
self match {
case Expire.SetExpireSeconds => "EX"
case Expire.SetExpireMilliseconds => "PX"
}
}
object Expire {
case object SetExpireSeconds extends Expire
case object SetExpireMilliseconds extends Expire
}

sealed trait ExpiredAt { self =>
private[redis] final def stringify: String =
self match {
case ExpiredAt.SetExpireAtSeconds => "EXAT"
case ExpiredAt.SetExpireAtMilliseconds => "PXAT"
}
}
object ExpiredAt {
case object SetExpireAtSeconds extends ExpiredAt
case object SetExpireAtMilliseconds extends ExpiredAt
}

sealed case class Count(count: Long)

sealed trait Order { self =>
Expand Down
35 changes: 35 additions & 0 deletions redis/src/test/scala/zio/redis/InputSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1059,6 +1059,41 @@ object InputSpec extends BaseSpec {
testM("valid value") {
Task(RankInput.encode(Rank(10L))).map(assert(_)(equalTo(respArgs("RANK", "10"))))
}
),
suite("GetEx")(
testM("GetExInput - valid value") {
for {
resultSeconds <- Task(GetExInput.encode(scala.Tuple3.apply("key", Expire.SetExpireSeconds, 1.second)))
resultMilliseconds <- Task(GetExInput.encode(scala.Tuple3("key", Expire.SetExpireMilliseconds, 100.millis)))
} yield assert(resultSeconds)(equalTo(respArgs("key", "EX", "1"))) && assert(resultMilliseconds)(
equalTo(respArgs("key", "PX", "100"))
)
},
testM("GetExAtInput - valid value") {
for {
resultSeconds <-
Task(
GetExAtInput.encode(
scala.Tuple3("key", ExpiredAt.SetExpireAtSeconds, Instant.parse("2021-04-06T00:00:00Z"))
)
)
resultMilliseconds <-
Task(
GetExAtInput.encode(
scala.Tuple3("key", ExpiredAt.SetExpireAtMilliseconds, Instant.parse("2021-04-06T00:00:00Z"))
)
)
} yield assert(resultSeconds)(equalTo(respArgs("key", "EXAT", "1617667200"))) && assert(resultMilliseconds)(
equalTo(respArgs("key", "PXAT", "1617667200000"))
)
},
testM("GetExPersistInput - valid value") {
for {
result <- Task(GetExPersistInput.encode("key" -> true))
resultWithoutOption <- Task(GetExPersistInput.encode("key" -> false))
} yield assert(result)(equalTo(respArgs("key", "PERSIST"))) &&
assert(resultWithoutOption)(equalTo(respArgs("key")))
}
)
)

Expand Down
88 changes: 88 additions & 0 deletions redis/src/test/scala/zio/redis/StringsSpec.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package zio.redis

import java.time.Instant

import zio.clock.Clock
import zio.duration._
import zio.redis.RedisError.{ ProtocolError, WrongType }
Expand Down Expand Up @@ -1435,6 +1437,92 @@ trait StringsSpec extends BaseSpec {
len <- strLen(key).either
} yield assert(len)(isLeft(isSubtype[WrongType](anything)))
}
),
suite("getEx")(
testM("value exists after removing ttl") {
for {
key <- uuid
value <- uuid
_ <- pSetEx(key, 10.millis, value)
exists <- getEx(key, true)
_ <- ZIO.sleep(20.millis)
res <- get(key)
} yield assert(res.isDefined)(equalTo(true)) && assert(exists)(equalTo(Some(value)))
} @@ eventually,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a question why is this test @eventually?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is because sleep is unreliable and subject to CPU and thread scheduling. 20ms is likely to cause failure. Here, it is similar to CAS. (guess)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah true, I forgot that we use the Live Clock in these tests.

testM("not found value when set seconds ttl") {
for {
key <- uuid
value <- uuid
_ <- set(key, value)
exists <- getEx(key, Expire.SetExpireSeconds, 1.second)
_ <- ZIO.sleep(1020.millis)
res <- get(key)
} yield assert(res.isDefined)(equalTo(false)) && assert(exists)(equalTo(Some(value)))
} @@ eventually,
testM("not found value when set milliseconds ttl") {
for {
key <- uuid
value <- uuid
_ <- set(key, value)
exists <- getEx(key, Expire.SetExpireMilliseconds, 1000.millis)
_ <- ZIO.sleep(1020.millis)
res <- get(key)
} yield assert(res.isDefined)(equalTo(false)) && assert(exists)(equalTo(Some(value)))
} @@ eventually,
testM("not found value when set seconds timestamp") {
for {
key <- uuid
value <- uuid
_ <- set(key, value)
exists <- getExAt(key, ExpiredAt.SetExpireAtSeconds, Instant.now().plusMillis(1000))
_ <- ZIO.sleep(1020.millis)
res <- get(key)
} yield assert(res.isDefined)(equalTo(false)) && assert(exists)(equalTo(Some(value)))
} @@ eventually,
testM("not found value when set milliseconds timestamp") {
for {
key <- uuid
value <- uuid
_ <- set(key, value)
exists <- getExAt(key, ExpiredAt.SetExpireAtMilliseconds, Instant.now().plusMillis(1000))
_ <- ZIO.sleep(1020.millis)
res <- get(key)
} yield assert(res.isDefined)(equalTo(false)) && assert(exists)(equalTo(Some(value)))
} @@ eventually,
testM("key not found") {
for {
key <- uuid
value <- uuid
_ <- set(key, value)
res <- getExAt(value, ExpiredAt.SetExpireAtMilliseconds, Instant.now().plusMillis(1000))
res2 <- getEx(value, Expire.SetExpireMilliseconds, 1000.millis)
res3 <- getEx(value, true)
} yield assert(res)(equalTo(None)) && assert(res2)(equalTo(None)) && assert(res3)(equalTo(None))
} @@ eventually
),
suite("getDel")(
testM("error when not string") {
for {
key <- uuid
_ <- sAdd(key, "a")
res <- getDel(key).either
} yield assert(res)(isLeft(isSubtype[WrongType](anything)))
},
testM("key not exists") {
for {
key <- uuid
res <- getDel(key)
} yield assert(res)(equalTo(None))
},
testM("get and remove key") {
for {
key <- uuid
value <- uuid
_ <- set(key, value)
res <- getDel(key)
notFound <- getDel(key)
} yield assert(res)(equalTo(Some(value))) && assert(notFound)(equalTo(None))
}
)
)
}