Assignment 4 - Add Dapr state management
October 16, 2024 · View on GitHub
Assignment goals
To complete this assignment, you must reach the following goals:
- The TrafficControl service saves the state of a vehicle (
VehicleStateclass) using the state management building block after vehicle entry. - The TrafficControl service reads and updates the state of a vehicle using the state management building block after vehicle exit.
This assignment targets number 3 in the end-state setup:
Step 1: Use the Dapr state management building block
First, you need to add something to the state management configuration file:
-
Open the file
dapr/components/statestore.yamlin VS Code. -
Add a
scopessection to the configuration file that specifies that only the TrafficControlService should use the state management building block:apiVersion: dapr.io/v1alpha1 kind: Component metadata: name: statestore spec: type: state.redis version: v1 metadata: - name: redisHost value: localhost:6379 - name: redisPassword value: "" - name: actorStateStore value: "true" scopes: - trafficcontrolservice
Now you will add code to the TrafficControlService so it uses the Dapr state management building block to store vehicle state:
-
Open the file
TrafficControlService/traffic_control/__init__.pyin VS Code. -
Inspect the code in the
entrycamandexitcammethods of this controller. The methods refer to arepositorywhich is defined earlier in the file:repository = repositories.VehicleStateRepository() -
Open the file
TrafficControlService/traffic_control/repositories.pyin VS Code.This is the repository used by the TrafficControlService. Inspect the code of this repository. As you can see, this repository uses a very simple in-memory key/value map to store the state. The license number of the vehicle is used as the key. You are going to replace this implementation with one that uses Dapr state management.
-
Replace the contents of the repositories file with the following code:
import requests from . import models class VehicleStateRepository: def __init__(self): self.state = {} def get_vehicle_state(self, license_number: str) -> models.VehicleState or None: pass def set_vehicle_state(self, vehicle_state: models.VehicleState) -> None: pass -
Replace the contents of the
set_vehicle_statemethod with the following contents:state_entry = [ dict(key=vehicle_state.license_number, value=vehicle_state.json()), ] requests.post("http://localhost:3600/v1.0/state/statestore/", json=state_entry)This may look a little cryptic. So let's take a look at what we're doing here. We create a new list of state entries to save. The first entry in the list is a dictionary containing two entries, key, and value. The value is the data we want to save in the statestore. The key identifies the data that we're storing.
Note that you can save multiple state entries with a single request and that the state entry value is a JSON-serialized version of the state information we want to store.
-
The URL for getting data using the Dapr state API is:
http://localhost:<daprPort>/v1.0/state/<statestore-name>/<key>. You'll use this API to retrieve the VehicleState. Replace the implementation of theget_vehicle_statemethod with the following code:response = requests.get(f"http://localhost:3600/v1.0/state/statestore/{license_number}") return models.VehicleState.parse_raw(response.json())
The repository code should now look like this:
import requests
from . import models
class VehicleStateRepository:
def __init__(self):
self.state = {}
def get_vehicle_state(self, license_number: str) -> models.VehicleState or None:
response = requests.get(f"http://localhost:3600/v1.0/state/statestore/{license_number}")
return models.VehicleState.parse_raw(response.json())
def set_vehicle_state(self, vehicle_state: models.VehicleState) -> None:
data = [
dict(key=vehicle_state.license_number, value=vehicle_state.json()),
]
requests.post("http://localhost:3600/v1.0/state/statestore/", json=data)
Now you're ready to test the application.
Step 2a: Test the application
-
Make sure no services from previous tests are running (close the terminal windows)
-
Make sure all the Docker containers introduced in the previous assignments are running (you can use the
Infrastructure/start-all.shscript to start them). -
Open the terminal window in VS Code and make sure the current folder is
VehicleRegistrationService. -
Enter the following command to run the VehicleRegistrationService with a Dapr sidecar:
dapr run --app-id vehicleregistrationservice --app-port 6002 --dapr-http-port 3602 --dapr-grpc-port 60002 --resources-path ../dapr/components -- uvicorn vehicle_registration:app --port 6002 -
Open a new terminal window in VS Code and change the current folder to
FineCollectionService. -
Enter the following command to run the FineCollectionService with a Dapr sidecar:
dapr run --app-id finecollectionservice --app-port 6001 --dapr-http-port 3601 --dapr-grpc-port 60001 --resources-path ../dapr/components -- uvicorn fine_collection:app --port 6001 -
Open a new terminal window in VS Code and change the current folder to
TrafficControlService. -
Enter the following command to run the TrafficControlService with a Dapr sidecar:
dapr run --app-id trafficcontrolservice --app-port 6000 --dapr-http-port 3600 --dapr-grpc-port 60000 --resources-path ../dapr/components -- uvicorn traffic_control:app --port 6000 -
Open a new terminal window in VS Code and change the current folder to
Simulation. -
Start the simulation:
python simulation
You should see similar logging as before.
Step 2b: Verify the state-store
Obviously, the behavior of the application is exactly the same as before. But are the VehicleState entries actually
stored in the default Redis state-store? To check this, you will use the redis CLI inside the dapr_redis container
that is used as state-store in the default Dapr installation.
-
Open a new terminal window in VS Code.
-
Execute the following command to start the redis-cli inside the running
dapr_rediscontainer:docker exec -it dapr_redis redis-cli -
In the redis-cli enter the following command to get the list of keys of items stored in the redis cache:
keys *You should see a list of entries with keys in the form
"trafficcontrolservice||<license-number>". -
Enter the following command in the redis-cli to get the data stored with this key (change the license-number to one in the list you see):
hgetall trafficcontrolservice||KL-495-J -
You should see something similar to this:
❯ docker exec -it dapr_redis redis-cli 127.0.0.1:6379> keys * 1) "trafficcontrolservice||18-RSS-4" 2) "trafficcontrolservice||84-GJ-06" 3) "trafficcontrolservice||KJ-HS-06" 4) "trafficcontrolservice||JN-TH-23" 5) "trafficcontrolservice||11-GT-84" 6) "trafficcontrolservice||RN-KR-35" 7) "trafficcontrolservice||10-HYD-5" 8) "trafficcontrolservice||YT-66-PY" 9) "trafficcontrolservice||ND-841-Y" 10) "trafficcontrolservice||T-375-NF" 127.0.0.1:6379> hgetall trafficcontrolservice||ND-841-Y 1) "version" 2) "1" 3) "data" 4) "{\"licenseNumber\":\"ND-841-Y\",\"entryTimestamp\":\"2021-09-15T11:19:18.1781609+02:00\",\"exitTimestamp\":\"0001-01-01T00:00:00\"}"
As you can see, the data is actually stored in the redis cache. The cool thing about Dapr is that the state management building block supports different state-stores through its component model. So without changing any code but only specifying a different Dapr component configuration, you could use an entirely different storage mechanism.
If you're up for it, try to swap-out Redis with another state provider. See the the list of available stores in the Dapr documentation). To configure a different state-store, you need to change the file
dapr/components/statestore.yaml.
Step 3: Use Dapr state management with the Dapr SDK for Python
In this step you're going to change the VehicleStateRepository class and replace calling the Dapr state management
API directly over HTTP with using the DaprClient from the Dapr SDK for Python.
-
Open the file
TrafficControlService/traffic_control/repositories.pyin VS Code. -
Add an import statement to the top of the file for the Dapr client:
from dapr.clients import DaprClient -
Replace the the code in the
set_vehicle_statewith the following code:with DaprClient() as client: client.save_state("statestore", vehicle_state.license_number, vehicle_state.json()) -
Replace the implementation of the
get_vehicle_statemethod with the following code:with DaprClient() as client: return models.VehicleState.parse_raw(client.get_state("statestore", license_number).text())
The repository code should now look like this:
import requests
from . import models
from dapr.clients import DaprClient
class VehicleStateRepository:
def __init__(self):
self.state = {}
def get_vehicle_state(self, license_number: str) -> models.VehicleState or None:
with DaprClient() as client:
return models.VehicleState.parse_raw(client.get_state("statestore", license_number).text())
def set_vehicle_state(self, vehicle_state: models.VehicleState) -> None:
with DaprClient() as client:
client.save_state("statestore", vehicle_state.license_number, vehicle_state.json())
Now you're ready to test the application. Just repeat steps 2a and 2b.
Next assignment
Make sure you stop all running processes and close all the terminal windows in VS Code before proceeding to the next assignment.
Go to assignment 5.