From: Ludwig Schwiedrzik Date: Tue, 25 Jun 2024 14:59:22 +0000 (+0200) Subject: Add documentation for persistent storage grpc API X-Git-Url: https://gerrit.automotivelinux.org/gerrit/gitweb?a=commitdiff_plain;h=f483ae953bafa435a8dc4fff44aa13f23c397794;p=AGL%2Fdocumentation.git Add documentation for persistent storage grpc API This change adds documentation for the recently merged AGL Persistent Storage API (Gerrit Change 29996). Bug-AGL: [SPEC-5187] Change-Id: Ic9743c95885b2d60b3728f58568a7e27aefe6a59 Signed-off-by: Ludwig Schwiedrzik Reviewed-on: https://gerrit.automotivelinux.org/gerrit/c/AGL/documentation/+/30013 Reviewed-by: Walt Miner Reviewed-by: Jan-Simon Moeller Tested-by: Jan-Simon Moeller --- 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 index 0000000..97ec0ac --- /dev/null +++ b/docs/06_Component_Documentation/12_AGL_Persistent_Storage_API.md @@ -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" } + +```