Skip to content

Commit 30dc314

Browse files
authored
Merge pull request #36 from nociza/storage-macro
Storage Macro Redis Support
2 parents c2ed6c6 + 99ec02e commit 30dc314

File tree

9 files changed

+228
-38
lines changed

9 files changed

+228
-38
lines changed

.cargo/config.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[env]
2+
RUST_TEST_THREADS = "1"

.github/workflows/check.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@ jobs:
1616
rabbitmq:
1717
image: rabbitmq:3.8-management
1818
ports:
19-
- 5672:5672
19+
- 5672:5672
2020
- 15672:15672
21+
redis:
22+
image: redis
23+
ports:
24+
- 6379:6379
2125
steps:
2226
- name: Checkout
2327
uses: actions/checkout@v3

Cargo.toml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "colink"
3-
version = "0.2.8"
3+
version = "0.2.9"
44
edition = "2021"
55
description = "CoLink Rust SDK"
66
license = "MIT"
@@ -10,9 +10,9 @@ documentation = "https://docs.rs/colink"
1010
repository = "https://github.com/CoLearn-Dev/colink-sdk-rust-dev"
1111

1212
[dependencies]
13-
async-recursion = { version = "1.0.0", optional = true }
13+
async-recursion = { version = "1.0", optional = true }
1414
async-trait = "0.1"
15-
base64 = "0.13.0"
15+
base64 = "0.13"
1616
chrono = "0.4"
1717
clap = { version = "4.0", features = ["derive", "env"] }
1818
futures-lite = "1.12"
@@ -21,8 +21,9 @@ hyper-rustls = { version = "0.23", optional = true }
2121
jsonwebtoken = { version = "7.2", optional = true }
2222
lapin = "2.1"
2323
prost = "0.10"
24-
rand = { version = "0.8.4", features = ["std_rng"] }
24+
rand = { version = "0.8", features = ["std_rng"] }
2525
rcgen = { version = "0.10", optional = true }
26+
redis = { version = "0.22", optional = true }
2627
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls-native-roots"], optional = true }
2728
secp256k1 = { version = "0.25", features = ["rand-std"] }
2829
serde = { version = "1.0", features = ["derive"] }
@@ -47,4 +48,4 @@ variable_transfer = ["extensions", "remote_storage", "hyper", "jsonwebtoken", "r
4748
registry = []
4849
policy_module = []
4950
instant_server = ["reqwest"]
50-
storage_macro = ["async-recursion"]
51+
storage_macro = ["async-recursion", "redis"]

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ CoLink SDK helps both application adnd protocol developers access the functional
99
Add this to your Cargo.toml:
1010
```toml
1111
[dependencies]
12-
colink = "0.2.8"
12+
colink = "0.2.9"
1313
```
1414

1515
## Getting Started

src/extensions/lock.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ type Error = Box<dyn std::error::Error + Send + Sync + 'static>;
55
impl crate::application::CoLink {
66
/// The default retry time cap is 100 ms. If you want to specify a retry time cap, use lock_with_retry_time instead.
77
pub async fn lock(&self, key: &str) -> Result<CoLinkLockToken, Error> {
8+
#[cfg(feature = "storage_macro")]
9+
let key = &key.replace('$', "_lock_dollar_");
810
self.lock_with_retry_time(key, 100).await
911
}
1012

src/extensions/storage_macro.rs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod chunk;
2+
mod db_redis;
23

34
type Error = Box<dyn std::error::Error + Send + Sync + 'static>;
45

@@ -30,12 +31,16 @@ impl crate::application::CoLink {
3031
key_name: &str,
3132
payload: &[u8],
3233
) -> Result<String, Error> {
33-
let (string_before, macro_type, _) = self._parse_macro(key_name);
34+
let (string_before, macro_type, string_after) = self._parse_macro(key_name);
3435
match macro_type.as_str() {
3536
"chunk" => {
3637
self._create_entry_chunk(string_before.as_str(), payload)
3738
.await
3839
}
40+
"redis" => {
41+
self._create_entry_redis(string_before.as_str(), string_after.as_str(), payload)
42+
.await
43+
}
3944
_ => Err(format!(
4045
"invalid storage macro, found {} in key name {}",
4146
macro_type, key_name
@@ -45,9 +50,13 @@ impl crate::application::CoLink {
4550
}
4651

4752
pub(crate) async fn _sm_read_entry(&self, key_name: &str) -> Result<Vec<u8>, Error> {
48-
let (string_before, macro_type, _) = self._parse_macro(key_name);
53+
let (string_before, macro_type, string_after) = self._parse_macro(key_name);
4954
match macro_type.as_str() {
5055
"chunk" => self._read_entry_chunk(string_before.as_str()).await,
56+
"redis" => {
57+
self._read_entry_redis(string_before.as_str(), string_after.as_str())
58+
.await
59+
}
5160
_ => Err(format!(
5261
"invalid storage macro, found {} in key name {}",
5362
macro_type, key_name
@@ -61,12 +70,16 @@ impl crate::application::CoLink {
6170
key_name: &str,
6271
payload: &[u8],
6372
) -> Result<String, Error> {
64-
let (string_before, macro_type, _) = self._parse_macro(key_name);
73+
let (string_before, macro_type, string_after) = self._parse_macro(key_name);
6574
match macro_type.as_str() {
6675
"chunk" => {
6776
self._update_entry_chunk(string_before.as_str(), payload)
6877
.await
6978
}
79+
"redis" => {
80+
self._update_entry_redis(string_before.as_str(), string_after.as_str(), payload)
81+
.await
82+
}
7083
_ => Err(format!(
7184
"invalid storage macro, found {} in key name {}",
7285
macro_type, key_name
@@ -76,9 +89,13 @@ impl crate::application::CoLink {
7689
}
7790

7891
pub(crate) async fn _sm_delete_entry(&self, key_name: &str) -> Result<String, Error> {
79-
let (string_before, macro_type, _) = self._parse_macro(key_name);
92+
let (string_before, macro_type, string_after) = self._parse_macro(key_name);
8093
match macro_type.as_str() {
8194
"chunk" => self._delete_entry_chunk(string_before.as_str()).await,
95+
"redis" => {
96+
self._delete_entry_redis(string_before.as_str(), string_after.as_str())
97+
.await
98+
}
8299
_ => Err(format!(
83100
"invalid storage macro, found {} in key name {}",
84101
macro_type, key_name

src/extensions/storage_macro/chunk.rs

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -50,34 +50,39 @@ impl crate::application::CoLink {
5050
) -> Result<String, Error> {
5151
let metadata_key = format!("{}:chunk_metadata", key_name);
5252
// lock the metadata entry to prevent simultaneous writes
53-
let lock_token = self.lock(&metadata_key.clone()).await?;
54-
// create the chunks and store them
55-
let chunk_paths = self._store_chunks(payload, key_name).await?;
56-
// make sure that the chunk paths are smaller than the maximum entry size
57-
let chunk_paths_string = self._check_chunk_paths_size(chunk_paths)?;
58-
// store the chunk paths in the metadata entry and update metadata
59-
let response = self
60-
.create_entry(&metadata_key.clone(), &chunk_paths_string.into_bytes())
61-
.await?;
53+
let lock_token = self.lock(&metadata_key).await?;
54+
// use a closure to prevent locking forever caused by errors
55+
let res = async {
56+
// create the chunks and store them
57+
let chunk_paths = self._store_chunks(payload, key_name).await?;
58+
// make sure that the chunk paths are smaller than the maximum entry size
59+
let chunk_paths_string = self._check_chunk_paths_size(chunk_paths)?;
60+
// store the chunk paths in the metadata entry and update metadata
61+
let response = self
62+
.create_entry(&metadata_key, &chunk_paths_string.into_bytes())
63+
.await?;
64+
Ok::<String, Error>(response)
65+
}
66+
.await;
6267
self.unlock(lock_token).await?;
63-
Ok(response)
68+
res
6469
}
6570

6671
#[async_recursion]
6772
pub(crate) async fn _read_entry_chunk(&self, key_name: &str) -> Result<Vec<u8>, Error> {
6873
let metadata_key = format!("{}:chunk_metadata", key_name);
69-
let metadata_response = self.read_entry(&metadata_key.clone()).await?;
70-
let payload_string = String::from_utf8(metadata_response.clone())?;
74+
let metadata_response = self.read_entry(&metadata_key).await?;
75+
let payload_string = String::from_utf8(metadata_response)?;
7176
let user_id = decode_jwt_without_validation(&self.jwt).unwrap().user_id;
7277

7378
// read the chunks into a single vector
7479
let chunks_paths = payload_string.split(';').collect::<Vec<&str>>();
7580
let mut payload = Vec::new();
7681
for (i, timestamp) in chunks_paths.iter().enumerate() {
77-
let response = self
82+
let mut response = self
7883
.read_entry(&format!("{}::{}:{}@{}", user_id, key_name, i, timestamp))
7984
.await?;
80-
payload.append(&mut response.clone());
85+
payload.append(&mut response);
8186
}
8287
Ok(payload)
8388
}
@@ -90,25 +95,30 @@ impl crate::application::CoLink {
9095
) -> Result<String, Error> {
9196
let metadata_key = format!("{}:chunk_metadata", key_name);
9297
// lock the metadata entry to prevent simultaneous writes
93-
let lock_token = self.lock(&metadata_key.clone()).await?;
94-
// split payload into chunks and update the chunks
95-
let chunk_paths = self._store_chunks(payload, key_name).await?;
96-
// make sure that the chunk paths are smaller than the maximum entry size
97-
let chunk_paths_string = self._check_chunk_paths_size(chunk_paths)?;
98-
// update the metadata entry
99-
let response = self
100-
.update_entry(&metadata_key.clone(), &chunk_paths_string.into_bytes())
101-
.await?;
98+
let lock_token = self.lock(&metadata_key).await?;
99+
// use a closure to prevent locking forever caused by errors
100+
let res = async {
101+
// split payload into chunks and update the chunks
102+
let chunk_paths = self._store_chunks(payload, key_name).await?;
103+
// make sure that the chunk paths are smaller than the maximum entry size
104+
let chunk_paths_string = self._check_chunk_paths_size(chunk_paths)?;
105+
// update the metadata entry
106+
let response = self
107+
.update_entry(&metadata_key, &chunk_paths_string.into_bytes())
108+
.await?;
109+
Ok::<String, Error>(response)
110+
}
111+
.await;
102112
self.unlock(lock_token).await?;
103-
Ok(response)
113+
res
104114
}
105115

106116
#[async_recursion]
107117
pub(crate) async fn _delete_entry_chunk(&self, key_name: &str) -> Result<String, Error> {
108118
let metadata_key = format!("{}:chunk_metadata", key_name);
109-
let lock_token = self.lock(&metadata_key.clone()).await?;
110-
let response = self.delete_entry(&metadata_key.clone()).await?;
119+
let lock_token = self.lock(&metadata_key).await?;
120+
let res = self.delete_entry(&metadata_key).await;
111121
self.unlock(lock_token).await?;
112-
Ok(response)
122+
res
113123
}
114124
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
use async_recursion::async_recursion;
2+
use redis::Commands;
3+
4+
type Error = Box<dyn std::error::Error + Send + Sync + 'static>;
5+
6+
impl crate::application::CoLink {
7+
fn _get_con_from_address(&self, redis_address: &str) -> Result<redis::Connection, Error> {
8+
let client = redis::Client::open(redis_address)?;
9+
let con = client.get_connection()?;
10+
Ok(con)
11+
}
12+
13+
async fn _get_con_from_stored_credentials(
14+
&self,
15+
key_path: &str,
16+
) -> Result<redis::Connection, Error> {
17+
let redis_url_key = format!("{}:redis_url", key_path);
18+
let redis_url = self.read_entry(redis_url_key.as_str()).await?;
19+
let redis_url_string = String::from_utf8(redis_url)?;
20+
self._get_con_from_address(redis_url_string.as_str())
21+
}
22+
23+
#[async_recursion]
24+
pub(crate) async fn _create_entry_redis(
25+
&self,
26+
address: &str,
27+
key_name: &str,
28+
payload: &[u8],
29+
) -> Result<String, Error> {
30+
let mut con = self._get_con_from_stored_credentials(address).await?;
31+
let response: i32 = con.set_nx(key_name, payload)?;
32+
if response == 0 {
33+
Err("key already exists.")?
34+
}
35+
Ok(response.to_string())
36+
}
37+
38+
#[async_recursion]
39+
pub(crate) async fn _read_entry_redis(
40+
&self,
41+
address: &str,
42+
key_name: &str,
43+
) -> Result<Vec<u8>, Error> {
44+
let mut con = self._get_con_from_stored_credentials(address).await?;
45+
let response: Option<Vec<u8>> = con.get(key_name)?;
46+
match response {
47+
Some(response) => Ok(response),
48+
None => Err("key does not exist.")?,
49+
}
50+
}
51+
52+
#[async_recursion]
53+
pub(crate) async fn _update_entry_redis(
54+
&self,
55+
address: &str,
56+
key_name: &str,
57+
payload: &[u8],
58+
) -> Result<String, Error> {
59+
let mut con = self._get_con_from_stored_credentials(address).await?;
60+
let response = con.set(key_name, payload)?;
61+
Ok(response)
62+
}
63+
64+
#[async_recursion]
65+
pub(crate) async fn _delete_entry_redis(
66+
&self,
67+
address: &str,
68+
key_name: &str,
69+
) -> Result<String, Error> {
70+
let mut con = self._get_con_from_stored_credentials(address).await?;
71+
let response: i32 = con.del(key_name)?;
72+
if response == 0 {
73+
Err("key does not exist.")?
74+
}
75+
Ok(response.to_string())
76+
}
77+
}

tests/test_storage_macro.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
use colink::{
2+
extensions::instant_server::{InstantRegistry, InstantServer},
3+
CoLink,
4+
};
5+
use rand::Rng;
6+
7+
#[tokio::test]
8+
async fn test_storage_macro_chunk() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>>
9+
{
10+
let _ir = InstantRegistry::new();
11+
let is = InstantServer::new();
12+
let cl = is.get_colink().switch_to_generated_user().await?;
13+
14+
let key_name = "storage_macro_test_chunk:$chunk";
15+
test_crud(&cl, key_name).await?;
16+
17+
Ok(())
18+
}
19+
20+
#[tokio::test]
21+
async fn test_storage_macro_redis() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>>
22+
{
23+
let _ir = InstantRegistry::new();
24+
let is = InstantServer::new();
25+
let cl = is.get_colink().switch_to_generated_user().await?;
26+
27+
cl.create_entry("storage_macro_test_redis:redis_url", b"redis://localhost")
28+
.await?;
29+
let key_name = "storage_macro_test_redis:$redis:redis_key";
30+
test_crud(&cl, key_name).await?;
31+
32+
Ok(())
33+
}
34+
35+
#[ignore]
36+
#[tokio::test]
37+
async fn test_storage_macro_chunk_redis(
38+
) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
39+
let _ir = InstantRegistry::new();
40+
let is = InstantServer::new();
41+
let cl = is.get_colink().switch_to_generated_user().await?;
42+
43+
cl.create_entry(
44+
"test_storage_macro_chunk_redis:redis_url",
45+
b"redis://localhost",
46+
)
47+
.await?;
48+
let key_name = "test_storage_macro_chunk_redis:$redis:redis_chunk:$chunk";
49+
test_crud(&cl, key_name).await?;
50+
51+
Ok(())
52+
}
53+
54+
async fn test_crud(
55+
cl: &CoLink,
56+
key_name: &str,
57+
) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
58+
let payload = rand::thread_rng()
59+
.sample_iter(&rand::distributions::Standard)
60+
.take(5e6 as usize)
61+
.collect::<Vec<u8>>();
62+
cl.create_entry(key_name, &payload.clone()).await?;
63+
assert!(cl.create_entry(key_name, b"").await.is_err());
64+
let data = cl.read_entry(key_name).await?;
65+
assert_eq!(data, payload);
66+
let new_payload = rand::thread_rng()
67+
.sample_iter(&rand::distributions::Standard)
68+
.take(3e6 as usize)
69+
.collect::<Vec<u8>>();
70+
cl.update_entry(key_name, &new_payload.clone()).await?;
71+
let data = cl.read_entry(key_name).await?;
72+
assert_eq!(data, new_payload);
73+
cl.delete_entry(key_name).await?;
74+
assert!(cl.read_entry(key_name).await.is_err());
75+
assert!(cl.delete_entry(key_name).await.is_err());
76+
Ok(())
77+
}

0 commit comments

Comments
 (0)