Persistent Storage in Webassembly Applications

This article is about implementing persistent storage in WebAssembly applications. More specifically, how Fermyon’s Spin framework provides a mechanism that allows WebAssembly executables to access different levels of on-disk persistence. WebAssembly is the next wave of cloud computing and persistent storage is arguably the cornerstone of any useful application, so let’s take a look at how we can implement both.

Persistent Storage

An application needs a mechanism that can store and load information each time the application’s business logic is executed. The application’s information can be anything from a game’s high score, to the entire inventory of a company. The point is, at the end of the day, an application that can safely and efficiently execute business logic whilst also storing the state of the business is a useful one.

In the context of storage and the web, it is important to note that the Web Storage API provides mechanisms by which a single web browser can store key/value pairs. This is useful for local individual client storage (analogous to cookie functionality).

In the context of persistent storage in Wasm applications, what we are trying to achieve here is to implement persistent storage across an entire Wasm application. For example, provide each client with access to the application’s datastore.

The TL;DR is that permanent storage is not possible by default with Wasm. Let’s quickly unpack why that is, and then provide a solution for you. First, a quick word about Wasm.

WebAssembly

We know that WebAssembly (Wasm) is a binary instruction format for a stack-based Virtual Machine (VM). We also know that the Wasm VM is ephemeral. For example, a Wasm VM is instantiated, and business logic is performed, at which point the VM instance swiftly disappears. In this deliberate design, which has many benefits including performance and more, there is no storage mechanism by default.

WebAssembly also boasts, by design, a security benefit. For example, the aforementioned Wasm VM executes the business logic in a sandboxed environment which means there is no contact between the VM and the host’s file system or network sockets etc. Again, deliberately by default but, fundamentally no persistent storage. So how do we implement both Wasm and persistent storage?

To unpack this, let’s start with a brief overview of Spin.

Spin

Spin

Spin is a framework for building and running event-driven microservice applications with Wasm components. Spin, is essentially a developer tool for creating serverless Wasm applications. Spin ships with a variety of SDKs, as well as an array of ready-made application templates, to get you started. So how do we use Spin?

Firstly, it is always preferable to follow Spin’s official documentation. We have done so in this article, so please feel free to follow along (for demonstration purposes) and remember, if in doubt “read the docs”.

Installing Spin

If you haven’t already, go ahead and install Rust.

There are ready-made executable binarys available (for Windows, macOS and Linux) in Spin’s quickstart guide if you are interested. However, for presentation purposes, we will show you how to clone and install Spin using Cargo:

cd ~
git clone https://github.com/fermyon/spin -b v0.5.0
cd spin
rustup target add wasm32-wasi
cargo install --locked --path .

Spin can use a Rust HTTP component (which compiles to a Wasm component) that interacts with Redis. Let’s go ahead and install Redis to make this happen.

Redis

Firstly, like Spin, Redis is an open-source project. Redis is an in-memory but persistent on-disk database that uses a simple command structure. This means that users have instant and reliable access to data and are not required to learn structured query languages of traditional databases.

Installing Redis

In this article, for presentation purposes, we will install Redis from source. However, please note there are also alternative installation instructions for Windows, macOS and Linux.

When installing from source, we first fetch and unpack Redis’ latest stable version:

cd ~
wget https://download.redis.io/redis-stable.tar.gz
tar -xzvf redis-stable.tar.gz

Once downloaded, we can install Redis:

cd ~
cd redis-stable
make
make install

On macOS (which is what we are using for this presentation), the above procedure will add the Redis binary executables in the /usr/local/bin directory (which should already be in the system’s path). You may need to alter your system’s path; depending on which installation procedure you use and which operating system you are on.

Once installed, we can go ahead and start Redis; using the following command:

redis-server 

The above command will produce output similar to the following:

30253:C 29 Sep 2022 17:35:44.597 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
30253:C 29 Sep 2022 17:35:44.597 # Redis version=7.0.5, bits=64, commit=00000000, modified=0, pid=30253, just started
30253:C 29 Sep 2022 17:35:44.597 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
30253:M 29 Sep 2022 17:35:44.598 * Increased maximum number of open files to 10032 (it was originally set to 2560).
30253:M 29 Sep 2022 17:35:44.598 * monotonic clock: POSIX clock_gettime
                _._                                                  
           _.-``__ ''-._                                             
      _.-``    `.  `_.  ''-._           Redis 7.0.5 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._                                  
 (    '      ,       .-`  | `,    )     Running in standalone mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 6379
 |    `-._   `._    /     _.-'    |     PID: 30253
  `-._    `-._  `-./  _.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |           https://redis.io       
  `-._    `-._`-.__.-'_.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |                                  
  `-._    `-._`-.__.-'_.-'    _.-'                                   
      `-._    `-.__.-'    _.-'                                       
          `-._        _.-'                                           
              `-.__.-'                                               

30253:M 29 Sep 2022 17:35:44.599 # WARNING: The TCP backlog setting of 511 cannot be enforced because kern.ipc.somaxconn is set to the lower value of 128.
30253:M 29 Sep 2022 17:35:44.599 # Server initialized
30253:M 29 Sep 2022 17:35:44.599 * Ready to accept connections

Keep the above Redis Server tab/terminal open, we will be back to use it a bit later on.

Once Redis is running on our system we can explore the Redis Command Line Interface (CLI).

Redis CLI

The Redis CLI provides a myriad of options to interact with Redis. Let’s explore a few to get acquainted. Firstly, open a new terminal and run the following command to start the Redis CLI:

redis-cli

The above command will provide a prompt; similar to the following:

127.0.0.1:6379>

The following section shows how we can interact with Redis via the CLI, please go ahead and try these examples if you are new to Redis:

# Set a value 
set num 0
# Get num - returns "0"
get num
# Increment num by 1
incr num
# Increment num by 21
incrby num 21
# Get num - returns 22
get num
# Delete num
del num
# Get num - returns (nil)
get num

Keep the above Redis CLI tab/terminal open, we will be back to use it a bit later on.

Now that we have Spin and Redis working, let’s create a Spin application.

Creating a Spin Application

In this section, we are going to demonstrate how to create a Redis message hander application; using the aforementioned Spin application templates.

Firstly, we list the Spin application templates (which are available by default):

spin templates list
+---------------------------------------------------+
| Name         Description                          |
+===================================================+
| http-go      HTTP request handler using (Tiny)Go  |
| http-rust    HTTP request handler using Rust      |
| redis-go     Redis message handler using (Tiny)Go |
| redis-rust   Redis message handler using Rust     |
+---------------------------------------------------+

In the event that you do not have these templates available, you can go ahead and install them using the following command:

spin templates install --git https://github.com/fermyon/spin

From here we can create our new application; using the following command:

cd ~
spin new redis-rust redisTriggerExample

The above command will ask a series of questions, see the example below where we provided Project description, Redis address and Redis channel:

Project description: A Redis Trigger example
Redis address: redis://localhost:6379
Redis channel: channelOne

Once created, you can start editing the pre-made application’s source code. For this example, let’s open the /src/lib.rs file and make the Redis listener println! everytime a message is published on the channelOne Redis channel. To do this we open the lib.rs file and create the print statement, as shown below:

vi src/lib.rs
#[redis_component]
fn on_message(msg: Bytes) -> Result<()> {
    println!("{}", from_utf8(&msg)?);
    Ok(())
}

Once the source code is changed, we can build and start the application; using the following commands:

cargo build --target wasm32-wasi --release
spin up --file spin.toml --follow-all

You will recall that we already have Redis Server and CLI running in separate terminals, from previous steps above. If you are curious, you can check the status of Redis by using ps -ef | grep -i redis and you can start a fresh Redis server and/or CLI process in their own separate terminals by typing redis-server and/or redis-cli again (if needed).

Ok, so here comes the fun part. It is now time to publish a message on the Redis channel called channelOne, we do so by running the following command in our Redis CLI terminal:

127.0.0.1:6379> publish channelOne "Hello There!"

The resulting output is that our Spin application prints Hello There! in the terminal; as expected:

Hello There!

This is super exciting because now we know that we can implement business logic inside a function that is executed each time a message is published on a Redis channel!

#[redis_component]
fn on_message(msg: Bytes) -> Result<()> {
    // Implement your custom business logic to execute when this listener hears each message
    Ok(())
}

This is a great segway for the next section, which dives into how you could tie a bunch of different Wasm components together to not only listen and execute business logic but to store and retrieve persistent data on an ongoing basis.

Many Components

To present multiple components within a single Spin application we can do the following.

First, we can create a new example Spin application called spin-hello-http:

spin new http-rust spin-hello-http

Then we can create a few new directories within the application’s spin-hello-http directory, like this:

cd spin-hello-http
mkdir rock
mkdir paper
mkdir scissors

We can then recreate the src directory inside each of these new directories, for this presentation, I am just copying the whole src directory into each of these new areas:

cp -rp src rock/
cp -rp src paper/
cp -rp src scissors/

The spin-hello-http/src directory is no longer needed after this point (because we have rock/src, paper/src & scissors/src).

Now, we can modify each of the src/lib.rs files as follows:

vi src/rock/lib.rs
/// A simple Spin HTTP component.
#[http_component]
fn spin_hello_http(req: Request) -> Result<Response> {
    println!("{:?}", req.headers());
    Ok(http::Response::builder()
        .status(200)
        .header("foo", "bar")
        .body(Some("Rock".into()))?)
}
vi src/paper/lib.rs
/// A simple Spin HTTP component.
#[http_component]
fn spin_hello_http(req: Request) -> Result<Response> {
    println!("{:?}", req.headers());
    Ok(http::Response::builder()
        .status(200)
        .header("foo", "bar")
        .body(Some("Paper".into()))?)
}
vi src/scissors/lib.rs
/// A simple Spin HTTP component.
#[http_component]
fn spin_hello_http(req: Request) -> Result<Response> {
    println!("{:?}", req.headers());
    Ok(http::Response::builder()
        .status(200)
        .header("foo", "bar")
        .body(Some("Scissors".into()))?)
}

Similarly, we can just copy the Cargo.toml file into the rock, paper and scissors directories:

cp -rp Cargo.toml rock/
cp -rp Cargo.toml paper/
cp -rp Cargo.toml scissors/

The spin-hello-http/Cargo.toml file is no longer needed after this point (because we have rock/Cargo.toml, paper/Cargo.toml & scissors/Cargo.toml).

We then update the name and description (in each new Cargo.toml location) to suit i.e. for scissors/Cargo.toml we do:

[package]
name = "scissors"
authors = ["tpmccallum <tim.mccallum@fermyon.com>"]
description = "A Scissors Application"
version = "0.1.0"
edition = "2021"

The last step to configuring the many components is to update the applications only spin.toml file, as follows:

vi ~/spin-hello-http/spin.toml
[[component]]
id = "rock"
source = "rock/target/wasm32-wasi/release/rock.wasm"
[component.trigger]
route = "/rock"
[component.build]
command = "cargo build --target wasm32-wasi --release"

[[component]]
id = "paper"
source = "paper/target/wasm32-wasi/release/paper.wasm"
[component.trigger]
route = "/paper"
[component.build]
command = "cargo build --target wasm32-wasi --release"

[[component]]
id = "scissors"
source = "scissors/target/wasm32-wasi/release/scissors.wasm"
[component.trigger]
route = "/scissors"
[component.build]
command = "cargo build --target wasm32-wasi --release"

Note, we have an id, source & route; which are all rock/paper/scissors themed.

To build each component we run the cargo build --target wasm32-wasi --release command from within each of the rock, paper and scissors directories.

To start the application we run spin up --follow-all from the top level directory:

cd ~/spin-hello-http
Serving http://127.0.0.1:3000
Available Routes:
  rock: http://127.0.0.1:3000/rock
  paper: http://127.0.0.1:3000/paper
  scissors: http://127.0.0.1:3000/scissors

If we go ahead and visit each of these endpoints, we will get the appropriate response, for example:

curl http://127.0.0.1:3000/rock
# returns "Rock"
curl http://127.0.0.1:3000/paper
# returns "Paper"
curl http://127.0.0.1:3000/scissors
# returns "Scissors"

This is also super exciting, in addition to Redis listener functionality in Spin, we now have many public-facing endpoints; each of which can execute its custom business logic when called.

It is also important to point out, at this stage, that you are not solely bound to using Rust as the source for each of your component .wasm files. Simply put, once you have the application built as shown above you could feasibly go ahead and create your Wasm binary executables using any language which compiles to the wasm32-wasi target. For example you could use the Grain programming language to write your business logic and then compile and simply place the resulting .wasm file in the appropriate Spin application location.

The above examples should be enough to start the creative juices flowing. We can see that there is a great deal of flexibility to create a WebAssembly web-based application. Now for the pièce de résistance; persistent storage.

Persistent Storage

As promised at the start of this article, we will now show you how Spin can maintain a persistent application-wide datastore. A great way to demonstrate this is to update our original redisTriggerExample from above. First, we change into the redisTriggerExample application directory:

cd ~/redisTriggerExample

Then, we open the spin.toml file and fill it with the component section with the following:

[[component]]
environment = { REDIS_ADDRESS = "redis://127.0.0.1:6379", REDIS_CHANNEL = "channelOne" }
id = "redis-trigger-example"
source = "target/wasm32-wasi/release/redis_trigger_example.wasm"
[component.trigger]
route = "/publish"
[component.build]
command = "cargo build --target wasm32-wasi --release"

In addition, to the above, we need to go ahead and find the trigger line:

trigger = { type = "redis", address = "redis://localhost:6379" }

Then replace that entire line with the following trigger line:

trigger = { type = "http", base = "/" }

Next, we open the Cargo.toml file and replace the entire dependencies section, with the following:

[dependencies]
# Useful crate to handle errors.
anyhow = "1"
# Crate to simplify working with bytes.
bytes = "1"
# General-purpose crate with common HTTP types.
http = "0.2"
# The Spin SDK.
spin-sdk = { git = "https://github.com/fermyon/spin", tag = "v0.5.0" }
# Crate that generates Rust Wasm bindings from a WebAssembly interface.
wit-bindgen-rust = { git = "https://github.com/bytecodealliance/wit-bindgen", rev = "cb871cfa1ee460b51eb1d144b175b9aab9c50aba" }

Then finally, we open the src/lib.rs file and fill the entire file with the following:

use anyhow::{anyhow, Result};
use spin_sdk::{
    http::{internal_server_error, Request, Response},
    http_component, redis,
};

// Specifying address and channel that component will publish to
const REDIS_ADDRESS_ENV: &str = "REDIS_ADDRESS";
const REDIS_CHANNEL_ENV: &str = "REDIS_CHANNEL";

#[http_component]
fn publish(_req: Request) -> Result<Response> {
    let address = std::env::var(REDIS_ADDRESS_ENV)?;
    let channel = std::env::var(REDIS_CHANNEL_ENV)?;

    // Get the message to publish from the Redis key "mykey"
    let payload = redis::get(&address, "mykey").map_err(|_| anyhow!("Error querying Redis"))?;

    // Set the Redis key "spin-example" to value "Eureka!"
    redis::set(&address, "spin-example", &b"Eureka!"[..])
        .map_err(|_| anyhow!("Error executing Redis set command"))?;

    // Set the Redis key "int-key" to value 0
    redis::set(&address, "int-key", format!("{:x}", 0).as_bytes())
        .map_err(|_| anyhow!("Error executing Redis set command"))?;
    // Increase by 1
    let int_value = redis::incr(&address, "int-key")
        .map_err(|_| anyhow!("Error executing Redis incr command",))?;
    assert_eq!(int_value, 1);

    // Publish to Redis
    match redis::publish(&address, &channel, &payload) {
        Ok(()) => Ok(http::Response::builder().status(200).body(None)?),
        Err(_e) => internal_server_error(),
    }
}

Note the use of spin_sdk::redis::get and spin_sdk::redis::publish? Remember those from the CLI examples at the start of this article? :)

Now that we have updated the redisTriggerExample application, let’s go ahead and build it, using the following command:

spin build

Once built, we run the application, using the following command:

spin up

The above command will produce an output similar to the following:

spin up   
Serving http://127.0.0.1:3000
Available Routes:
  redis-trigger-example: http://127.0.0.1:3000/publish

If we visit the http://127.0.0.1:3000/publish endpoint, then naturally the code in the src/lib.rs function will execute. Specifically, the Spin application will, amongst other things, set the Redis key spin-example to value Eureka!.

Let’s go ahead and check that this worked. To confirm, we simply go to our Redis CLI terminal and type the following:

get spin-example

The above command correctly returns the value Eureka!.

Confirming Persistence

As per normal Redis operation, if at any point we want to take a snapshot of the state of our data, the Redis CLI’s save command produces a point-in-time snapshot of the data inside our Redis instance and saves that snapshot to the dump.rdb file. Executing the save is as simple as the one-word command, shown below:

127.0.0.1:6379> save
OK

If we go ahead and execute the save command we can reboot the entire system and all of the data from our application will persist.


This amount of flexibility i.e. separate components, listeners, web endpoints and permanent storage certainly makes for some brilliant microservices applications. Do you have any ideas about what you would like to see built? Do you have any questions about anything in this article? If so please reach out to us via Discord; see link below.

I sincerely hope you have enjoyed reading this article and hope to hear from you soon.

Discord

We have a great Discord presence. Please join us in Discord and ask questions and share your experiences with Fermyon products like Spin.

Thanks for reading!

Interested in learning more?

Talk to us