Skip to content

Commit

Permalink
Merge pull request #38 from scoquelin/jl-keys-part1
Browse files Browse the repository at this point in the history
  • Loading branch information
72squared authored Aug 28, 2024
2 parents a185105 + 64dae09 commit c085d19
Show file tree
Hide file tree
Showing 4 changed files with 688 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,181 @@ package com.github.scoquelin.arugula.commands
import scala.concurrent.Future
import scala.concurrent.duration.FiniteDuration

import com.github.scoquelin.arugula.commands.RedisBaseAsyncCommands.{InitialCursor, ScanResults}

import java.time.Instant

/**
* Asynchronous commands for manipulating/querying Keys
*
* @tparam K The key type
* @tparam V The value type
*/
trait RedisKeyAsyncCommands[K, V] {

/**
* Copy a key to another key
* @param srcKey The key to copy
* @param destKey The key to copy to
* @return True if the key was copied, false otherwise
*/
def copy(srcKey: K, destKey: K): Future[Boolean]

/**
* Copy a key to another key with additional arguments
* @param srcKey The key to copy
* @param destKey The key to copy to
* @param args Additional arguments for the copy operation
*/
def copy(srcKey: K, destKey: K, args: RedisKeyAsyncCommands.CopyArgs): Future[Unit]

/**
* Delete one or more keys
* @param key The key(s) to delete
* @return The number of keys that were removed
*/
def del(key: K*): Future[Long]

/**
* Unlink one or more keys. (non-blocking version of DEL)
* @param key The key(s) to unlink
* @return The number of keys that were unlinked
*/
def unlink(key: K*): Future[Long]

/**
* Serialize a key
* @param key The key to serialize
* @return The serialized value of the key
*/
def dump(key: K): Future[Array[Byte]]

/**
* Determine if a key exists
* @param key The key to check
* @return True if the key exists, false otherwise
*/
def exists(key: K*): Future[Boolean]

/**
* Set a key's time to live. The key will be automatically deleted after the timeout.
* Implementations may round the timeout to the nearest second if necessary
* but could set a more precise timeout if the underlying Redis client supports it.
* @param key The key to set the expiration for
* @param expiresIn The duration until the key expires
* @return True if the timeout was set, false otherwise
*/
def expire(key: K, expiresIn: FiniteDuration): Future[Boolean]

/**
* Set the expiration for a key as an Instant
* @param key The key to set the expiration for
* @param timestamp The point in time when the key should expire
* @return True if the timeout was set, false otherwise
*/
def expireAt(key: K, timestamp: Instant): Future[Boolean]

/**
* Get the time to live for a key as an Instant
* @param key The key to get the expiration for
* @return The time to live as a point in time, or None if the key does not exist or does not have an expiration
*/
def expireTime(key: K): Future[Option[Instant]]

/**
* Find all keys matching the given pattern
* To match all keys, use "*"
* @param pattern The pattern to match
* @return The keys that match the pattern
*/
def keys(pattern: K): Future[List[K]]

/**
* Move a key to a different database
* @param key The key to move
* @param db The database to move the key to
* @return True if the key was moved, false otherwise
*/
def move(key: K, db: Int): Future[Boolean]

/**
* Rename a key
* @param key The key to rename
* @param newKey The new name for the key
*/
def rename(key: K, newKey: K): Future[Unit]

/**
* Rename a key, but only if the new key does not already exist
* @param key The key to rename
* @param newKey The new name for the key
* @return True if the key was renamed, false otherwise
*/
def renameNx(key: K, newKey: K): Future[Boolean]

/**
* Restore a key from its serialized form
* @param key The key to restore
* @param serializedValue The serialized value of the key
* @param args Additional arguments for the restore operation
*/
def restore(key: K, serializedValue: Array[Byte], args: RedisKeyAsyncCommands.RestoreArgs = RedisKeyAsyncCommands.RestoreArgs()): Future[Unit]

/**
* Scan the keyspace
* @param cursor The cursor to start scanning from
* @param matchPattern An optional pattern to match keys against
* @param limit An optional limit on the number of keys to return
* @return The keys that were scanned
*/
def scan(cursor: String = InitialCursor, matchPattern: Option[String] = None, limit: Option[Int] = None): Future[ScanResults[List[K]]]

/**
* Get the time to live for a key.
* Implementations may return a more precise time to live if the underlying Redis client supports it.
* Rather than expose the underlying Redis client's API, this method returns a FiniteDuration which can
* be rounded to the nearest second if necessary.
* @param key The key to get the expiration for
* @return The time to live, or None if the key does not exist or does not have an expiration
*/
def ttl(key: K): Future[Option[FiniteDuration]]

/**
* Alters the last access time of a key(s). A key is ignored if it does not exist.
* @param key The key(s) to touch
* @return The number of keys that were touched
*/
def touch(key: K*): Future[Long]

/**
* Get the type of a key
* @param key The key to get the type of
* @return The type of the key
*/
def `type`(key: K): Future[String]
}

object RedisKeyAsyncCommands {
case class CopyArgs(replace: Boolean = false, destinationDb: Option[Int] = None)

case class RestoreArgs(
replace: Boolean = false,
idleTime: Option[FiniteDuration] = None,
ttl: Option[FiniteDuration] = None,
absTtl: Option[Instant] = None,
frequency: Option[Long] = None,
){
def isEmpty: Boolean = !replace && idleTime.isEmpty && frequency.isEmpty && ttl.isEmpty && absTtl.isEmpty
}
}

// Commands to be Implemented:
//migrate
//objectEncoding
//objectFreq
//objectIdletime
//objectRefcount
//randomkey
//sort
//sortReadOnly
//sortStore
Original file line number Diff line number Diff line change
@@ -1,17 +1,37 @@
package com.github.scoquelin.arugula.commands

import scala.collection.convert.ImplicitConversions.`collection AsScalaIterable`
import scala.concurrent.Future
import scala.concurrent.duration.FiniteDuration

import com.github.scoquelin.arugula.commands.RedisBaseAsyncCommands.InitialCursor
import com.github.scoquelin.arugula.internal.LettuceRedisCommandDelegation
import io.lettuce.core.{CopyArgs, ScanCursor}

import java.time.Instant
import java.util.concurrent.TimeUnit

private[arugula] trait LettuceRedisKeyAsyncCommands[K, V] extends RedisKeyAsyncCommands[K, V] with LettuceRedisCommandDelegation[K, V] {
import LettuceRedisKeyAsyncCommands.toFiniteDuration

override def copy(srcKey: K, destKey: K): Future[Boolean] =
delegateRedisClusterCommandAndLift(_.copy(srcKey, destKey)).map(Boolean2boolean)

override def copy(srcKey: K, destKey: K, args: RedisKeyAsyncCommands.CopyArgs): Future[Unit] = {
val copyArgs: CopyArgs = CopyArgs.Builder.replace(args.replace)
args.destinationDb.foreach(copyArgs.destinationDb(_))
delegateRedisClusterCommandAndLift(_.copy(srcKey, destKey, copyArgs)).map(_ => ())
}

override def del(key: K*): Future[Long] =
delegateRedisClusterCommandAndLift(_.del(key: _*)).map(Long2long)

override def unlink(key: K*): Future[Long] =
delegateRedisClusterCommandAndLift(_.unlink(key: _*)).map(Long2long)

override def dump(key: K): Future[Array[Byte]] =
delegateRedisClusterCommandAndLift(_.dump(key))

override def exists(key: K*): Future[Boolean] =
delegateRedisClusterCommandAndLift(_.exists(key: _*)).map(_ == key.size.toLong)

Expand All @@ -23,8 +43,74 @@ private[arugula] trait LettuceRedisKeyAsyncCommands[K, V] extends RedisKeyAsyncC
delegateRedisClusterCommandAndLift(_.expire(key, expiresIn.toSeconds))
}).map(Boolean2boolean)


override def expireAt(key: K, timestamp: Instant): Future[Boolean] =
delegateRedisClusterCommandAndLift(_.pexpireat(key, timestamp.toEpochMilli)).map(Boolean2boolean)

override def expireTime(key: K): Future[Option[Instant]] = {
delegateRedisClusterCommandAndLift(_.pexpiretime(key)).map {
case d if d < 0 => None
case d => Some(Instant.ofEpochMilli(d))
}
}

override def keys(pattern: K): Future[List[K]] =
delegateRedisClusterCommandAndLift(_.keys(pattern)).map(_.toList)

override def move(key: K, db: Int): Future[Boolean] =
delegateRedisClusterCommandAndLift(_.move(key, db)).map(Boolean2boolean)

override def rename(key: K, newKey: K): Future[Unit] =
delegateRedisClusterCommandAndLift(_.rename(key, newKey)).map(_ => ())

override def renameNx(key: K, newKey: K): Future[Boolean] =
delegateRedisClusterCommandAndLift(_.renamenx(key, newKey)).map(Boolean2boolean)

override def restore(key: K, serializedValue: Array[Byte], args: RedisKeyAsyncCommands.RestoreArgs = RedisKeyAsyncCommands.RestoreArgs()): Future[Unit] = {
val restoreArgs = new io.lettuce.core.RestoreArgs()
args.ttl.foreach { duration =>
restoreArgs.ttl(duration.toMillis)
}
args.idleTime.foreach { duration =>
restoreArgs.idleTime(duration.toMillis)
}
args.frequency.foreach { frequency =>
restoreArgs.frequency(frequency)
}
if(args.replace) restoreArgs.replace()
args.absTtl.foreach{ instant =>
restoreArgs.absttl(true)
restoreArgs.ttl(instant.toEpochMilli)
}
delegateRedisClusterCommandAndLift(_.restore(key, serializedValue, restoreArgs)).map(_ => ())
}

override def scan(cursor: String = InitialCursor, matchPattern: Option[String] = None, limit: Option[Int] = None): Future[RedisBaseAsyncCommands.ScanResults[List[K]]] = {
val scanArgs = (matchPattern, limit) match {
case (Some(pattern), Some(count)) => Some(io.lettuce.core.ScanArgs.Builder.matches(pattern).limit(count))
case (Some(pattern), None) => Some(io.lettuce.core.ScanArgs.Builder.matches(pattern))
case (None, Some(count)) => Some(io.lettuce.core.ScanArgs.Builder.limit(count))
case _ => None
}
val result = scanArgs match {
case Some(args) => delegateRedisClusterCommandAndLift(_.scan(ScanCursor.of(cursor), args))
case None => delegateRedisClusterCommandAndLift(_.scan(ScanCursor.of(cursor)))
}
result.map { scanResult =>
RedisBaseAsyncCommands.ScanResults(scanResult.getCursor, scanResult.isFinished, scanResult.getKeys.toList)
}
}

override def ttl(key: K): Future[Option[FiniteDuration]] =
delegateRedisClusterCommandAndLift(_.ttl(key)).map(toFiniteDuration(TimeUnit.SECONDS))
delegateRedisClusterCommandAndLift(_.pttl(key)).map(toFiniteDuration(TimeUnit.MILLISECONDS))

override def touch(key: K*): Future[Long] = {
delegateRedisClusterCommandAndLift(_.touch(key: _*)).map(Long2long)
}

override def `type`(key: K): Future[String] = {
delegateRedisClusterCommandAndLift(_.`type`(key))
}
}

private[this] object LettuceRedisKeyAsyncCommands {
Expand Down
Loading

0 comments on commit c085d19

Please sign in to comment.