How-To: Build a stateful service

Use state management with a scaled, replicated service

In this article, you’ll learn how to create a stateful service which can be horizontally scaled, using opt-in concurrency and consistency models. Consuming the state management API frees developers from difficult state coordination, conflict resolution, and failure handling.

Set up a state store

A state store component represents a resource that Dapr uses to communicate with a database. For the purpose of this guide, we’ll use the default Redis state store.

Using the Dapr CLI

When you run dapr init in self-hosted mode, Dapr creates a default Redis statestore.yaml and runs a Redis state store on your local machine, located:

  • On Windows, under %UserProfile%\.dapr\components\statestore.yaml
  • On Linux/MacOS, under ~/.dapr/components/statestore.yaml

With the statestore.yaml component, you can easily swap out underlying components without application code changes.

See a list of supported state stores.

Kubernetes

See how to setup different state stores on Kubernetes.

Strong and eventual consistency

Using strong consistency, Dapr makes sure that the underlying state store:

  • Returns the response once the data has been written to all replicas.
  • Receives an ACK from a quorum before writing or deleting state.

For get requests, Dapr ensures the store returns the most up-to-date data consistently among replicas. The default is eventual consistency, unless specified otherwise in the request to the state API.

The following examples illustrate how to save, get, and delete state using strong consistency. The example is written in Python, but is applicable to any programming language.

Saving state

  1. import requests
  2. import json
  3. store_name = "redis-store" # name of the state store as specified in state store component yaml file
  4. dapr_state_url = "http://localhost:3500/v1.0/state/{}".format(store_name)
  5. stateReq = '[{ "key": "k1", "value": "Some Data", "options": { "consistency": "strong" }}]'
  6. response = requests.post(dapr_state_url, json=stateReq)

Getting state

  1. import requests
  2. import json
  3. store_name = "redis-store" # name of the state store as specified in state store component yaml file
  4. dapr_state_url = "http://localhost:3500/v1.0/state/{}".format(store_name)
  5. response = requests.get(dapr_state_url + "/key1", headers={"consistency":"strong"})
  6. print(response.headers['ETag'])

Deleting state

  1. import requests
  2. import json
  3. store_name = "redis-store" # name of the state store as specified in state store component yaml file
  4. dapr_state_url = "http://localhost:3500/v1.0/state/{}".format(store_name)
  5. response = requests.delete(dapr_state_url + "/key1", headers={"consistency":"strong"})

If the concurrency option hasn’t been specified, the default is last-write concurrency mode.

First-write-wins and last-write-wins

Dapr allows developers to opt-in for two common concurrency patterns when working with data stores:

  • First-write-wins: useful in situations where you have multiple instances of an application, all writing to the same key concurrently.
  • Last-write-wins: Default mode for Dapr.

Dapr uses version numbers to determine whether a specific key has been updated. You can:

  1. Retain the version number when reading the data for a key.
  2. Use the version number during updates such as writes and deletes.

If the version information has changed since the version number was retrieved, an error is thrown, requiring you to perform another read to get the latest version information and state.

Dapr utilizes ETags to determine the state’s version number. ETags are returned from state requests in an ETag header. Using ETags, your application knows that a resource has been updated since the last time they checked by erroring during an ETag mismatch.

The following example shows how to:

  • Get an ETag.
  • Use the ETag to save state.
  • Delete the state.

The following example is written in Python, but is applicable to any programming language.

  1. import requests
  2. import json
  3. store_name = "redis-store" # name of the state store as specified in state store component yaml file
  4. dapr_state_url = "http://localhost:3500/v1.0/state/{}".format(store_name)
  5. response = requests.get(dapr_state_url + "/key1", headers={"concurrency":"first-write"})
  6. etag = response.headers['ETag']
  7. newState = '[{ "key": "k1", "value": "New Data", "etag": {}, "options": { "concurrency": "first-write" }}]'.format(etag)
  8. requests.post(dapr_state_url, json=newState)
  9. response = requests.delete(dapr_state_url + "/key1", headers={"If-Match": "{}".format(etag)})

Handling version mismatch failures

In the following example, you’ll see how to retry a save state operation when the version has changed:

  1. import requests
  2. import json
  3. # This method saves the state and returns false if failed to save state
  4. def save_state(data):
  5. try:
  6. store_name = "redis-store" # name of the state store as specified in state store component yaml file
  7. dapr_state_url = "http://localhost:3500/v1.0/state/{}".format(store_name)
  8. response = requests.post(dapr_state_url, json=data)
  9. if response.status_code == 200:
  10. return True
  11. except:
  12. return False
  13. return False
  14. # This method gets the state and returns the response, with the ETag in the header -->
  15. def get_state(key):
  16. response = requests.get("http://localhost:3500/v1.0/state/<state_store_name>/{}".format(key), headers={"concurrency":"first-write"})
  17. return response
  18. # Exit when save state is successful. success will be False if there's an ETag mismatch -->
  19. success = False
  20. while success != True:
  21. response = get_state("key1")
  22. etag = response.headers['ETag']
  23. newState = '[{ "key": "key1", "value": "New Data", "etag": {}, "options": { "concurrency": "first-write" }}]'.format(etag)
  24. success = save_state(newState)

Last modified October 11, 2024: Fixed typo (#4389) (fe17926)