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

Implement PTTL command #60

Merged
merged 2 commits into from
Oct 21, 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
9 changes: 7 additions & 2 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub mod mset;
pub mod msetnx;
pub mod object;
pub mod ping;
pub mod pttl;
pub mod scan;
pub mod select;
pub mod set;
Expand Down Expand Up @@ -70,6 +71,7 @@ use mset::Mset;
use msetnx::Msetnx;
use object::Object;
use ping::Ping;
use pttl::Pttl;
use scan::Scan;
use select::Select;
use set::Set;
Expand All @@ -88,8 +90,8 @@ pub enum Command {
Del(Del),
Exists(Exists),
Get(Get),
Getex(Getex),
Getdel(Getdel),
Getex(Getex),
Getrange(Getrange),
Incr(Incr),
IncrBy(IncrBy),
Expand All @@ -101,6 +103,7 @@ pub enum Command {
Mset(Mset),
Msetnx(Msetnx),
Object(Object),
Pttl(Pttl),
Scan(Scan),
Set(Set),
Setnx(Setnx),
Expand Down Expand Up @@ -132,8 +135,8 @@ impl Executable for Command {
Command::Del(cmd) => cmd.exec(store),
Command::Exists(cmd) => cmd.exec(store),
Command::Get(cmd) => cmd.exec(store),
Command::Getex(cmd) => cmd.exec(store),
Command::Getdel(cmd) => cmd.exec(store),
Command::Getex(cmd) => cmd.exec(store),
Command::Getrange(cmd) => cmd.exec(store),
Command::Incr(cmd) => cmd.exec(store),
Command::IncrBy(cmd) => cmd.exec(store),
Expand All @@ -148,6 +151,7 @@ impl Executable for Command {
Command::Msetnx(cmd) => cmd.exec(store),
Command::Object(cmd) => cmd.exec(store),
Command::Ping(cmd) => cmd.exec(store),
Command::Pttl(cmd) => cmd.exec(store),
Command::Scan(cmd) => cmd.exec(store),
Command::Select(cmd) => cmd.exec(store),
Command::Set(cmd) => cmd.exec(store),
Expand Down Expand Up @@ -217,6 +221,7 @@ impl TryFrom<Frame> for Command {
"setrange" => Setrange::try_from(parser).map(Command::Setrange),
"strlen" => Strlen::try_from(parser).map(Command::Strlen),
"ttl" => Ttl::try_from(parser).map(Command::Ttl),
"pttl" => Pttl::try_from(parser).map(Command::Pttl),
"type" => Type::try_from(parser).map(Command::Type),
_ => Err(CommandParserError::UnknownCommand {
command: command_name,
Expand Down
36 changes: 36 additions & 0 deletions src/commands/pttl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use crate::commands::executable::Executable;
use crate::commands::CommandParser;
use crate::frame::Frame;
use crate::store::Store;
use crate::Error;

/// Like TTL this command returns the remaining time to live of a key that has an expire set, with
/// the sole difference that TTL returns the amount of remaining time in seconds while PTTL returns
/// it in milliseconds.
///
/// Ref: <https://redis.io/docs/latest/commands/pttl/>
#[derive(Debug, PartialEq)]
pub struct Pttl {
pub key: String,
}

impl Executable for Pttl {
fn exec(self, store: Store) -> Result<Frame, Error> {
let state = store.lock();
let ttl = if state.exists(&self.key) { -1 } else { -2 };
let ttl = state
.get_ttl(&self.key)
.map(|ttl| ttl.as_millis() as i64)
.unwrap_or(ttl);
Ok(Frame::Integer(ttl))
}
}

impl TryFrom<&mut CommandParser> for Pttl {
type Error = Error;

fn try_from(parser: &mut CommandParser) -> Result<Self, Self::Error> {
let key = parser.next_string()?;
Ok(Self { key })
}
}
34 changes: 24 additions & 10 deletions tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ where
let mut pipeline = redis::pipe();
f(&mut pipeline);

let our_response: Result<Res, _> = pipeline.clone().query_async(&mut our_connection).await;

// Since we use the same Redis instance for all tests, we flush it to start fresh.
// NOTE: our implementation doesn't yet persist data between runs.
let _: Value = redis::pipe()
Expand All @@ -39,6 +37,7 @@ where
.await
.unwrap();

let our_response: Result<Res, _> = pipeline.clone().query_async(&mut our_connection).await;
let their_response: Result<Res, _> = pipeline.clone().query_async(&mut their_connection).await;

assert!(
Expand Down Expand Up @@ -123,14 +122,29 @@ async fn test_set_and_get() {
#[serial]
async fn test_getex() {
test_compare::<Vec<Value>>(|p| {
p.cmd("SET").arg("set_getex_1").arg(1).arg("EX").arg(1);
p.cmd("GETEX").arg("set_getex_1").arg("PERSIST");
p.cmd("TTL").arg("set_getex_1");

p.cmd("SET").arg("set_getex_2").arg(1).arg("EX").arg(1);
p.cmd("GETEX").arg("set_getex_2").arg("EX").arg(10);
// `TTL set_getex_2` gives different results here.
// It isn't clear if it is a race condition or a bug in our implementation.
p.cmd("SET").arg("getex_key_1").arg(1).arg("EX").arg(1);
p.cmd("GETEX").arg("getex_key_1").arg("PERSIST");
p.cmd("TTL").arg("getex_key_1");

p.cmd("SET").arg("getex_key_2").arg(1).arg("EX").arg(1);
p.cmd("TTL").arg("getex_key_2");
p.cmd("GETEX").arg("getex_key_2").arg("EX").arg(10);
p.cmd("TTL").arg("getex_key_2");
})
.await;
}

#[tokio::test]
#[serial]
async fn test_pttl() {
test_compare::<Vec<Value>>(|p| {
p.cmd("SET").arg("pttl_key_1").arg(1).arg("EX").arg(1);
p.cmd("PTTL").arg("pttl_key_1");

p.cmd("SET").arg("pttl_key_2").arg(1);
p.cmd("PTTL").arg("pttl_key_2");

p.cmd("PTTL").arg("pttl_key_3");
})
.await;
}
Expand Down
Loading