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
- Software Engineering Approach: Design Patterns and Concepts
- Posit Connect and Plumber APIs Written in R
- The Solution to Deploy Rust on Posit Connect
- Using Software Engineering Concepts to Deploy Rust API on Posit Connect
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
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.