Add documentation for persistent storage grpc API
[AGL/documentation.git] / docs / 06_Component_Documentation / 12_AGL_Persistent_Storage_API.md
diff --git a/docs/06_Component_Documentation/12_AGL_Persistent_Storage_API.md b/docs/06_Component_Documentation/12_AGL_Persistent_Storage_API.md
new file mode 100644 (file)
index 0000000..97ec0ac
--- /dev/null
@@ -0,0 +1,225 @@
+# Persistent Storage API for the Automotive Grade Linux demo
+
+The [AGL Persistent Storage API](https://github.com/LSchwiedrzik/agl-persistent-storage-api)
+is a grpc API for [AGL](https://www.automotivelinux.org/) 
+that serves as persistent storage API for the demo. The API is written 
+in Rust and makes use of [tonic](https://crates.io/crates/tonic-build) for grpc
+functionality as well as [RocksDB](https://rocksdb.org/) as a database backend,
+using [rust-rocksdb](https://crates.io/crates/rust-rocksdb). Use cases include
+retaining settings over a system shutdown (e.g. audio, HVAC, profile data, Wifi
+settings, radio presets, metric vs imperial units).
+
+The most important hardware consideration for this project is that the AGL demo
+runs on embedded hardware with flash storage, so we want to minimize number of
+write operations. This impacts the choice of database; we have chosen to work
+with RocksDB as it is well-suited for embedded computing and tunable with
+respect to write amplification. In principle the API is flexible with
+respect to database used (pluggable backends), but only RocksDB is implemented. 
+This API is part of the AGL demo as of release 'Royal Ricefish'.
+
+The AGL Persistent Storage API is constructed using a layered architecture:
+
+- Controller layer: translates proto calls to service calls.
+- Service layer: communicates with the controller and facade layers, implements
+  the business logic
+- Facade layer: implements RocksDB.
+
+## API Specification
+
+**Namespaces**
+The rpcs described below interact with keys belonging to specific namespaces. This feature enables applications to maintain private namespaces within the same database. Not specifying a namespace when calling the API will result in the default namespace "" being used. Alternatively, a specific namespace (e.g. "AppName") can be chosen. With the exception of DestroyDB, which acts on the entire database, all rpcs can only interact with one namespace at a time.
+
+- `DestroyDB() -> StandardResponse(success: boolean, message: string)`
+
+  - Consumer wants to destroy the entire database.
+
+    ```text
+    DestroyDB() -> //destroys entire database.
+    ```
+
+- `Write(key: string, value: string, namespace: string) -> StandardResponse(success: boolean, message: string)`
+
+  - Consumer wants to save *key* + *value* to a given *namespace* (default is ""), (e.g.
+    'Vehicle.Infotainment.Radio.CurrentStation':'hr5').
+  - This overwrites existing *value* under *key*.
+  - An empty string cannot be used as a *key*.
+
+    ```text
+    Write('Vehicle.Infotainment.Radio.CurrentStation':'wdr 4') -> Response
+
+    Write('Vehicle.Infotainment':'yes') -> Response
+
+    Write('test':'1') -> Response
+
+    Write('':'test') -> Error
+
+    Write(key: 'Private.Info', value: 'test', namespace: 'AppName') -> Response
+    ```
+
+- `Read(key: string, namespace: string) -> ReadResponse(success: boolean, message: string, value: string)`
+
+  - Consumer wants to read *value* of existing *key* in a given *namespace* (default is ""), e.g.
+    'Vehicle.Infotainment.Radio.CurrentStation':
+
+    ```text
+    Read('Vehicle.Infotainment.Radio.CurrentStation') -> 'wdr 4'
+
+    Read('Vehicle.doesNotExist') -> ERROR
+
+    Read(key: 'Private.Info', namespace: 'AppName') -> 'test'
+    ```
+
+- `Delete(key: string, namespace: string) -> StandardResponse(success: boolean, message: string)`
+
+  - Consumer wants to delete an existing *key* + *value* from a given *namespace* (default is ""), e.g.
+    'Vehicle.Infotainment.Radio.CurrentStation':
+
+    ```text
+    Delete('Vehicle.Infotainment.Radio.CurrentStation') -> Response
+
+    Delete('Vehicle.doesNotExist') -> ERROR
+
+    Delete(key: 'Private.Info', namespace: 'AppName') -> Response
+    ```
+
+- `Search(key: string, namespace: string) -> ListResponse(success: boolean, message: string, keys: repeated string)`
+
+  - Consumer wants to list all keys that contain *key* in a given *namespace* (default is ""), e.g. 'Radio'
+
+    ```text
+    Search('Radio') -> ('Vehicle.Infotainment.Radio.CurrentStation', 'Vehicle.Communication.Radio.Volume')
+
+    Search('Info') -> ('Vehicle.Infotainment.Radio.CurrentStation', 'Vehicle.Infotainment.Radio.Volume', 'Vehicle.Infotainment.HVAC.OutdoorTemperature')
+
+    Search('nt.Rad') -> ('Vehicle.Infotainment.Radio.CurrentStation')
+
+    Search('') -> ('Vehicle.Infotainment.Radio.CurrentStation', 'Vehicle.Infotainment.Radio.Volume', 'Vehicle.Infotainment.HVAC.OutdoorTemperature', 'Vehicle.Communication.Radio.Volume')
+
+    Search(key: '', namespace: 'AppName') -> ('Private.Info')
+    ```
+
+- `DeleteNodes(key: string, namespace: string) -> StandardResponse(success: boolean, message: string)`
+
+  - Consumer wants to delete all keys located in the subtree with root *key*, within the given *namespace* (default is ""), e.g.
+    'Vehicle.Infotainment'
+  - `key = ''` returns `ERROR`
+  - This rpc assumes that keys follow a VSS-like tree structure.
+
+    ```text
+    DeleteNodes('Vehicle.Infotainment') -> Response //deletes ('Vehicle.Infotainment', 'Vehicle.Infotainment.Radio.CurrentStation', 'Vehicle.Infotainment.Radio.Volume', 'Vehicle.Infotainment.HVAC.OutdoorTemperature')
+
+    DeleteNodes('Vehicle') -> Response //deletes ('Vehicle.Infotainment', 'Vehicle.Infotainment.Radio.CurrentStation', 'Vehicle.Infotainment.Radio.Volume', 'Vehicle.Infotainment.HVAC.OutdoorTemperature', 'Vehicle.Communication.Radio.Volume')
+
+    DeleteNodes('') -> ERROR
+
+    DeleteNodes('DoesNotExist') -> ERROR
+
+    DeleteNodes('Vehic') -> ERROR
+
+    DeleteNodes(key: 'Private', namespace: 'AppName') -> Response //deletes ('Private.Info')
+    ```
+
+- `ListNodes(node: string, layers: optional int, namespace: string) -> ListResponse(boolean, message, repeated string keys)`
+
+  - Consumer wants to list all nodes located in the subtree with root *node* exactly *layers*
+    layers deep, within the given *namespace* (default is "") , e.g. 'Vehicle.Infotainment'
+
+    - `layers = 0` lists all keys that start in *node* any number of *layers* deep
+    - `layers` default value is 1
+    - `node = ''` returns top-level root node(s)
+    - This rpc assumes that keys follow a VSS-like tree structure.
+
+    ```text
+    ListNodes('Vehicle.Infotainment', 1) -> ('Vehicle.Infotainment.Radio', 'Vehicle.Infotainment.HVAC')
+
+    ListNodes('Vehicle.Infotainment', 2) -> ('Vehicle.Infotainment.Radio.CurrentStation', 'Vehicle.Infotainment.Radio.Volume', 'Vehicle.Infotainment.HVAC.OutdoorTemperature')
+
+    ListNodes('Vehicle', 0) -> ('Vehicle.Infotainment.Radio.CurrentStation', 'Vehicle.Infotainment.Radio.Volume', 'Vehicle.Infotainment.HVAC.OutdoorTemperature', 'Vehicle.Communication.Radio.Volume', 'Vehicle.Infotainment')
+
+    ListNodes('', 0) -> ('Vehicle.Infotainment.Radio.CurrentStation', 'Vehicle.Infotainment.Radio.Volume', 'Vehicle.Infotainment.HVAC.OutdoorTemperature', 'Vehicle.Communication.Radio.Volume', 'Vehicle.Infotainment', 'test')
+
+    ListNodes('Vehicle.Infotainment') -> ('Vehicle.Infotainment.Radio', 'Vehicle.Infotainment.HVAC')
+
+    ListNodes('', 1) -> ('Vehicle', 'test')
+
+    ListNodes('Vehicle.Infotainment.Radio.Volume', 1) -> ()
+
+    ListNodes('Vehicle', -1) -> ERROR
+
+    ListNodes('Vehicle.DoesNotExist', 1) -> ERROR
+
+    ListNodes(key: 'Private', namespace: 'AppName') -> ('Private.Info')
+
+    For empty data base:
+    ListNodes('', 1) -> ()
+    ```
+
+## Example Tree
+
+Note: nodes marked by \* are keys (and therefore have a value)
+
+**Namespace: ""**
+- Vehicle
+  - Infotainment \*
+    - Radio
+      - CurrentStation \*
+      - Volume \*
+    - HVAC
+      - OutdoorTemperature \*
+  - Communication
+    - Radio
+      - Volume \*
+- test \*
+
+**Namespace: "AppName"**
+- Private
+  - Info \*
+
+## Setup instructions
+
+1. Install rust
+
+2. Download or install protobuf (e.g. from
+   [here](https://github.com/protocolbuffers/protobuf/releases)) and set the
+   `PROTOC` environment variable:
+   `echo -n "export PROTOC=/path/to/protoc.exe" >> ~/.bashrc`
+   
+3. Build application
+
+   ```bash
+   cargo build
+   ```
+
+4. Run tests
+
+   ```bash
+   cargo test
+   ```
+
+5. Start server
+
+   ```bash
+   cargo run --release --bin server
+   ```
+
+## rpc Usage
+
+Insomnia usage for manual testing is describd in
+https://konghq.com/blog/engineering/building-grpc-apis-with-rust
+
+```text
+DestroyDB: {}
+
+Write: { "key": "foo", "value": "foobar", "namespace": "bar" }
+
+Read: { "key": "foo", "namespace": "bar" }
+
+Delete: { "key": "foo", "namespace": "bar" }
+
+Search: { "key": "foo", "namespace": "bar" }
+
+DeleteNodes: { "key": "foo", "namespace": "bar" }
+
+ListNodes: { "key": "foo", "layers": 1, "namespace": "bar" }
+
+```