Turbolift 🚡

  1. crates.io version
  2. documentation
  3. github (ci check)
  4. github (last commit)

Turbolift is a toy distribution interface for Rust programs. It provides a simple proof‑of‑concept for distributing programs over a Kubernetes cluster.

How It Works

With Turbolift, the programmer tags specific functions to be converted automatically into services, which are distributed, and handles communication between the main program (which is not distributed) and each generated service. Distribution can be activated with a feature defined in the project's Cargo.toml (cargo doc, example).

Example

Here is an example of a program that distributes a function as a Kubernetes service using Turbolift:

#[macro_use]
extern crate lazy_static;
use tokio::sync::Mutex;

use turbolift::kubernetes::K8s;
use turbolift::on;

/// Instantiate the global cluster
/// manager. We provide a function
/// that allows the cluster manager
/// to make images available to the
/// cluster. With `5`, we set each
/// service to autoscale from 1 to 5
/// replicas depending on traffic.
lazy_static! {
  static ref K8S: Mutex<K8s> =
    Mutex::new(
      K8s::new(
        Box::new(load_container),
        5
      )
    );
}

/// Takes a container built in
/// local Docker and makes the image
/// available to the cluster. This
/// usually means uploading the
/// image to a private registry.
/// Input is the image tag in local
/// Docker. Output is the image
/// tag that should be used in the
/// cluster, or an error.
///
/// In this example, we're uploading
/// to a local repo via kind.
fn load_container(tag: String) ->
      anyhow::Result<String> {
  std::process::Command::new("kind")
    .args(
      format!(
        "load docker-image {}",
        tag
      )
      .as_str()
      .split(' '))
      .status()?;
   Ok(tag)
}

/// This function is distributed
/// using the `on` macro.
#[on(K8S)]
fn square(u: i32) -> i32 {
  u * u
}

fn main() {
  let input: i32 = rand::random();
  let mut rt =
    tokio::runtime::Runtime::new()
      .unwrap();
  let output = rt
    .block_on(square(input))
    .unwrap();
  println!(
    "input: {:?}\noutput: {:?}",
    input, output
  );
}

When applied to a function with the output type T, Turbolift causes the function to return a future of Result<T, turbolift::DistributionError>.

There are no additional configuration files; the orchestration is part of the code. There isn't a lot of configuration at all– Turbolift doesn't provide the customizability of a more complex distribution interface. But, the exciting thing about Turbolift is that, for programs that do fit the pattern, the code change to enable distribution is extremely minor.

Lifecycle

When a programmer writes a program that uses Turbolift, they declare a global state manager, and tag functions to be distributed as services using a macro.

At compile time, these macros perform two important tasks. First, the macros extract the necessary dependencies to run the function and rewrite the function as an http service. The source code for these derived services is then stored in the program binary, to be accessed at runtime. Second, the macros rewrite the function in the main program to instantiate and then call the newly generated service, using the global state manager.

At runtime, the source code for each derived service is transformed into a format readable by the cluster manager. For Kubernetes, this means putting the source code into a Docker image, and requesting the necessary pod, deployment, service, autoscaler, and ingress.

When the main program completes and Turbolift's global state is dropped, cleanup code is executed to remove the distributed services for the specific session. This means that if your code panics or your network fails, your services may not be deleted. Make sure to avoid panics in your code, and to clean out old services from your cluster if you experience network problems. Images are not cleaned up by Turbolift.

Each time Turbolift is executed, a new version of each service is generated with a unique name, to avoid collisions in case multiple programs are distributing functions with the same name on the same cluster at the same time.

Kubernetes

Features

Turbolift sets up the following while targeting Kubernetes:

Design

Turbolift extracts the code for a given function, transforms it into a simple server program with the relevant probes, and generates a generic microservice with the following architecture for each distributed function. These are the Kubernetes components generated by Turbolift:

Turbolift requires the user to:

Requirements

The Kubernetes implementation assumes that ingresses can be generated and that they will not be exposed to public traffic. Here are the requirements and technologies used in the CI:

Setup

Turbolift itself can be installed using Cargo and work in any environment that meets the requirements. If you want to set up a local cluster that can work with turbolift, see the install guide for a turbolift-compatible raspberry pi cluster. If you have a turbolift-ready cluster and want to adapt a current project, see this pr to use turbolift in an existing project.