July 26, 2023

One Small Step For Slats, One Giant Leap For Cloud Computing

Sohan Maheshwar Sohan Maheshwar

cloud finicky whiskers custom domains NoOps DB

One Small Step For Slats, One Giant Leap For Cloud Computing

A little over a year ago, Fermyon launched Finicky Whiskers featuring Slats the cat - a game that was designed to showcase why we’re excited about WebAssembly as the next wave of cloud computing.

Since then, Fermyon has launched the Open Beta of Fermyon Cloud, enabling you to go from a blinking cursor to a deployed serverless app in 66 seconds, and with a host of features such as a Key-Value store and a Growth Plan tier for higher limits.

Finicky Whiskers

And all this while Slats has been busy at work, gobbling up 140K pieces of food (yes, we checked).

Today, we are proud to introduce yet another set of features that makes Fermyon Cloud the easiest way to deploy and manage WebAssembly apps, namely - custom domains and a SQLite Database. We’ve used these features to release a major update to Finicky Whiskers to showcase how the Fermyon Cloud offers all the capabilities you need to run your applications.

Finicky Whiskers

Finicky Whiskers

Back at the Open Source Summit in Austin in 2022, we introduced Slats the Cat, who can never decide on what to eat. Sometimes she craves fish, other times veggies, then chicken, and then she’s back to fish again. This is the premise of the game Finicky Whiskers. With this new feature update, we have a major update to our game of feline fickleness.

Custom Domains

A domain is a user-friendly way of referring to the address a website accesses on the internet. For example, the domain you’re reading this on is fermyon.com. Earlier, Fermyon Cloud users could apply custom subdomains to their Spin applications in this format: finickywhiskers.fermyon.app. Today we’re launching the ability to assign a custom domain to your Spin Application. Custom domains allow you to make your sites accessible at your own address, i.e. finickywhiskers.com. All top-level domains are supported for your custom domain. Do note that you have to first own this domain name before hosting a Spin app on it.

Setting up finickywhiskers.com

How it works

You can bring your custom domain, purchased through a domain registrar of your choice, to Fermyon Cloud in a few easy steps. For example we want to port Finicky Whiskers from finickywhiskers.fermyon.app to finickywhiskers.com.

Getting started

  1. Navigate to the Fermyon Cloud Dashboard and select “Manage Domains” on your Spin application that hosts the app.

  2. You can see the default subdomain applied to the Spin application. To add the custom domain, select the “Add custom domain” button.

  3. Add the new custom domain and hit the ‘Save’ button.

  4. Fermyon Cloud automatically generates 4 name server records to update with your domain registrar. Add these name server records to your domain registrar to ensure traffic for finickywhiskers.com is directed to the Fermyon-managed DNS. Instructions on how to do this will vary by domain registrar.

  5. Test your verified domain

Setting up finickywhiskers.com

The process is complete once you receive a green check from Fermyon Cloud. This signifies the domain has been successfully verified, and Fermyon Cloud has generated a Let’s Encrypt certificate on your behalf. The application is now hosted on finickywhiskers.com

Read our Announcing Custom Domains blog post for more information.


Slats Stats

Turns out people love playing Finicky Whiskers and are surprisingly competitive about their high scores. Just like old-school arcade games where the machine would remember your high score forever (at least until it was unplugged 😉) we added an update to the game where you can update a leaderboard if you get a high score. We implemented this update to the game via our new feature, a SQLite database for your application.

Finicky Whiskers High Scores

SQLite Database

We’re excited to announce support for a SQLite Database in Fermyon Cloud is now live and in private beta! In Spin 1.4, we announced support for the Spin SQLite interface, which provides developers with an interface to persist data in a SQLite database fully managed by Spin. We wanted to bring the same experience Spin developers enjoy locally to the Fermyon Cloud.

Serverless workloads often need to store state in-between application invocations, and Spin applications are no exception to this rule. Workloads ranging from Extract Transfer and Load (ETL) pipelines to full-stack workloads benefit from being able to quickly store and retrieve data from a relational store. With a SQLite DB in Fermyon Cloud, developers don’t need to worry about database configuration. Instead, they can use the Spin SDK in their programming language of choice to store and retrieve data from a database that’s provisioned and managed by Fermyon Cloud on their behalf.

We have partnered with Turso and will be using their edge database service, as the backing infrastructure for Fermyon Cloud’s SQLite Database. Turso is built using libSQl, an OSS fork of SQLite.

Let’s take a look at how we added a persistent leaderboard to Finicky Whiskers using a SQLite database in the Fermyon Cloud.

Getting Started

The SQLite Database using SQLite has been built in a manner that allows you to seamlessly push your application to Fermyon Cloud, giving you the same experience of developing locally on Spin.

By default, Spin components do not have access to the SQLite database. Developers must grant access to specific Spin components with the following line in the application manifest. This is the only line required to get started with the SQLite database:

[component]
sqlite_databases = ["default"]

The Spin manifest file contains multiple components, including a component to persist high scores in a database. Here’s a snippet from the spin.toml file:

spin_manifest_version = "1"
name = "finicky-whiskers"
version = "1.1.0"
trigger = { type = "http", base = "/" }

# Other components excluded for brevity

# Stores highscores in redis
[[component]]
id = "highscore"
source = "components/highscore.wasm"
sqlite_databases = ["default"]
[component.trigger]
route = "/highscore"
[component.build]
workdir = "highscore"
command = "make"
watch = ["src/**/*.rs", "Cargo.toml"]

The highscores component is granted access to the default SQLite database. Now that we have our application manifest set-up, it is time to dive into the source code.

The highscore function processed the incoming score from a round of Finicky Whiskers. This high score is then passed along to the check_highscore function which handles the logic related to checking and updating high scores based on the incoming request. Now here’s where the SQLite database plays an important part. The get_highscore function retrieves the Top 10 highscores from the database. If the latest round does indeed make it to the Top 10 highest scores, the replace_highscore function inserts/replaces an entry in the database:


#[http_component]
fn highscore(req: Request) -> Result<Response> {
    let res_body: String = match *req.method() {
        Method::GET => {
            serde_json::to_string_pretty(&get_highscore().unwrap()).unwrap()
        }
        Method::POST => check_highscore(req).unwrap_or_else(|_| "".to_string()),
        _ => "".to_string(),
    };

    let mut status = 200;

    if res_body.is_empty() {
        status = 405;
    }

    Ok(http::Response::builder()
        .status(status)
        .body(Some(res_body.into()))?)
}

fn check_highscore(req: Request) -> Result<String> {
    println!("Incoming body: {:?}", req.body());

    // Parsing incoming request to HighScore
    let incoming_score: HighScore = match req.body() {
        Some(b) => serde_json::from_slice(b)?,
        None => panic!("Failed to parse the incoming request"),
    };

    // Inserting the highscore into the database
    replace_highscore(&incoming_score)?;

    // Fetching the highscores from store (JsonBin)
    let highscores = match get_highscore() {
        Ok(highscores) => highscores,
        Err(e) => panic!("Tried to get high score: {}", Error::msg(e.to_string())),
    };

    // Check if the incoming score made the high score list
    let incoming_score_pos = highscores
        .iter()
        .position(|s| s.ulid.unwrap() == incoming_score.ulid.unwrap());

    let rank = match incoming_score_pos {
        Some(r) => {
            println!("It is a high score at {}", r + 1);
            r + 1
        },
        None => {
            println!("It is not a high score");
            delete_highscore(incoming_score.ulid.unwrap())?;
            0
        },
    };

    // Setting up response
    let response = HighScoreResult {
        is_high_score: rank > 0,
        rank,
        high_score_table: highscores,
    };

    let res_body = serde_json::to_string_pretty(&response)?;

    Ok(res_body)
}

fn get_highscore() -> Result<Vec<HighScore>> {
    let conn = spin_sdk::sqlite::Connection::open_default()?;
    let query = "SELECT ulid, score, username FROM highscore ORDER BY score DESC LIMIT 10";
    let result = conn.execute(query, &[])?;
    let highscores = result.rows().map(HighScore::from).collect::<Vec<_>>();
    Ok(highscores)
}

fn replace_highscore(highscore: &HighScore) -> Result<()> {
    let conn = spin_sdk::sqlite::Connection::open_default()?;
    let query = "REPLACE INTO highscore (ulid, score, username) VALUES (?, ?, ?)";

    let ulid = highscore.ulid.expect("ulid is required").to_string();
    let params = &[
        ValueParam::Text(&ulid),
        ValueParam::Integer(highscore.score as i64),
        ValueParam::Text(&highscore.username)
    ];
    conn.execute(query, params)?;
    Ok(())
}

fn delete_highscore(ulid: Ulid) -> Result<()> {
    let conn = spin_sdk::sqlite::Connection::open_default()?;
    let query = "DELETE FROM highscore WHERE ulid = ?";
    let ulid = ulid.to_string();
    let params = &[ValueParam::Text(&ulid)];
    conn.execute(query, params)?;
    Ok(())
}

#[derive(Deserialize, Serialize)]
struct HighScore {
    score: i32,
    username: String,
    ulid: Option<Ulid>,
}

impl From<Row<'_>> for HighScore {
    fn from(row: Row<'_>) -> Self {
        let uscore = row.get::<u32>("score")
            .expect("column 'score' not found in row");
        let username = row.get::<&str>("username")
            .expect("column 'username' not found in row");
        let ulid = row.get::<&str>("ulid")
            .expect("column 'ulid' not found in row");
        HighScore {
            score: i32::try_from(uscore).expect("failed to convert score to an i32"),
            username: username.to_string(),
            ulid: ulid.parse::<Ulid>().ok(),
        }
    }
}

#[derive(Deserialize, Serialize)]
struct HighScoreResult {
    is_high_score: bool,
    rank: usize,
    high_score_table: Vec<HighScore>,
}

Let’s do a deep-dive into the get_highscore() function to see what’s happening under the hood:

fn get_highscore() -> Result<Vec<HighScore>> {
    let conn = spin_sdk::sqlite::Connection::open_default()?;
    let query = "SELECT ulid, score, username FROM highscore ORDER BY score DESC LIMIT 10";
    let result = conn.execute(query, &[])?;
    let highscores = result.rows().map(HighScore::from).collect::<Vec<_>>();
    Ok(highscores)
}
  • The spin_sdk::sqlite::Connection::open_default() command creates a new database connection.
  • Once this connection is established, you can query using typical SQL commands such as SELECT, FROM, DELETE and more. Here we query the database for the Top 10 scores along with the username and ulid - a unique identifier.
  • The conn.execute line executes the SQL query against the database using the conn connection and returns a Result<ResultSet> containing the query result or an error.
  • Finally, result.rows() returns an iterator over the rows in the result set and map(HighScore::from) maps each row to a HighScore struct using the From trait implementation for HighScore.

We’ll test our Spin application’s behavior locally by running the following command:

$ spin build --up
Building component highscore with `cargo build --target wasm32-wasi --release`
   Compiling highscore v0.1.0 (/Users/sohanm/fermyon/finickywhiskers)

Managing SQLite Databases on Fermyon Cloud

Once the application has been validated, we can migrate it to the Fermyon Cloud with one easy step:

$ spin cloud deploy

Just like that, we have a Spin application using SQLite Database with SQLite on Fermyon Cloud. Using this command, you can list all the databases associated with your Spin applications. The Spin Cloud plugin generates random names for your databases:

$ spin cloud sql list
inspirational-dog (default) (currently used by "finicky-whiskers")

For more info about the SQLite on Fermyon Cloud - including an example of a To-Do app - read the Announcing SQLite Databases blog post.


The whole ball of yarn

We believe Fermyon Cloud is the easiest way to deploy and manage WebAssembly apps. Launching custom domains and the SQLite DB enables developers to build light production workloads in the cloud. Don’t believe us? Play the new and improved Finicky Whiskers and see for yourself.

Play Finicky Whiskers now and beat our high score!

 


Read more about today’s announcements:

Announcing Custom Domains in Fermyon Cloud Announcing SQLite Databases in Fermyon Cloud Announcing the Spin Up Hub Press Release

 

 

 


🔥 Recommended Posts


Quickstart Your Serveless Apps with Spin

Get Started