Skip to content

Latest commit

 

History

History
379 lines (277 loc) · 10.9 KB

02-key-value-store.md

File metadata and controls

379 lines (277 loc) · 10.9 KB

Using a Key Value Store with Spin

In this module, we'll use a Key Value store to increment a counter on each request. As Spin loads a new instance of the component on each request, components have no shared memory in-between requests. Therefore we can use the Key Value store in Spin to persist data.

Note This document assumes you have followed the previous step and have a working Spin application.

Note The section Quick Reference for this Section, at the end of this page, contains all the commands and code needed to complete this module, for a quick reference.

Implement a Counter

The Spin SDK provides an easy way to use a Key Value store. Depending on the programming language you've chosen, please refer to the Quick Reference for this Section to see the complete sample code. In this section we'll be using TypeScript as an example.

First we need to open a store, then check if a counter exists, and finally either set it, or increment it.

import Kv from "@fermyon/spin-sdk"

const store = Kv.open("default");

When opening a store, we can provide the name of the store. This is because you can use multiple stores within your application. If we use a store named default (or the method Kv.openDefault()), we will automatically get a local sqlite implementation in our development environment, and don't have to configure what the implementation of the store is. We'll get back to this later.

Now, let's check for a counter key, and either increment or set it, and then return the counter in the body.

import { HandleRequest, HttpRequest, HttpResponse, Kv } from "@fermyon/spin-sdk"
// Let's add an interface
interface Counter {
    count: number;
}

export const handleRequest: HandleRequest = async function(request: HttpRequest): Promise<HttpResponse> {

    // Open the KV Store named "default"
    const store = Kv.open("default");

    // Get the key "counter" or create a new instance of the interface and set count to 0
    let counter: Counter = store.getJson("counter") ?? { count: 0 };
    // Increment the counter
    counter.count++;
    // Store the instance of the counter
    store.setJson("counter", counter);

    return {
        status: 200,
        headers: { "content-type": "text/plain" },
        // Let's return the counter in the body
        body: JSON.stringify(counter)
    }
}

Now we need to allow the Spin component to access the Key Value store. In the spin.toml file, add the name of the store to the stores which will be made available for the Spin component:

[component.mycomponent]
source = "target/wasm32-wasi/release/mycomponent.wasm"
key_value_stores = ["default"]

Build and test the Spin application:

spin build --up

curl localhost:3000

You should now see the counter increment on each request.

Note When using the "default" store locally, Spin stores the value i a sqlite file in the .spin directory in the root of the app directory. If you need to reset the KV store, simply delete the sqlite_key_value.db file in that folder.

That's it, we now have a Spin application with the ability to persist state in a Key Value store.

Using Redis for Key Value

Moving our Spin application to any other environment, we probably want a better solution than a sqlite file to store our data. Spin provides the ability to map the Key Value store to an external provider - e.g., Redis and Azure Cosmos DB.

In order to do so, we can provide a Runtime Configuration to our Spin application.

Let's go ahead and use a Redis container as the provider for a Key Value store in Spin.

Let's start by running a Redis container instance, by running the following command:

# This will run the Redis image from RedisLabs, and expose Redis on port 6379 on your host.
docker run -d -p 6379:6379 --name my-redis redis

Next, let's provide the Runtime Configuration in a .toml file - e.g., runtime-config.toml

# We provide the name of the Key Value store in the declaration. In this case `default`
[key_value_store.default]
# The type of the KV provider: "redis", "azure_cosmos", or "spin" (sqlite)
type = "redis"
# The following depends on the provider chose, for "redis", this is the redis endpoint
url = "redis://localhost"

Once we have this file save, we can now run our Spin application again, providing the runtime configuration:

$ spin up --runtime-config-file runtime-config.toml

Hit the endpoint, and let's observe the keys being written in the Redis database:

$ curl localhost:3000

# Start the Redis CLI in the container
$ docker exec -it my-redis redis-cli
127.0.0.1:6379> GET counter

That's it! We now have a stateful Spin application, backed by Redis.

Code samples for this Section

The below sections contains code samples for this section.

Using TypeScript

import { HandleRequest, HttpRequest, HttpResponse, Kv } from "@fermyon/spin-sdk"

interface Counter {
    count: number;
}

export const handleRequest: HandleRequest = async function(request: HttpRequest): Promise<HttpResponse> {

    const store = Kv.open("default");

    let counter: Counter = store.getJson("counter") ?? { count: 0 };
    counter.count++;
    store.setJson("counter", counter);

    return {
        status: 200,
        headers: { "content-type": "text/plain" },
        body: JSON.stringify(counter)
    }
}

Using Rust

lib.rs

use spin_sdk::http::{IntoResponse, Request, Response};
use spin_sdk::http_component;
use spin_sdk::key_value;
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct Counter {
    count: usize
}

/// A simple Spin HTTP component.
#[http_component]
fn handle_kv(_req: Request) -> anyhow::Result<impl IntoResponse> {
    
    let store = key_value::Store::open("redis")?;

    let count: Counter = match store.get_json::<Counter>("counter").unwrap() {
        Some(c) => {
            Counter {
                count: c.count + 1
            }
        },
        None => {
            Counter {
                count: 1
            }
        },
    };

    store.set_json::<Counter>("counter", &count).unwrap();

    Ok(Response::builder()
        .status(200)
        .header("content-type", "text/plain")
        .body(serde_json::to_string_pretty::<Counter>(&count).unwrap())
        .build())
}

Cargo.toml

[package]
name = "kv"
authors = ["Mikkel Mørk Hegnhøj <[email protected]>"]
description = ""
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
anyhow = "1"
spin-sdk = "3.0.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

[workspace]

Using Go

Source: https://github.com/rajatjindal/spin-kv-counter-go/tree/main

// Content of main.go
package main

import (
	"encoding/json"
	"fmt"
	"net/http"

	spinhttp "github.com/fermyon/spin-go-sdk/http"
	kv "github.com/fermyon/spin-go-sdk/kv"
)

type Counter struct {
	Count int `json:"count"`
}

func init() {
	spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) {
		store, err := kv.OpenStore("default")
		if err != nil {
			http.Error(w, "failed to open store", http.StatusInternalServerError)
			return
		}

		counter, err := getJson[Counter](store, "counter")
		if err != nil {
			http.Error(w, "failed to get counter from kv", http.StatusInternalServerError)
			return
		}

		counter.Count += 1

		updatedValue, err := setJson(store, "counter", counter)
		if err != nil {
			http.Error(w, "failed to get counter from kv", http.StatusInternalServerError)
			return
		}

		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write(updatedValue)
	})
}

func main() {}

func getJson[T any](store *kv.Store, key string) (T, error) {
	var val T
	exists, err := store.Exists("counter")
	if err != nil {
		return val, err
	}

	if !exists {
		return val, nil
	}

	value, err := store.Get(key)
	if err != nil {
		return val, fmt.Errorf("failed to get value for key %q from kv store", key)
	}

	err = json.Unmarshal(value, &val)
	if err != nil {
		return val, fmt.Errorf("failed to unmarshal into struct Counter")
	}

	return val, nil
}

func setJson[T any](store *kv.Store, key string, value T) ([]byte, error) {
	updatedValue, err := json.Marshal(value)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal updated value into raw bytes")
	}

	err = store.Set(key, updatedValue)
	if err != nil {
		return nil, fmt.Errorf("failed to update value in kv")
	}

	return updatedValue, nil
}

go.mod

module github.com/go

go 1.20

require github.com/fermyon/spin-go-sdk v0.0.0-20240220234050-48ddef7a2617

require github.com/julienschmidt/httprouter v1.3.0

Using Python

Source: https://github.com/rajatjindal/spin-kv-counter-py/tree/main

# Content of app.py
from spin_sdk import http, key_value
from spin_sdk.http import IncomingHandler, Request, Response
import json

class Counter:
    def __init__(self, count):
        self.count = count
    
def asCounter(dct):
    return Counter(dct['count'])

class IncomingHandler(IncomingHandler):
    def handle_request(self, request: Request) -> Response:
        store = key_value.open_default()
        
        raw = store.get("counter")
        if raw is not None:
            counter: Counter = asCounter(json.loads(store.get("counter")))
        else:
            counter = Counter(0)
        
        counter.count += 1
        
        store.set("counter", bytes(json.dumps(counter.__dict__), "utf-8"))
        
        return Response(
            200,
            {"content-type": "application/json"},
            bytes(json.dumps(counter.__dict__), "utf-8")
        )

Learning Summary

In this section you learned how to:

  • Use the Key Value store feature in Spin
  • Use Runtime configuration to change the provider of the Key Value store

Navigation

Let us know what you think in this short Survey.