Persistent Storage in Webassembly Applications
Tim McCallum
spin
redis
wasm
webassembly
cloud
rust
microservices
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 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
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
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!