Compare commits
10 Commits
bd19ddd6cb
...
0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b0449e995 | |||
| e05a9d3d11 | |||
| 4d45b5c4bc | |||
| 3d2d9d30ee | |||
| 4d4ccc1d14 | |||
| 7c0acc3ecf | |||
| 2c0942fee1 | |||
| 0a9c8f81aa | |||
| c51c90b597 | |||
| 8ac4dac2f0 |
54
Cargo.lock
generated
54
Cargo.lock
generated
@@ -83,8 +83,10 @@ dependencies = [
|
||||
"bon",
|
||||
"bytes",
|
||||
"byteyarn",
|
||||
"clap",
|
||||
"env_logger",
|
||||
"log",
|
||||
"shell-words",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
@@ -177,6 +179,46 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
@@ -253,6 +295,12 @@ version = "0.31.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
@@ -500,6 +548,12 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shell-words"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.5"
|
||||
|
||||
12
Cargo.toml
12
Cargo.toml
@@ -2,12 +2,24 @@
|
||||
name = "archive"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
authors = ["409"]
|
||||
default-run = "archive-server"
|
||||
|
||||
[dependencies]
|
||||
bon = "3.6.4"
|
||||
bytes = "1.10.1"
|
||||
byteyarn = "0.5.1"
|
||||
clap = { version = "4.5.40", features = ["derive"] }
|
||||
env_logger = "0.11.8"
|
||||
log = "0.4.27"
|
||||
shell-words = "1.1.0"
|
||||
thiserror = "2.0.12"
|
||||
tokio = { version = "1.45.1", features = ["full"] }
|
||||
|
||||
[[bin]]
|
||||
name = "archive-server"
|
||||
path = "src/bin/server.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "archive-cli"
|
||||
path = "src/bin/cli.rs"
|
||||
|
||||
17
Dockerfile
Normal file
17
Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
||||
FROM rust:bullseye AS builder
|
||||
WORKDIR /usr/src/archive
|
||||
|
||||
COPY Cargo.toml .
|
||||
RUN mkdir -p src/bin && echo "fn main() {}" > src/bin/server.rs
|
||||
RUN cargo build --release --bin archive-server
|
||||
|
||||
COPY src src
|
||||
RUN touch src/main.rs
|
||||
RUN cargo build --release --bin archive-server
|
||||
|
||||
RUN strip target/release/archive-server
|
||||
|
||||
FROM debian:12.11
|
||||
RUN apt-get update
|
||||
COPY --from=builder /usr/src/archive/target/release/archive-server /usr/local/bin/archive-server
|
||||
ENTRYPOINT ["archive-server"]
|
||||
34
README.md
Normal file
34
README.md
Normal file
@@ -0,0 +1,34 @@
|
||||

|
||||
|
||||
# ArcHIVE
|
||||
|
||||
A lightweight in-memory database written in Rust
|
||||
|
||||
## Usage
|
||||
|
||||
### CLI
|
||||
|
||||
Download a binary or compile it yourself using `cargo build --release --bin archive-cli`
|
||||
|
||||
To compile the CLI and install it globally run `cargo install --path .`
|
||||
|
||||
### Server
|
||||
|
||||
#### Docker
|
||||
|
||||
The repository contains a simple `compose.yaml`
|
||||
|
||||
#### Binary
|
||||
|
||||
Alternatively you can run the server binary directly
|
||||
|
||||
Download a binary or compile it yourself using `cargo build --release --bin archive-server`
|
||||
|
||||
#### Environment variables
|
||||
|
||||
This is a list of the server's environment variables and their default values:
|
||||
|
||||
- SERVER_HOST=0.0.0.0
|
||||
- SERVER_PORT=6171
|
||||
- MAX_CONNECTIONS=256
|
||||
- LOG_LEVEL=info
|
||||
12
compose.yaml
Normal file
12
compose.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
services:
|
||||
archive-server:
|
||||
container_name: 'archive-server'
|
||||
image: 'git.409dev.buzz/409/archive:latest'
|
||||
build: '.'
|
||||
ports:
|
||||
- '6171:6171/tcp'
|
||||
environment:
|
||||
- 'SERVER_HOST=0.0.0.0'
|
||||
- 'SERVER_PORT=6171'
|
||||
- 'MAX_CONNECTIONS=256'
|
||||
- 'LOG_LEVEL=info'
|
||||
123
src/bin/cli.rs
Normal file
123
src/bin/cli.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
use std::io::{Write as _, stdin};
|
||||
|
||||
use archive::{Result, client::Client};
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(version, about, long_about = None)]
|
||||
struct Args {
|
||||
#[arg(long, default_value = "127.0.0.1")]
|
||||
host: String,
|
||||
#[arg(short, long, default_value_t = 6171)]
|
||||
port: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(help_template = "{subcommands}")]
|
||||
enum Commands {
|
||||
Get {
|
||||
key: String,
|
||||
},
|
||||
Set {
|
||||
key: String,
|
||||
value: String,
|
||||
expiration: Option<u64>,
|
||||
},
|
||||
Delete {
|
||||
key: String,
|
||||
},
|
||||
Has {
|
||||
key: String,
|
||||
},
|
||||
Ttl {
|
||||
key: String,
|
||||
},
|
||||
Expire {
|
||||
key: String,
|
||||
seconds: u64,
|
||||
},
|
||||
Persist {
|
||||
key: String,
|
||||
},
|
||||
#[command(aliases = &["exit", "q"])]
|
||||
Quit,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
|
||||
let mut client = Client::new(&format!("{}:{}", args.host, args.port)).await?;
|
||||
|
||||
print_welcome();
|
||||
|
||||
let stdin = stdin();
|
||||
let mut input = String::new();
|
||||
|
||||
loop {
|
||||
input.clear();
|
||||
print!("> ");
|
||||
std::io::stdout().flush()?;
|
||||
stdin.read_line(&mut input)?;
|
||||
|
||||
let Ok(mut words) = shell_words::split(&input) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
words.insert(0, "".into());
|
||||
|
||||
let command = match Commands::try_parse_from(words.clone()) {
|
||||
Ok(command) => command,
|
||||
Err(e) => {
|
||||
println!("{}", e.render());
|
||||
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
match command {
|
||||
Commands::Get { key } => {
|
||||
let value = client.get(&key).await?;
|
||||
println!("{value:?}");
|
||||
}
|
||||
Commands::Set {
|
||||
key,
|
||||
value,
|
||||
expiration,
|
||||
} => {
|
||||
client.set(&key, value.as_bytes(), expiration).await?;
|
||||
println!("1");
|
||||
}
|
||||
Commands::Delete { key } => {
|
||||
let value = client.delete(&key).await?;
|
||||
println!("{value:?}");
|
||||
}
|
||||
Commands::Has { key } => {
|
||||
let value = client.has(&key).await?;
|
||||
println!("{value:?}");
|
||||
}
|
||||
Commands::Ttl { key } => {
|
||||
let value = client.ttl(&key).await?;
|
||||
println!("{value:?}");
|
||||
}
|
||||
Commands::Expire { key, seconds } => {
|
||||
let value = client.expire(&key, seconds).await?;
|
||||
println!("{value:?}");
|
||||
}
|
||||
Commands::Persist { key } => {
|
||||
let value = client.persist(&key).await?;
|
||||
println!("{value:?}");
|
||||
}
|
||||
Commands::Quit => break,
|
||||
}
|
||||
}
|
||||
|
||||
println!("{input}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_welcome() {
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
println!("archive-cli {version}");
|
||||
}
|
||||
@@ -1,27 +1,15 @@
|
||||
use config::ServerConfig;
|
||||
use errors::AppError;
|
||||
use server::Server;
|
||||
use archive::Result;
|
||||
use archive::config::ServerConfig;
|
||||
use archive::server::Server;
|
||||
|
||||
use tokio::{signal::ctrl_c, sync::oneshot};
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
|
||||
pub mod client;
|
||||
pub mod commands;
|
||||
pub mod config;
|
||||
pub mod connection;
|
||||
pub mod database;
|
||||
pub mod errors;
|
||||
pub mod handler;
|
||||
pub mod server;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, AppError>;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
env_logger::builder()
|
||||
.format_target(false)
|
||||
.filter_level(log::LevelFilter::Info)
|
||||
.parse_env("LOG_LEVEL")
|
||||
.parse_default_env()
|
||||
.init();
|
||||
|
||||
@@ -170,6 +170,60 @@ impl Client {
|
||||
Ok(ttl)
|
||||
}
|
||||
|
||||
pub async fn expire(&mut self, key: &str, seconds: u64) -> Result<bool> {
|
||||
let mut bytes = BytesMut::new();
|
||||
bytes.put_u16(6);
|
||||
bytes.put_slice(b"expire");
|
||||
|
||||
let key_length: u16 = key
|
||||
.len()
|
||||
.try_into()
|
||||
.map_err(|_| AppError::KeyLength(key.len()))?;
|
||||
|
||||
bytes.put_u16(key_length);
|
||||
bytes.put_slice(key.as_bytes());
|
||||
|
||||
bytes.put_u64(seconds);
|
||||
|
||||
self.connection.write(bytes.into()).await?;
|
||||
|
||||
let mut r = self.get_response().await?;
|
||||
|
||||
let success = match r.try_get_u8()? {
|
||||
1 => true,
|
||||
0 => false,
|
||||
_ => return Err(AppError::InvalidCommandResponse),
|
||||
};
|
||||
|
||||
Ok(success)
|
||||
}
|
||||
|
||||
pub async fn persist(&mut self, key: &str) -> Result<bool> {
|
||||
let mut bytes = BytesMut::new();
|
||||
bytes.put_u16(7);
|
||||
bytes.put_slice(b"persist");
|
||||
|
||||
let key_length: u16 = key
|
||||
.len()
|
||||
.try_into()
|
||||
.map_err(|_| AppError::KeyLength(key.len()))?;
|
||||
|
||||
bytes.put_u16(key_length);
|
||||
bytes.put_slice(key.as_bytes());
|
||||
|
||||
self.connection.write(bytes.into()).await?;
|
||||
|
||||
let mut r = self.get_response().await?;
|
||||
|
||||
let success = match r.try_get_u8()? {
|
||||
1 => true,
|
||||
0 => false,
|
||||
_ => return Err(AppError::InvalidCommandResponse),
|
||||
};
|
||||
|
||||
Ok(success)
|
||||
}
|
||||
|
||||
async fn get_response(&mut self) -> Result<Bytes> {
|
||||
self.connection
|
||||
.read_bytes()
|
||||
|
||||
37
src/commands/expire.rs
Normal file
37
src/commands/expire.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use std::io::Cursor;
|
||||
|
||||
use bytes::{Buf as _, Bytes};
|
||||
|
||||
use crate::{Result, connection::Connection, database::Database, errors::AppError};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Expire {
|
||||
key: String,
|
||||
seconds: u64,
|
||||
}
|
||||
|
||||
impl Expire {
|
||||
pub async fn execute(self, db: &Database, connection: &mut Connection) -> Result<()> {
|
||||
let value = db.expire(&self.key, self.seconds).await?;
|
||||
|
||||
connection
|
||||
.write(Bytes::from_static(if value { &[1] } else { &[0] }))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn parse(bytes: &mut Cursor<&[u8]>) -> Result<Self> {
|
||||
let key_length = bytes.try_get_u16()? as usize;
|
||||
|
||||
if bytes.remaining() < key_length {
|
||||
return Err(AppError::IncompleteCommandBuffer);
|
||||
}
|
||||
|
||||
let key = String::from_utf8(bytes.copy_to_bytes(key_length).to_vec())?;
|
||||
|
||||
let seconds = bytes.try_get_u64()?;
|
||||
|
||||
Ok(Self { key, seconds })
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
mod delete;
|
||||
mod expire;
|
||||
mod get;
|
||||
mod has;
|
||||
mod persist;
|
||||
mod set;
|
||||
mod ttl;
|
||||
|
||||
@@ -8,8 +10,10 @@ use std::io::Cursor;
|
||||
|
||||
use bytes::{Buf, BytesMut};
|
||||
use delete::Delete;
|
||||
use expire::Expire;
|
||||
use get::Get;
|
||||
use has::Has;
|
||||
use persist::Persist;
|
||||
use set::Set;
|
||||
use ttl::Ttl;
|
||||
|
||||
@@ -22,6 +26,8 @@ pub enum Command {
|
||||
Delete(Delete),
|
||||
Has(Has),
|
||||
Ttl(Ttl),
|
||||
Expire(Expire),
|
||||
Persist(Persist),
|
||||
}
|
||||
|
||||
impl Command {
|
||||
@@ -32,6 +38,8 @@ impl Command {
|
||||
Command::Delete(delete) => delete.execute(db, connection).await,
|
||||
Command::Has(has) => has.execute(db, connection).await,
|
||||
Command::Ttl(ttl) => ttl.execute(db, connection).await,
|
||||
Command::Expire(expire) => expire.execute(db, connection).await,
|
||||
Command::Persist(persist) => persist.execute(db, connection).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +64,8 @@ impl Command {
|
||||
"delete" => Self::Delete(Delete::parse(bytes)?),
|
||||
"has" => Self::Has(Has::parse(bytes)?),
|
||||
"ttl" => Self::Ttl(Ttl::parse(bytes)?),
|
||||
"expire" => Self::Expire(Expire::parse(bytes)?),
|
||||
"persist" => Self::Persist(Persist::parse(bytes)?),
|
||||
_ => return Err(AppError::UnknownCommand(command_name)),
|
||||
};
|
||||
|
||||
|
||||
34
src/commands/persist.rs
Normal file
34
src/commands/persist.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use std::io::Cursor;
|
||||
|
||||
use bytes::{Buf as _, Bytes};
|
||||
|
||||
use crate::{Result, connection::Connection, database::Database, errors::AppError};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Persist {
|
||||
key: String,
|
||||
}
|
||||
|
||||
impl Persist {
|
||||
pub async fn execute(self, db: &Database, connection: &mut Connection) -> Result<()> {
|
||||
let value = db.persist(&self.key).await?;
|
||||
|
||||
connection
|
||||
.write(Bytes::from_static(if value { &[1] } else { &[0] }))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn parse(bytes: &mut Cursor<&[u8]>) -> Result<Self> {
|
||||
let key_length = bytes.try_get_u16()? as usize;
|
||||
|
||||
if bytes.remaining() < key_length {
|
||||
return Err(AppError::IncompleteCommandBuffer);
|
||||
}
|
||||
|
||||
let key = String::from_utf8(bytes.copy_to_bytes(key_length).to_vec())?;
|
||||
|
||||
Ok(Self { key })
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,18 @@
|
||||
use std::{io::Cursor, time::Duration};
|
||||
use std::io::Cursor;
|
||||
|
||||
use crate::{Result, connection::Connection, database::Database, errors::AppError};
|
||||
use bytes::{Buf as _, Bytes};
|
||||
use tokio::time::Instant;
|
||||
|
||||
use crate::{
|
||||
Result,
|
||||
connection::Connection,
|
||||
database::{Database, Value},
|
||||
errors::AppError,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Set {
|
||||
key: String,
|
||||
value: Value,
|
||||
data: Box<[u8]>,
|
||||
expiration: Option<u64>,
|
||||
}
|
||||
|
||||
impl Set {
|
||||
pub async fn execute(self, db: &Database, connection: &mut Connection) -> Result<()> {
|
||||
db.set(self.key, self.value).await?;
|
||||
db.set(self.key, self.data, self.expiration).await?;
|
||||
|
||||
connection.write(Bytes::from_static(&[1])).await?;
|
||||
|
||||
@@ -42,15 +36,16 @@ impl Set {
|
||||
|
||||
let data = bytes.copy_to_bytes(value_length);
|
||||
|
||||
let expiration: Option<Instant> = match bytes.try_get_u8()? {
|
||||
1 => Some(Instant::now() + Duration::from_secs(bytes.try_get_u64()?)),
|
||||
let expiration: Option<u64> = match bytes.try_get_u8()? {
|
||||
1 => Some(bytes.try_get_u64()?),
|
||||
0 => None,
|
||||
_ => return Err(AppError::UnexpectedCommandData),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
key,
|
||||
value: Value::new(data, expiration),
|
||||
data: (*data).into(),
|
||||
expiration,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use bytes::{BufMut, Bytes, BytesMut};
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use byteyarn::Yarn;
|
||||
use tokio::{
|
||||
sync::{Mutex, Notify},
|
||||
@@ -32,11 +33,8 @@ pub struct Value {
|
||||
}
|
||||
|
||||
impl Value {
|
||||
pub fn new(data: Bytes, expiration: Option<Instant>) -> Self {
|
||||
Self {
|
||||
data: data.into_iter().collect(),
|
||||
expiration,
|
||||
}
|
||||
pub fn new(data: Box<[u8]>, expiration: Option<Instant>) -> Self {
|
||||
Self { data, expiration }
|
||||
}
|
||||
|
||||
pub fn from_string(data: String, expiration: Option<Instant>) -> Self {
|
||||
@@ -68,13 +66,15 @@ impl Database {
|
||||
state.entries.get(key).map(|v| v.data.clone())
|
||||
}
|
||||
|
||||
pub async fn set(&self, key: String, value: Value) -> Result<()> {
|
||||
pub async fn set(&self, key: String, data: Box<[u8]>, expiration: Option<u64>) -> Result<()> {
|
||||
let mut state = self.state.lock().await;
|
||||
|
||||
let expiration = value.expiration.clone();
|
||||
let expiration = expiration.map(|seconds| Instant::now() + Duration::from_secs(seconds));
|
||||
|
||||
let key = Yarn::from(key.clone());
|
||||
|
||||
let value = Value::new(data, expiration);
|
||||
|
||||
let previous = state.entries.insert(key.clone(), value);
|
||||
|
||||
let mut notify = false;
|
||||
@@ -138,6 +138,73 @@ impl Database {
|
||||
.flatten()
|
||||
}
|
||||
|
||||
pub async fn expire(&self, key: &str, seconds: u64) -> Result<bool> {
|
||||
let mut state = self.state.lock().await;
|
||||
|
||||
let key = Yarn::copy(key);
|
||||
|
||||
let expiration = Instant::now() + Duration::from_secs(seconds);
|
||||
|
||||
let notify =
|
||||
state
|
||||
.expirations
|
||||
.iter()
|
||||
.next()
|
||||
.is_none_or(|&(instant, ref next_expiration_key)| {
|
||||
next_expiration_key == &key || instant > expiration
|
||||
});
|
||||
|
||||
let Some(value) = state.entries.get_mut(&key) else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
let previous_expiration = value.expiration.take();
|
||||
value.expiration = Some(expiration);
|
||||
|
||||
if let Some(previous_expiration) = previous_expiration {
|
||||
state
|
||||
.expirations
|
||||
.remove(&(previous_expiration, key.clone()));
|
||||
};
|
||||
|
||||
state.expirations.insert((expiration, key));
|
||||
|
||||
if notify {
|
||||
self.notify.notify_one();
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub async fn persist(&self, key: &str) -> Result<bool> {
|
||||
let mut state = self.state.lock().await;
|
||||
|
||||
let key = Yarn::copy(key);
|
||||
|
||||
let notify = state
|
||||
.expirations
|
||||
.iter()
|
||||
.next()
|
||||
.is_some_and(|&(_, ref next_expiration_key)| next_expiration_key == &key);
|
||||
|
||||
let Some(value) = state.entries.get_mut(&key) else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
match value.expiration.take() {
|
||||
Some(expiration) => {
|
||||
state.expirations.remove(&(expiration, key));
|
||||
}
|
||||
None => return Ok(false),
|
||||
}
|
||||
|
||||
if notify {
|
||||
self.notify.notify_one();
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub async fn shutdown(&mut self) {
|
||||
self.state.lock().await.shutdown = true;
|
||||
self.notify.notify_one();
|
||||
|
||||
15
src/lib.rs
Normal file
15
src/lib.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use errors::AppError;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
|
||||
pub mod client;
|
||||
pub mod commands;
|
||||
pub mod config;
|
||||
pub mod connection;
|
||||
pub mod database;
|
||||
pub mod errors;
|
||||
pub mod handler;
|
||||
pub mod server;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, AppError>;
|
||||
15
src/tests.rs
15
src/tests.rs
@@ -15,7 +15,7 @@ async fn expiration() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut client = client("127.0.0.1:6171").await?;
|
||||
|
||||
client
|
||||
.set("test-key", "test-value".as_bytes(), Some(3))
|
||||
.set("test-key", b"test-value", Some(3))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -27,9 +27,22 @@ async fn expiration() -> Result<(), Box<dyn std::error::Error>> {
|
||||
assert!(client.has("test-key").await.unwrap());
|
||||
assert_eq!(client.ttl("test-key").await.unwrap(), Some(1));
|
||||
|
||||
assert!(client.expire("test-key", 2).await?);
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
assert!(!client.has("test-key").await.unwrap());
|
||||
|
||||
assert!(!client.expire("test-key", 10).await?);
|
||||
|
||||
client.set("test-key", b"test-value", Some(2)).await?;
|
||||
|
||||
assert_eq!(client.ttl("test-key").await?, Some(1));
|
||||
assert!(client.persist("test-key").await?);
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
|
||||
assert_eq!(client.get("test-key").await?, Some("test-value".into()));
|
||||
|
||||
shutdown_tx.send(()).unwrap();
|
||||
|
||||
server_handle.await??;
|
||||
|
||||
Reference in New Issue
Block a user