Join the Shiny Community every month at Shiny Gatherings

Rust Api on Posit Connect Blog

How to Deploy a Rust API to Posit Connect


No matter your programming skill level – you can become a versatile developer or familiar with extending open source by familiarizing yourself with a few software engineering concepts and design patterns. In this post, I will present how I went about understanding the Plumber code base, and show how you can extend it to deploy a Rust API to Posit Connect.

TOC:


The Problem: Posit Connect and APIs

I am a big fan of Rust. I have been using it to build data intensive applications for a while now, mostly using Actix

For a recent project I wanted to use an API to interact with the data in a fast and type-safe manner, however the requirements of the project were such that I had to deploy the entirety of the application (a Shiny app front end and the Rust API) to a Posit Connect instance. This is where things got a bit tricky. 

Posit Connect only has official support for Plumber APIs which are written in R and some Python frameworks making it near-impossible to deploy an API written in other languages and frameworks without some work.

Software Engineering Approach: Design Patterns and Concepts

Software engineering is all about breaking down problems and understanding the common patterns and abstractions that are used to solve them. Here I will show you how I went about breaking down the deployment of a Rust API to Posit Connect.

Posit Connect and HTTP Servers

Posit Connect offers much more than a data app (Shiny apps, APIs, etc) deployment platform. However, for the purposes of this post, we will focus on the app deployment part.

How Does Posit Connect Serve Apps?

In short, it’s a reverse proxy that forwards authenticated requests to an HTTP server running on the same machine. For APIs in R and Python, Posit Connect takes care of starting the HTTP server correctly, however for other languages and frameworks we will have to do this ourselves.

Posit Connect and Plumber APIs Written in R

So, how exactly does Posit Connect start the HTTP server for a Plumber API written in R? In order to answer this question we will have to take a look at the Plumber code base. 

Every Plumber API has an entry point which is just an R script that contains code that looks like this:


library(plumber)
# 'plumber.R' is the location of the file shown above
pr("plumber.R") |>
  pr_run(port=8000)

This code is pretty simple; it loads the Plumber library and then starts the HTTP server on port 8000. This must be pretty similar to how Posit Connect starts the HTTP server for a Plumber API, so let’s take a look at the Plumber code base to see if we can find the code that does this.

Exploring the Plumber Code Base

Let’s clone the Plumber Github repo and immediately do a search for the definition of the pr_run function.

‘pr_run’ Function Definition

We can see that the pr_run function is defined in the R/pr.R file. Let’s take a look at the definition of the function.


pr_run <- function(pr,
                   host = '127.0.0.1',
                   port = getOption('plumber.port', NULL),
                   ...,
                   debug = missing_arg(),
                   docs = missing_arg(),
                   swaggerCallback = missing_arg(),
                   quiet = FALSE
) {
  validate_pr(pr)
  ellipsis::check_dots_empty()
  pr$run(host = host,
         port = port,
         debug = debug,
         docs = docs,
         swaggerCallback = swaggerCallback,
         quiet = quiet)
}

This function is pretty simple, it just calls the run method on some pr object. If we take a look back at our entry point, we see that the pr object is created by calling the pr function. So, let’s take a look at the definition of the pr function.

‘pr’ Function Definition


pr <- function(file = NULL,
               filters = defaultPlumberFilters,
               envir = new.env(parent = .GlobalEnv)) {
  Plumber$new(file = file, filters = filters, envir = envir)
}

We are getting closer! 

The pr object appears to be an instance of the Plumber class. Now, let’s look for the Plumber class definition.

‘Plumber’ Class Definition

The Plumber class has a very long definition, however we can see that it inherits from the Hookable class. So we need to take note of that.


Plumber <- R6Class(
  "Plumber",
  inherit = Hookable,
  public = list(
    ...
  )
)

Since we know that the logic necessary for starting our HTTP server is somewhere in this class or maybe in one of its parent classes, let’s look for a typical variable used when starting an HTTP server, such as port.

Finding the Variable for Starting the HTTP Server

After some quick digging, we find the run method which takes in a port argument. Lo and behold, we find that it calls a runServer function

We found it!


run = function(
  host = '127.0.0.1',
  port = getOption('plumber.port', NULL),
  swagger = deprecated(),
  debug = missing_arg(),
  swaggerCallback = missing_arg(),
  ...,
  # any new args should go below `...`
  docs = missing_arg(),
  quiet = FALSE
) {
  ...
  httpuv::runServer(host, port, self)
}

At this point we have everything we need to trick Posit Connect into thinking that our Rust API is a Plumber API. We just need to create a mock Plumber class that has a run method.

The Solution to Deploy Rust on Posit Connect

Now that we know what we need to do, we can start writing some code. I started by creating a mock Plumber class that had a run method that started the Rust HTTP server on the specified port. 

In order for this to work, I compiled the Rust code into a binary in the same operating system that Posit Connect runs on and then bundled it with the R project. 

I then set up some command line arguments that would allow me to specify the host and port that the Rust HTTP server should run on. I also added some code that would make the binary executable. 

The code for the mock Plumber class is shown below:


run = function(
  host = '127.0.0.1',
  port = getOption('plumber.port', NULL),
  swagger = deprecated(),
  debug = missing_arg(),
  swaggerCallback = missing_arg(),
  ...,
  # any new args should go below `...`
  docs = missing_arg(),
  quiet = FALSE
) {
  command <- paste0("./fetching_api --host=", host, " --port=", port)
  Sys.setenv("RUST_LOG" = "info")
  # Necessary only because the bundled binary is not executable
  system("chmod +x fetching_api")
  system(command)
}

Mock Plumber API Error on Posit Connect

I then tried to deploy the API to Posit Connect and.. it didn’t work. 

I got an error:


2023/07/18 10:37:54 AM: Starting R with process ID: '3129349'
2023/07/18 10:37:54 AM: Error in plumb(dir = getwd()) :
2023/07/18 10:37:54 AM: 'entrypoint.R' must return a runnable Plumber router.
2023/07/18 10:37:54 AM: Plumber API exiting ...

As we can see, Posit Connect is not happy with our mock Plumber class. Since R is an interpreted language, in theory there should be no difference between our mock Plumber class. Perhaps Posit Connect is making use of other methods in the Plumber class. 

Because Posit Connect is not open source, we can’t really know for sure. However, we can try to make our mock Plumber class more similar to the real Plumber class.

Mock ‘Hookable’ Class

So, I added a new mock Hookable class that the mock Plumber class inherits from. Then I added empty methods for all of the methods in the real Plumber class.

Then I tried to deploy the API once again and.. it worked! I was able to deploy the API to Posit Connect and make requests to it.


2023/08/07 3:45:26 AM: Starting R with process ID: '755944'
2023/08/07 3:45:26 AM: [2023-08-07T08:45:26Z INFO actix_server::builder] starting 2 workers
2023/08/07 3:45:26 AM: [2023-08-07T08:45:26Z INFO actix_server::server] Actix runtime found; starting in Actix runtime

Rust API connect deployed to Posit Connect and requests

Now we can take advantage of all of the features that Posit Connect has to offer, such as API versioning, authentication and more. We can also use the Posit Connect UI to monitor the API and view logs.

Using Software Engineering Concepts to Deploy Rust API on Posit Connect

The main takeaway from this post is that understanding software engineering concepts and design patterns can be very useful when trying to solve problems. 

In this case, we were able to solve the problem of deploying an unsupported API by understanding how reverse proxy servers work and identifying typical design patterns used in starting HTTP servers. Ultimately, this allowed us to logically traverse and understand the Plumber code base. 

Even if you are a data scientist without formal development training, understanding these concepts can be very useful in order to become a versatile developer. Of course, if you still need assistance, we are here to help. Reach out and make the most out of your deployment experience.