Skip to content

Commit 19331d6

Browse files
authored
feat(outbound-redis): Add DEL command (#828)
* chore(outbound-redis): Add basic integration test Signed-off-by: Konstantin Shabanov <[email protected]> * feat(outbound-redis): Add DEL command Closes: #801. Signed-off-by: Konstantin Shabanov <[email protected]> * feat(go-sdk): Add INCR and DEL support Signed-off-by: Konstantin Shabanov <[email protected]> Signed-off-by: Konstantin Shabanov <[email protected]>
1 parent 29fd382 commit 19331d6

File tree

16 files changed

+291
-4
lines changed

16 files changed

+291
-4
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ vergen = { version = "7", default-features = false, features = [ "build", "git"
7575
[features]
7676
default = []
7777
e2e-tests = []
78+
outbound-redis-tests = []
7879

7980
[workspace]
8081
members = [

Makefile

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ build:
99
.PHONY: test
1010
test: lint test-unit test-integration
1111

12-
.PHONY: lint
12+
.PHONY: lint
1313
lint:
1414
cargo clippy --all-targets --all-features -- -D warnings
1515
cargo fmt --all -- --check
@@ -30,9 +30,13 @@ test-integration:
3030

3131
.PHONY: test-e2e
3232
test-e2e:
33-
RUST_LOG=$(LOG_LEVEL) cargo test --test integration --features e2e-tests --no-fail-fast -- integration_tests::test_dependencies --nocapture
33+
RUST_LOG=$(LOG_LEVEL) cargo test --test integration --features e2e-tests --no-fail-fast -- integration_tests::test_dependencies --nocapture
3434
RUST_LOG=$(LOG_LEVEL) cargo test --test integration --features e2e-tests --no-fail-fast -- --skip integration_tests::test_dependencies --nocapture
3535

36+
.PHONY: test-outbound-redis
37+
test-outbound-redis:
38+
RUST_LOG=$(LOG_LEVEL) cargo test --test integration --features outbound-redis-tests --no-fail-fast -- --nocapture
39+
3640
.PHONY: test-sdk-go
3741
test-sdk-go:
3842
$(MAKE) -C sdk/go test

build.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use cargo_target_dep::build_target_dep;
88

99
const RUST_HTTP_INTEGRATION_TEST: &str = "tests/http/simple-spin-rust";
1010
const RUST_HTTP_INTEGRATION_ENV_TEST: &str = "tests/http/headers-env-routes-test";
11+
const RUST_OUTBOUND_REDIS_INTEGRATION_TEST: &str = "tests/outbound-redis/http-rust-outbound-redis";
1112

1213
fn main() {
1314
println!("cargo:rerun-if-changed=build.rs");
@@ -53,6 +54,7 @@ error: the `wasm32-wasi` target is not installed
5354

5455
cargo_build(RUST_HTTP_INTEGRATION_TEST);
5556
cargo_build(RUST_HTTP_INTEGRATION_ENV_TEST);
57+
cargo_build(RUST_OUTBOUND_REDIS_INTEGRATION_TEST);
5658

5759
let mut config = vergen::Config::default();
5860
*config.git_mut().sha_kind_mut() = vergen::ShaKind::Short;

crates/abi-conformance/src/outbound_redis.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ pub struct RedisReport {
4141
/// function with the arguments \["127.0.0.1", "foo"\] and expect `ok(42)` as the result. The host will assert
4242
/// that said function is called exactly once with the specified arguments.
4343
pub incr: Result<(), String>,
44+
45+
/// Result of the Redis `DEL` test
46+
///
47+
/// The guest module should expect a call according to [`super::InvocationStyle`] with \["outbound-redis-del",
48+
/// "127.0.0.1", "foo"\] as arguments. The module should call the host-implemented `outbound-redis::del`
49+
/// function with the arguments \["127.0.0.1", \["foo"\]\] and expect `ok(0)` as the result. The host will assert
50+
/// that said function is called exactly once with the specified arguments.
51+
pub del: Result<(), String>,
4452
}
4553

4654
wit_bindgen_wasmtime::export!("../../wit/ephemeral/outbound-redis.wit");
@@ -51,6 +59,7 @@ pub(super) struct OutboundRedis {
5159
set_set: HashSet<(String, String, Vec<u8>)>,
5260
get_map: HashMap<(String, String), Vec<u8>>,
5361
incr_map: HashMap<(String, String), i64>,
62+
del_map: HashMap<(String, String), i64>,
5463
}
5564

5665
impl outbound_redis::OutboundRedis for OutboundRedis {
@@ -93,6 +102,12 @@ impl outbound_redis::OutboundRedis for OutboundRedis {
93102
.map(|value| value + 1)
94103
.ok_or(outbound_redis::Error::Error)
95104
}
105+
106+
fn del(&mut self, address: &str, keys: Vec<&str>) -> Result<i64, outbound_redis::Error> {
107+
self.del_map
108+
.remove(&(address.into(), format!("{keys:?}")))
109+
.ok_or(outbound_redis::Error::Error)
110+
}
96111
}
97112

98113
pub(super) fn test(store: &mut Store<Context>, pre: &InstancePre<Context>) -> Result<RedisReport> {
@@ -183,5 +198,26 @@ pub(super) fn test(store: &mut Store<Context>, pre: &InstancePre<Context>) -> Re
183198
},
184199
)
185200
},
201+
202+
del: {
203+
store.data_mut().outbound_redis.del_map.insert(
204+
("127.0.0.1".into(), format!("{:?}", vec!["foo".to_owned()])),
205+
0,
206+
);
207+
208+
super::run_command(
209+
store,
210+
pre,
211+
&["outbound-redis-del", "127.0.0.1", "foo"],
212+
|store| {
213+
ensure!(
214+
store.data().outbound_redis.del_map.is_empty(),
215+
"expected module to call `outbound-redis::del` exactly once"
216+
);
217+
218+
Ok(())
219+
},
220+
)
221+
},
186222
})
187223
}

crates/outbound-redis/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ impl outbound_redis::OutboundRedis for OutboundRedis {
4141
let value = conn.incr(key, 1).await.map_err(log_error)?;
4242
Ok(value)
4343
}
44+
45+
async fn del(&mut self, address: &str, keys: Vec<&str>) -> Result<i64, Error> {
46+
let conn = self.get_conn(address).await.map_err(log_error)?;
47+
let value = conn.del(keys).await.map_err(log_error)?;
48+
Ok(value)
49+
}
4450
}
4551

4652
impl OutboundRedis {

examples/tinygo-outbound-redis/main.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"net/http"
55
"os"
6+
"strconv"
67

78
spin_http "github.com/fermyon/spin/sdk/go/http"
89
"github.com/fermyon/spin/sdk/go/redis"
@@ -41,6 +42,25 @@ func init() {
4142
} else {
4243
w.Write([]byte("mykey value was: "))
4344
w.Write(payload)
45+
w.Write([]byte("\n"))
46+
}
47+
48+
// incr `spin-go-incr` by 1
49+
if payload, err := redis.Incr(addr, "spin-go-incr"); err != nil {
50+
http.Error(w, err.Error(), http.StatusInternalServerError)
51+
} else {
52+
w.Write([]byte("spin-go-incr value: "))
53+
w.Write([]byte(strconv.FormatInt(payload, 10)))
54+
w.Write([]byte("\n"))
55+
}
56+
57+
// delete `spin-go-incr` and `mykey`
58+
if payload, err := redis.Del(addr, []string{"spin-go-incr", "mykey", "non-existing-key"}); err != nil {
59+
http.Error(w, err.Error(), http.StatusInternalServerError)
60+
} else {
61+
w.Write([]byte("deleted keys num: "))
62+
w.Write([]byte(strconv.FormatInt(payload, 10)))
63+
w.Write([]byte("\n"))
4464
}
4565
})
4666
}

sdk/go/redis/internals.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,45 @@ func set(addr, key string, payload []byte) error {
6666
return toErr(err)
6767
}
6868

69+
func incr(addr, key string) (int64, error) {
70+
caddr := redisStr(addr)
71+
ckey := redisStr(key)
72+
73+
var cpayload C.int64_t
74+
75+
defer func() {
76+
C.outbound_redis_string_free(&caddr)
77+
C.outbound_redis_string_free(&ckey)
78+
}()
79+
80+
err := C.outbound_redis_incr(&caddr, &ckey, &cpayload)
81+
return int64(cpayload), toErr(err)
82+
}
83+
84+
func del(addr string, keys []string) (int64, error) {
85+
caddr := redisStr(addr)
86+
ckeys := redisListStr(keys)
87+
88+
var cpayload C.int64_t
89+
90+
defer func() {
91+
C.outbound_redis_string_free(&caddr)
92+
C.outbound_redis_list_string_free(&ckeys)
93+
}()
94+
95+
err := C.outbound_redis_del(&caddr, &ckeys, &cpayload)
96+
return int64(cpayload), toErr(err)
97+
}
98+
99+
func redisListStr(xs []string) C.outbound_redis_list_string_t {
100+
var cxs []C.outbound_redis_string_t
101+
102+
for i := 0; i < len(xs); i++ {
103+
cxs = append(cxs, redisStr(xs[i]))
104+
}
105+
return C.outbound_redis_list_string_t{ptr: &cxs[0], len: C.size_t(len(cxs))}
106+
}
107+
69108
func redisStr(x string) C.outbound_redis_string_t {
70109
return C.outbound_redis_string_t{ptr: C.CString(x), len: C.size_t(len(x))}
71110
}

sdk/go/redis/outbound-redis.c

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,22 @@ typedef struct {
5656
outbound_redis_error_t err;
5757
} val;
5858
} outbound_redis_expected_payload_error_t;
59+
typedef struct {
60+
bool is_err;
61+
union {
62+
int64_t ok;
63+
outbound_redis_error_t err;
64+
} val;
65+
} outbound_redis_expected_s64_error_t;
66+
void outbound_redis_list_string_free(outbound_redis_list_string_t *ptr) {
67+
for (size_t i = 0; i < ptr->len; i++) {
68+
outbound_redis_string_free(&ptr->ptr[i]);
69+
}
70+
canonical_abi_free(ptr->ptr, ptr->len * 8, 4);
71+
}
5972

60-
__attribute__((aligned(4)))
61-
static uint8_t RET_AREA[12];
73+
__attribute__((aligned(8)))
74+
static uint8_t RET_AREA[16];
6275
__attribute__((import_module("outbound-redis"), import_name("publish")))
6376
void __wasm_import_outbound_redis_publish(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t);
6477
outbound_redis_error_t outbound_redis_publish(outbound_redis_string_t *address, outbound_redis_string_t *channel, outbound_redis_payload_t *payload) {
@@ -123,3 +136,47 @@ outbound_redis_error_t outbound_redis_set(outbound_redis_string_t *address, outb
123136
}
124137
}return expected.is_err ? expected.val.err : -1;
125138
}
139+
__attribute__((import_module("outbound-redis"), import_name("incr")))
140+
void __wasm_import_outbound_redis_incr(int32_t, int32_t, int32_t, int32_t, int32_t);
141+
outbound_redis_error_t outbound_redis_incr(outbound_redis_string_t *address, outbound_redis_string_t *key, int64_t *ret0) {
142+
int32_t ptr = (int32_t) &RET_AREA;
143+
__wasm_import_outbound_redis_incr((int32_t) (*address).ptr, (int32_t) (*address).len, (int32_t) (*key).ptr, (int32_t) (*key).len, ptr);
144+
outbound_redis_expected_s64_error_t expected;
145+
switch ((int32_t) (*((uint8_t*) (ptr + 0)))) {
146+
case 0: {
147+
expected.is_err = false;
148+
149+
expected.val.ok = *((int64_t*) (ptr + 8));
150+
break;
151+
}
152+
case 1: {
153+
expected.is_err = true;
154+
155+
expected.val.err = (int32_t) (*((uint8_t*) (ptr + 8)));
156+
break;
157+
}
158+
}*ret0 = expected.val.ok;
159+
return expected.is_err ? expected.val.err : -1;
160+
}
161+
__attribute__((import_module("outbound-redis"), import_name("del")))
162+
void __wasm_import_outbound_redis_del(int32_t, int32_t, int32_t, int32_t, int32_t);
163+
outbound_redis_error_t outbound_redis_del(outbound_redis_string_t *address, outbound_redis_list_string_t *keys, int64_t *ret0) {
164+
int32_t ptr = (int32_t) &RET_AREA;
165+
__wasm_import_outbound_redis_del((int32_t) (*address).ptr, (int32_t) (*address).len, (int32_t) (*keys).ptr, (int32_t) (*keys).len, ptr);
166+
outbound_redis_expected_s64_error_t expected;
167+
switch ((int32_t) (*((uint8_t*) (ptr + 0)))) {
168+
case 0: {
169+
expected.is_err = false;
170+
171+
expected.val.ok = *((int64_t*) (ptr + 8));
172+
break;
173+
}
174+
case 1: {
175+
expected.is_err = true;
176+
177+
expected.val.err = (int32_t) (*((uint8_t*) (ptr + 8)));
178+
break;
179+
}
180+
}*ret0 = expected.val.ok;
181+
return expected.is_err ? expected.val.err : -1;
182+
}

sdk/go/redis/outbound-redis.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,16 @@ extern "C"
2424
size_t len;
2525
} outbound_redis_payload_t;
2626
void outbound_redis_payload_free(outbound_redis_payload_t *ptr);
27+
typedef struct {
28+
outbound_redis_string_t *ptr;
29+
size_t len;
30+
} outbound_redis_list_string_t;
31+
void outbound_redis_list_string_free(outbound_redis_list_string_t *ptr);
2732
outbound_redis_error_t outbound_redis_publish(outbound_redis_string_t *address, outbound_redis_string_t *channel, outbound_redis_payload_t *payload);
2833
outbound_redis_error_t outbound_redis_get(outbound_redis_string_t *address, outbound_redis_string_t *key, outbound_redis_payload_t *ret0);
2934
outbound_redis_error_t outbound_redis_set(outbound_redis_string_t *address, outbound_redis_string_t *key, outbound_redis_payload_t *value);
35+
outbound_redis_error_t outbound_redis_incr(outbound_redis_string_t *address, outbound_redis_string_t *key, int64_t *ret0);
36+
outbound_redis_error_t outbound_redis_del(outbound_redis_string_t *address, outbound_redis_list_string_t *keys, int64_t *ret0);
3037
#ifdef __cplusplus
3138
}
3239
#endif

sdk/go/redis/redis.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,16 @@ func Get(addr, key string) ([]byte, error) {
3636
func Set(addr, key string, payload []byte) error {
3737
return set(addr, key, payload)
3838
}
39+
40+
// Increments the number stored at key by one. If the key does not exist,
41+
// it is set to 0 before performing the operation. An error is returned if
42+
// the key contains a value of the wrong type or contains a string that can not
43+
// be represented as integer.
44+
func Incr(addr, key string) (int64, error) {
45+
return incr(addr, key)
46+
}
47+
48+
// Removes the specified keys. A key is ignored if it does not exist.
49+
func Del(addr string, keys []string) (int64, error) {
50+
return del(addr, keys)
51+
}

tests/integration.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,30 @@ mod integration_tests {
602602
}
603603
}
604604

605+
#[cfg(feature = "outbound-redis-tests")]
606+
mod outbound_pg_tests {
607+
use super::*;
608+
609+
const RUST_OUTBOUND_REDIS_INTEGRATION_TEST: &str =
610+
"tests/outbound-redis/http-rust-outbound-redis";
611+
612+
#[tokio::test]
613+
async fn test_outbound_redis_rust_local() -> Result<()> {
614+
let s = SpinTestController::with_manifest(
615+
&format!(
616+
"{}/{}",
617+
RUST_OUTBOUND_REDIS_INTEGRATION_TEST, DEFAULT_MANIFEST_LOCATION
618+
),
619+
&[],
620+
None,
621+
)
622+
.await?;
623+
624+
assert_status(&s, "/test", 204).await?;
625+
Ok(())
626+
}
627+
}
628+
605629
#[tokio::test]
606630
async fn test_simple_rust_local() -> Result<()> {
607631
let s = SpinTestController::with_manifest(
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[build]
2+
target = "wasm32-wasi"
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[package]
2+
name = "http-rust-outbound-redis"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[lib]
7+
crate-type = [ "cdylib" ]
8+
9+
[dependencies]
10+
# Useful crate to handle errors.
11+
anyhow = "1"
12+
# Crate to simplify working with bytes.
13+
bytes = "1"
14+
# General-purpose crate with common HTTP types.
15+
http = "0.2"
16+
# The Spin SDK.
17+
spin-sdk = { path = "../../../sdk/rust" }
18+
# Crate that generates Rust Wasm bindings from a WebAssembly interface.
19+
wit-bindgen-rust = { git = "https://github.com/bytecodealliance/wit-bindgen", rev = "cb871cfa1ee460b51eb1d144b175b9aab9c50aba" }
20+
21+
[workspace]
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
spin_version = "1"
2+
authors = ["Fermyon Engineering <[email protected]>"]
3+
name = "rust-outbound-redis-example"
4+
trigger = { type = "http", base = "/" }
5+
version = "0.1.0"
6+
7+
[[component]]
8+
environment = { REDIS_ADDRESS = "redis://127.0.0.1:6379" }
9+
id = "outbound-redis"
10+
source = "target/wasm32-wasi/release/http_rust_outbound_redis.wasm"
11+
[component.trigger]
12+
route = "/test"
13+
[component.build]
14+
command = "cargo build --target wasm32-wasi --release"
15+

0 commit comments

Comments
 (0)