# rmemstore Fast, type-aware data structure cache. # About `rmemstore` is similar to other caches you may have used, like redis, but it has some differences. The primary aims of `rmemstore` is to be typesafe, fast, and useful as a data structure cache. Of course, usefulness is an ongoing exercise, as it takes time to grow features. However, `rmemstore` is a type- aware data structure store, which means you can store maps of maps - and the server knows what that means. It is fast now, however. `rmemstore` uses the new Sieve eviction strategy when pressed to eviction. With a 10:1 read:write ratio, 2 threads on an 11 year old Intel i5 server are capable of over 3.3 million operations per second. Even while being pushed to eviction. `rmemstore` is built on "safe" Rust code. It doesn't rely on subtle tricks to get speed. It does use standard libraries like the excellent `tokio` which may use dark magic, but they're trustworthy. `rmemstore` uses bare tcp - no application frameworks. Each 0 and every 1 that your network card transmits to or from an `rmemstored` server has a direct purpose. Inventing a new ostensibly-portable wire protocol is a vaguely hubric exercise when suitable alternatives exist. With that in mind, `rmemstore` uses `protosockets`, which is a compromise between the aforementioned hubris and pragmatism. # Protocol The tcp stream inbound to `rmemstored` is a stream of standard, length-delimited protocol buffers `rmemstore.Rpc` structures. These messages carry an id, and `rmemstored` responds with that id - possibly out of order. It is a multithreaded, multiplexing server. You can send as much as you want as fast as you can, subject to your network and cpu capabilities. The tcp stream outbound from `rmemstored` is a stream of standard, length-delimited protocol buffers `rmemstore.Response` structures. These messages carry the id from the Rpc that initiated the response. Every `rmemstore.Rpc` has a corresponding `rmemstore.Response`. Inbound and outbound streams are: `varint` `message` `varint` `message`[...]. The varint before the message is the length of the message. So once you have read the bytes for `varint` and the length of `varint`, you have a complete message. # Languages ## Rust You can look at [`rmem`](./rmem/src/main.rs) for an example of how you can use the client. Usage boils down to 3 lines: ```rust let mut configuration = rmemstore::ClientConfiguration::new(); let client = configuration.connect(args.host.to_string()).await?; client.put("some key", "some value").await?; ``` You can also put dictionaries: ```rust client.put( "some key", HashMap::<&str, &str>::from_iter([ ("hello", "world") ]), ).await?; ``` or dictionaries of strings and dictionaries, however wild you want to get: ```rust client .put( "some key", HashMap::<&str, MemstoreValue>::from_iter([ ( "hello", MemstoreValue::String { string: "world".to_string(), }, ), ( "nested", MemstoreValue::Map { map: HashMap::from_iter([( "inner".to_string(), MemstoreValue::String { string: "values".to_string(), }, )]), }, ), ]), ) .await?; ``` ## Bash You can use `rms` to put and get. For strings, the output is a little more brief. ```bash $ rms put foo `{"string": "some value"}` ``` ```bash $ rms get foo some value ``` For maps, the interaction has some verbosity, but it is typed! ```bash $ rms put foo '{"map": {"bar":{"map":{"baz":{"string": "haha"}, "other": {"string": "verbose"}}, "outer": {"string": "another"}}}}' ``` ``` $ rms get foo { "bar": { "map": { "baz": { "string": "haha" }, "other": { "string": "verbose" } } } } ``` ## Python Don't want to use rust? Any tool or language capable of sending and receiving protocol buffers-encoded bytes over tcp is capable of using `rmemstored`. See [`example-python`](./example-python/main.py) for an example in another language. Note that python, in particular, is a bit of a pain due to not exposing the protobuf varint encoder. # Comparisons ## k-cache internal cache implementation Rather than using the popular `moka` cache, rmemstore has its own cache implementation. Here's an example result from [the benchmarks](./k-cache/benches) that motivates this deviation: ![benchmark data showing 2.2-5x better latency for k-cache](./k-cache/example-benchmark-run.svg) You can see that the benchmark under eviction favors k-cache at all thread counts. Note that sieve pays on insert, so this 100% insert benchmark is pessimistic, and get will outperform by a wider margin.