summaryrefslogtreecommitdiffstats
path: root/docs/06_Component_Documentation/12_AGL_Persistent_Storage_API.md
blob: 97ec0ac128ea64922e05c4ce3b15f37117095bd5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
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" }

```