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 some commonly used key methods #38

Merged
merged 1 commit into from
Aug 28, 2024
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
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