lab/lab1/
mod.rs

1#![allow(unused_variables)]
2//! Welcome to Lab 1! The goal of this lab is to implement a key-value storage
3//! service that is called via RPCs
4//!
5//! ## Lab 1
6//!
7//! Welcome to Lab 1. The goal of this lab is to implement a key-value storage
8//! service that is called via RPCs. In particular you need to:
9//!
10//! 1. Implement a key-value storage server type that satisfies the
11//!    [tribbler::storage::Storage] trait and takes http RPC requests from the
12//!    network.
13//! 2. Implement a key-value storage client type that satisfies the
14//!    [tribbler::storage::Storage] trait which relays all of its requests back
15//!    to the server.
16//!
17//! More specifically, you need to implement two entry functions that are
18//! defined in the `lab/src/lab1/lab.rs` file: [serve_back] and [new_client].
19//! Presently, they are both implemented with [todo!].
20//!
21//! ## Get Your Repo
22//!
23//! As mentioned earlier, we will send out an invitation link on Piazza/Canvas.
24//! Once available, access it and follow the instructions provided in the
25//! repository to get started.
26//!
27//! ## The Key-value Pair Service Interface
28//!
29//! The goal of Lab 1 is to wrap a key-value pair interface with RPC. You don't
30//! need to implement the key-value pair storage by yourself, but you need to
31//! use it extensively in later labs, so it will be good for you to understand
32//! the service semantics here.
33//!
34//! The data structure and interfaces for the key-value pair service are defined
35//! in the [tribbler::storage] module (in the [tribbler] crate). The main
36//! interface is [tribbler::storage::Storage], which consists of three logical
37//! parts.
38//!
39//! First is the key-string pair part, which is its own interface.
40//!
41//! ```rust
42//! #[async_trait] // important annotation!
43//! /// Key-value pair interfaces
44//! /// Default value for all keys is empty string
45//! pub trait KeyString {
46//!     /// Gets a value. If no value set, return [None]
47//!     async fn get(&self, key: &str) -> TribResult<Option<String>>;
48//!
49//!     /// Set kv.key to kv.value. return true when no error.
50//!     async fn set(&self, kv: &KeyValue) -> TribResult<bool>;
51//!
52//!     /// List all the keys of non-empty pairs where the key matches
53//!     /// the given pattern.
54//!     async fn keys(&self, p: &Pattern) -> TribResult<List>;
55//! }
56//! ```
57//!
58//! [Pattern](tribbler::storage::Pattern) is a (prefix, suffix) tuple. It has a
59//! `match` function that returns true when the string matches has the prefix
60//! and suffix of the pattern.
61//!
62//! The second part is the key-list pair interface that handles list-valued
63//! key-value pairs.
64//!
65//! ```rust
66//! #[async_trait]
67//! /// Key-list interfaces
68//! pub trait KeyList {
69//!     /// Get the list. Empty if not set.
70//!     async fn list_get(&self, key: &str) -> TribResult<List>;
71//!
72//!     /// Append a string to the list. return true when no error.
73//!     async fn list_append(&self, kv: &KeyValue) -> TribResult<bool>;
74//!
75//!     /// Removes all elements that are equal to `kv.value` in list `kv.key`
76//!     /// returns the number of elements removed.
77//!     async fn list_remove(&self, kv: &KeyValue) -> TribResult<u32>;
78//!
79//!     /// List all the keys of non-empty lists, where the key matches
80//!     /// the given pattern.
81//!     async fn list_keys(&self, p: &Pattern) -> TribResult<List>;
82//! }
83//! ```
84//!
85//! The [Storage](tribbler::storage::Storage) interface glues these two
86//! interfaces together, and also includes an auto-incrementing clock feature.
87//!
88//! ```rust
89//! #[async_trait]
90//! pub trait Storage: KeyString + KeyList + Send + Sync {
91//!     /// Returns an auto-incrementing clock. The returned value of each call will
92//!     /// be unique, no smaller than `at_least`, and strictly larger than the
93//!     /// value returned last time, unless it was [u64::MAX]
94//!     async fn clock(&self, at_least: u64) -> TribResult<u64>;
95//! }
96//! ```
97//!
98//! Note that the function signatures of these methods are already RPC-friendly.
99//! You should implement the RPC interface with the rust-based gRPC framework
100//! [`tonic`](https://docs.rs/tonic/latest/tonic/).  By doing this, another
101//! person's client that speaks the same protocol will be able to talk to your
102//! server as well.
103//!
104//! All errors you see from this interface will be communication errors. You can
105//! assume that each call (on the same key) is an atomic transaction; two
106//! concurrent writes won't give the key a weird value that came from nowhere.
107//! However, when an error occurs, the caller won't know if the transaction
108//! committed or not, because the error might have occured before or after the
109//! transaction executed on the server.
110//!
111//! ## Rust and Async
112//!
113//! Rust has this neat keyword called `async`. Function scopes can be marked as
114//! async which allows you to use a special keyword called `.await` within them.
115//! Using `.await` on an async function an executor (from a crate like [tokio])
116//! to drive the computation to completion. This is especially great for
117//! functions which need to do some kind of IO. Return values of async functions
118//! are implicitly wrapped by a [Future](std::future::Future). Calling `.await`
119//! on a future will retrieve its value.
120//!
121//! ```rust
122//! async fn do_io() -> bool {
123//!     println!("look ma! I'm doin' I/O!");
124//!     true
125//! }
126//!
127//! #[tokio::main]
128//! async fn main() {
129//!     let fute = do_io(); // type: Future<Output = bool>;
130//!     let result = fute.await; // type: bool
131//!     println!("we got the future value: {}", result);
132//! }
133//! ```
134//!
135//! This might sound and look confusing, but you don't need to understand the
136//! internals of how this happens. Just understand that if you see `async fn`,
137//! you can call `.await` on other `async fn` inside of it. There's [an entirely
138//! separate book for async
139//! Rust](https://rust-lang.github.io/async-book/01_getting_started/01_chapter.html).
140//! You can read this if you want to understand more.
141//!
142//! Lastly, there's one more note about `async` in Rust. Currently, the async
143//! language features are still in their infancy. A consequence of this is that
144//! the compiler doesn't support `async` in trait definitions. However, a
145//! [special crate](async_trait) supplying the `#[async_trait::async_trait]`
146//! annotation allows you to use `async` on trait functions through some
147//! syntactic sugar and tricky macros. You should use it when implementing parts
148//! of the lab.
149//!
150//!
151//! ## Entry Functions
152//!
153//! These are the two entry functions you need to implement for this Lab.  This
154//! is how other people's code (and your own code in later labs) will use your
155//! code.
156//!
157//! ### Server-side
158//!
159//! ```
160//! pub async fn serve_back(config: BackConfig) -> TribResult<()>
161//! ```
162//!
163//! This function creates an instance of a back-end server based on
164//! configuration [tribbler::config::BackConfig]
165//!
166//! - `addr` is the address the server should listen on, in the form of
167//!   `<host>:<port>`
168//! - `storage` is the storage device you will use for storing data. You should
169//!   not store persistent data anywhere else.
170//! - `ready` is a channel for notifying the other parts in the program that the
171//!   server is ready to accept RPC calls from the network (indicated by the
172//!   server sending the value `true`) or if the setup failed (indicated by
173//!   sending `false`). `ready` might be `None`, which means the caller does not
174//!   care about when the server is ready.
175//! - `shutdown` is another type of channel for receiving a shutdown
176//! notification. when a message is received on this channel, the server should
177//! shut down. **Hint**: take a look at
178//! [serve_with_shutdown](tonic::transport::server::Router)
179//!
180//! This function should block indefinitely unless there is errors or the server
181//! is sent a shutdown signal. It is `async`, you should be able to call
182//! `.await` on futures within it.
183//!
184//! Note that you don't need to (and should not) implement the key-value pair
185//! storage service yourself.  You only need to wrap the given
186//! [tribbler::storage::Storage] with RPC, so that a remote client can access it
187//! via the network.
188//!
189//! ### Client-side
190//!
191//! ```rust
192//! pub async fn new_client(addr: &str) -> TribResult<Box<dyn Storage>>
193//! ```
194//!
195//! This function takes `addr` in the form of `<host>:<port>`, and connects to
196//! this address for an http RPC server. It returns an implementation of
197//! [tribbler::storage::Storage], which will provide the interface, and forward
198//! all calls as RPCs to the server. You can assume that `addr` will always be a
199//! valid TCP address.
200//!
201//! Note that when [new_client] is called, the server may not have started yet.
202//! While it is okay to try to connect to the server at this time, you should
203//! not report any error if your attempt fails. It might be best to wait to
204//! establish the connection until you need it to perform your first RPC
205//! function call.
206//!
207//! ## RPCs via gRPC, protobuf, and tonic
208//!
209//! Rust doesn't have any functionality built in to the standard libary to make
210//! writing RPCs easier, so we're using a few packages to reduce the amount of
211//! code that you are required to write to complete the lab. Here are some terms
212//! you should know:
213//!
214//! - **gRPC** is a language-agnostic RPC framework
215//! - **protobuf** (or protocol buffers) is an RPC message and service interface
216//!   language (`.proto` files) and a wire serialization format.
217//! - **tonic** is a gRPC server and client implementation which has protobuf
218//! compilation support baked in.
219//!
220//! Tonic is a gRPC server and client implementation written and designed in
221//! rust. gRPC implementations use a protobuf (`.proto`) file to define message
222//! types and RPC service interfaces. You can find the proto files for the
223//! tribbler service defined in `tribbler/proto/rpc.proto`. During the build
224//! process, cargo will compile the RPC definitions into [tribbler::rpc] . You
225//! shouldn't need to make changes to either of these files to complete the lab.
226//! The types defined in [tribbler::rpc] are used to implement the
227//! tonic-specific RPC methods.
228//!
229//! ## Testing
230//!
231//! Defining unit tests in rust is simple. Write a `mod test {}` tests within
232//! the module you wish to test using and annotate it with `#[cfg(test)]`. To
233//! run the tests, you can then simply run `cargo test`. Example
234//!
235//! ```rust
236//! // your code here
237//! // ...
238//!
239//! #[cfg(test)]
240//! mod test {
241//!   #[test]
242//!   fn my_test() {
243//!     // test code here
244//!   }
245//! }
246//!
247//! // In your terminal
248//! // $ cargo test
249//! ```
250//!
251//! A non-exhaustive suite of tests are already provided inside of the
252//! [lab](crate) module.
253//!
254//! When you implement the logic behind Lab 1, you should pass these tests, and
255//! you can be fairly confident that you'll get at least 30% of the credit for
256//! Lab 1 (assuming you're not cheating somehow).
257//!
258//! However, the tests that come with the repository are fairly basic and
259//! simple. Though you're not required to, you should consider writing more test
260//! cases to make sure your implementation matches the specification.
261//!
262//! For more information on writing tests in Rust, please read the
263//! [testing](https://doc.rust-lang.org/book/ch11-01-writing-tests.html)
264//! documentation in the Rust book.
265//!
266//! ## Starting Hints
267//!
268//! While you are free to do the project in your own way as long as it fits the
269//! specification, matches the interfaces, and passes the tests, here are some
270//! suggested first steps.
271//!
272//! First, create a `client.rs` file under the [crate::lab1] module, declare a
273//! new struct called `StorageClient`, and make it visible to your lab 1 module:
274//!
275//! ```rust
276//! // ... in lab/src/lab1/client.rs
277//! pub struct StorageClient;
278//! // ... in lab/src/lab1/mod.rs
279//! mod client;
280//! ```
281//!
282//! Then, add method implementations to this new `StorageClient` type so that it
283//! matches the [tribbler::storage::Storage] trait. For example, for the `get()`
284//! function:
285//!
286//! ```rust
287//! use async_trait::async_trait;
288//!
289//! #[async_trait] // VERY IMPORTANT !!
290//! impl KeyString for StorageClient {
291//!     async fn get(&self, key: &str) -> TribResult<Option<String>> {
292//!         todo!()
293//!     }
294//! }
295//! ```
296//!
297//! After you've added all of the functions with [todo!]s, you should have
298//! client type that satisfies [tribbler::storage::Storage], we can return this
299//! type in our entry function [new_client]. Remove the `todo!()` line in
300//! [new_client], and replace it by returning a new `client` object. The
301//! function returns a `TribResult<Box<dyn Storage>>` so we need to wrap our
302//! type with `Ok(Box::new())` to satisfy the type constraints. Now the
303//! [new_client] function should look something like this:
304//!
305//! ```rust
306//! use crate::lab1::client::StorageClient;
307//! pub async fn new_client(addr: &str) -> TribResult<Box<dyn Storage>> {
308//!     Ok(Box::new(StorageClient { }))
309//! }
310//! ```
311//!
312//! Now all you need to do for the client half is to fill in the code skeleton
313//! with the correct RPC logic, removing the [todo!]s along the way. You
314//! probably will need to add fields to your struct. E.g. the `addr` parameter
315//! is something you should probably store. Note that in rust [str] and [String]
316//! are distinct types. [Read more
317//! here](https://doc.rust-lang.org/book/ch08-02-strings.html). To convert from
318//! `&str` to [String], call the `to_string()` method.
319//!
320//! ```rust
321//! pub struct StorageClient {
322//!     pub addr: String,
323//! }
324//!
325//! // now when creating a new client:
326//! // let _ = StorageClient { addr: addr.to_string() };
327//! ```
328//!
329//! To implement the RPCs, we need to
330//!
331//! ```rust
332//! use tribbler::rpc;
333//! ```
334//!
335//! and then implement the methods on the
336//! [tribbler::rpc::trib_storage_client::TribStorageClient] RPC client service
337//! stub. The examples in the [tonic] documentation show how to write the basic RPC
338//! client logic.
339//!
340//! Following their example, you could create a `get()` method that looks
341//! something like this (though you shouldn't be connecting a new `client` on
342//! every RPC):
343//!
344//! ```rust
345//! use async_trait::async_trait;
346//! use tribbler::rpc::trib_storage_client::TribStorageClient;
347//!
348//! #[async_trait] // VERY IMPORTANT !!
349//! impl KeyString for StorageClient {
350//!     async fn get(&self, key: &str) -> TribResult<Option<String>> {
351//!         let mut client = TribStorageClient::connect(self.addr.clone()).await?;
352//!         let r = client.get(Key {
353//!             key: key.to_string(),
354//!         }).await?;
355//!         Ok(Some(r.into_inner().value))
356//!     }
357//!   ...
358//! }
359//! ```
360//!
361//! However, if you do it this way, you will open a new HTTP connection for
362//! every RPC call. This approach is acceptable but obviously not the most
363//! efficient way available to you.  We leave it to you to figure out how to
364//! maintain a persistent RPC connection, if it's something you want to tackle.
365//!
366//! We recommend reading the [tonic] documentation and examples from the
367//! [project
368//! repository](https://github.com/hyperium/tonic/tree/master/examples/src) to
369//! see how to efficiently utilize the library.
370//!
371//! Once you've completed the client side, you also need to wrap the server side
372//! in the [serve_back] function using the same [tribbler::rpc] module. This
373//! should be similar to the examples in the `tonic` documentation. You do this
374//! by creating a new struct for the RPC server, and implementing the the
375//! [TribStorage](tribbler::rpc::trib_storage_server::TribStorage) trait for the
376//! server side. You should write a new struct which implements this trait. Just
377//! remember that you need to send a `true` over the `ready`
378//! [channel](std::sync::mpsc::channel) when the service is ready (when `ready`
379//! is not `None`), and send a `false` when you encounter any error on starting
380//! your service. The `shutdown` signal also needs to be handled similarly.
381//!
382//! When all of these changes are done, you should pass the test cases written
383//! in the `lab1_test.rs` file. It performs some basic checks to see if an RPC
384//! client and a server (that runs on the same host) will satisfy the
385//! specification of a key-value pair service (as a local
386//! [tribbler::storage::Storage] does without RPC).
387//!
388//!
389//! ```console
390//! # run the lab 1 tests
391//! $ cargo test -p lab --tests -- --test-threads 1
392//! ```
393//!
394//! ## Syntactic Sugar for RPC Server Implementations with `async_trait`
395//!
396//! It's very helpful to utilize the `#[async_trait]` mentioned earlier. While
397//! implementing the storage server, You might encounter examples of
398//! implementation that look like:
399//!
400//! ```rust
401//! // StorageServer is a type you can define in your lab
402//! impl rpc::trib_storage_server::TribStorage for StorageServer {
403//!     fn set<'life0, 'async_trait>(
404//!         &'life0 self,
405//!         request: tonic::Request<rpc::KeyValue>,
406//!     ) -> core::pin::Pin<
407//!         Box<
408//!             dyn core::future::Future<Output = Result<tonic::Response<rpc::Bool>, tonic::Status>>
409//!                 + core::marker::Send
410//!                 + 'async_trait,
411//!         >,
412//!     >
413//!     where
414//!         'life0: 'async_trait,
415//!         Self: 'async_trait,
416//!     {
417//!         todo!()
418//!     }
419//!     // ... more method implementations below
420//! }
421//! ```
422//!
423//! Gross! Reading that type signature can be quite difficult. All those
424//! [core::pin::Pin] and [Box] types correspond to internal workings of `async`.
425//!  Fortunately, `async_trait` can make it so that we don't need to worry about
426//!  all that.
427//!
428//! Using the [async_trait::async_trait] above your `impl` results in methods
429//! that can look like:
430//!
431//! ```
432//! #[async_trait::async_trait]
433//! impl rpc::trib_storage_server::TribStorage for StorageServer {
434//!     async fn set(&self, request: tonic::Request<rpc::KeyValue>,
435//!     ) -> Result<tonic::Response<rpc::Bool>, tonic::Status> {
436//!         todo!();
437//!     }
438//! }
439//! ```
440//!
441//! Much better!
442//!
443//! ## Playing with your implementation
444//!
445//! To do some simple testing with your own implementation, you can use the
446//! `kv-client` and `kv-server` command line utilities.
447//!
448//! First make sure your code compiles, then run the server.
449//!
450//! ```
451//! $ cargo run --bin kv-server
452//! ```
453//!
454//! You should see an address print out (e.g. `127.0.0.1:7799`). If desired, you
455//! can override this setting with a command line flag.
456//!
457//! Now you can play with your server via the `kv-client` program. For example:
458//!
459//! ```
460//! $ cargo run --bin kv-client
461//! > set foo value
462//! Ok(true)
463//! > get foo
464//! Ok(Some("value"))
465//! > keys fo
466//! Ok(List(["foo"]))
467//! > list-get hello
468//! Ok(List([]))
469//! > list-get foo
470//! Ok(List([]))
471//! > list-append foo something
472//! Ok(true)
473//! > list-get foo
474//! Ok(List(["something"]))
475//! > clock
476//! Ok(0)
477//! > clock
478//! Ok(1)
479//! > clock
480//! Ok(2)
481//! > clock 200
482//! Ok(200)
483//! ```
484//!
485//! Both `kv-client` and `kv-server` have options which allow you to specify the
486//! host address.
487//!
488//! ## Requirements
489//!
490//! - When the network and storage are errorless, RPC to your server should
491//!   never return an error.
492//! - When the network has an error (like the back-end server crashed, and thus
493//!   the client cannot connect), your RPC client should return an error.  As
494//!   soon as the server is back up and running, your RPC client should act as
495//!   normal again (without needing to create a new client).
496//! - When the server and the clients are running on the lab machines, your RPC
497//!   should introduce less than 100 milliseconds of additional latency.
498//!
499//! ## Turning In Your Code
500//!
501//! Instructions for turning in the assignment are provided in the lab
502//! repository.
503//!
504//! ## Happy Lab 1!
505//!
506mod lab;
507pub use crate::lab1::lab::new_client;
508pub use crate::lab1::lab::serve_back;