At the heart of the server side of the AnalysisPageServer system is a mapping from
handler functions to web services. You define the set of functions to
represent by building an AnalysisPageRegistry
, then deploy
the application either in Rook (running in your local R server)
or under Apache using the Rapache system.
Your handler functions are built into objects called AnalysisPage
s.
Each AnalysisPage
can be a service, which might return for example
some arbitrary JSON string, or it can be a fully interactive page,
for example making a plot and returning a data frame. This document
explains the simpler service-type AnalysisPage
. It
also explains how to deploy your application with Rook. Interactive
pages and servers are described on another page.
Rook version >= 1.1 and fork are required for the examples in this vignette. I am going to check if they are available.
## Rook >= 1.1 or fork is not available, so although you can still use the static reporting
## features of AnalysisPageServer, and deploy dynamic servers with FastRWeb and RApache, you
## will not be able to deploy using Rook/Rhttpd, or build this vignette properly. You'll see
## the message "Rook >= 1.1 + fork not available" at all the points in this vignette where
## Rook+fork would have been required: starting the server, querying the server and stopping
## the server. If you want you can install/update the missing dependencies like this:
##
## install.packages("fork")
## install.packages("devtools")
## library(devtools)
## install_github("Rook", "jeffreyhorner")
service-type AnalysisPage
s are built with the build.service()
constructor. All you have to do is pass a handler function to it and
give it a name:
library(AnalysisPageServer)
hello <- build.service(function() "Hello, world!",
name = "hello")
class(hello)
## [1] "AnalysisPage"
Note: Even though, as we will demonstrate in a moment, this is just a service and not a full-blown web page we still call it an
AnalysisPage
. Since, from the server's point of view, there is not much difference between a webservice and a web page, in this documentation I usually just say "page" when saying "page or service" would be more accurate.
Next we build an AnalysisPageRegistry
. This example only has one
AnalysisPage
, not too interesting, but we still need this object:
reg <- new.registry(hello)
class(reg)
## [1] "AnalysisPageRegistry"
An AnalysisPageRegistry
is not much more than a lookup for
AnalysisPage
s. You can get the names of all registered pages:
pages(reg, include.services = TRUE)
## [1] "hello"
The include.services
switch has to be turned on because the default
is to return only the interactive pages, and hello
is not interactive.
You can retrieve a particular page:
identical(get.page(reg, "hello"), hello)
## [1] TRUE
Once the registry object is built it can be deployed. In this documentation page we'll deploy all of our applications via Rook using the local R server. This is useful for development and informal purposes but for production scale an RApache or FastRWeb deployment is required. The combination of Rook+Rhttpd does not handle concurrency well and generally tends to crash a lot. Also, some versions of Rook will only listen on the local lookback device, which would prevent sharing your server with anyone else.
Note: Your choice of deployment is completely independent of how you build the
AnalysisPageRegistry
, which is the main focus of this documentation page.
Now we'll start the server:
port <- 3324 ## or some other port
server <- startRookAnalysisPageServer(reg, port = port)
server
"Rook >= 1.1 + fork not available"
Be aware that the function startRookAnalysisPageServer
starts the server in a new process. This seems slightly more stable than the typical
Rook deployment in the same process, where your continued computations might interfere
with incoming requests. In particular, if you try to call to the server from the
same R process it would hang. The object that this function returns can be
passed to kill.process()
to turn
off the server:
kill.process(server)
"Rook >= 1.1 + fork not available"
The downside of starting a new process is that you might forget about it when you quit R. Not only does this waste system resources, but it actually holds on to the port, which means you won't be able to start another server on that port. To protect against this, you should get in the habit of starting your servers with two lines of code:
server <- startRookAnalysisPageServer(reg, port = port)
on.exit(try(kill.process(server)))
The second line registers an expression to evalute when R exits, which ensures that the
children don't survive. Since the triggering of these on.exit
handlers is a bit tricky
in the knitr
environment
we'll make explicit calls to kill.process(server)
in this document.
If you are a Rook nerd and want to do fancier things, like combine this Rook app with other applications, then you can get the Rook App object like this:
app <- new.rook.analysis.page.app(reg)
With this object you also have the possibility of deploying your server in the normal
Rook way, which avoids using the fork
package.
rook.server <- Rook::Rhttpd$new()
rook.server$add(app, name = "RAPS")
rook.server$start(port = port)
## Ping the server from another R process or from your web browser ...
## Then stop the server
rook.server$stop()
"Rook >= 1.1 + fork not available"
We provide the startRookAnalysisPageServer()
function to start
your server, rather than interfacing directly with Rook::Rhttpd()
.
This function accepts an AnalysisPageRegistry
object, so you don't
have to build your Rook object, and also uses the fork
package to
start the server in a child process. This lets us call to the server
from the parent process, which would otherwise block. It also makes
it easy to recover from disaster by simply killing the child process
without having to restart the parent R process.
Let's turn the server back on and see how it works.
server <- startRookAnalysisPageServer(reg, port = port)
Then open your web browser:
Dissecting the URL: "127.0.0.1" is the loopback device that Rook uses---it means your local host. The port is an arbitrary choice for this demonstration page (sorry if the port from this screen shot does not match the port in this documentation). If you don't specify a port then Rook/R will choose one for you at random, and each time you try your server you will have to modify the port. The "/custom" prefix cannot be modified---that is how Rook+Rhttpd works. "RAPS" is what I've named the server within Rook. "R/analysis" is a prefix within the
AnalysisPageServer
system, which is the location from which all the pages are served. Finally, the name of the page is JSON-encoded to form the full request.
Once the server is running we could query it in theory from any language
and possibly even remotely. In this document we will demonstrate
the server is working by constructing the URL (using the service.link
function) and then calling readLines()
.
app.base.url <- paste0("http://127.0.0.1:", port, "/custom/RAPS")
url <- service.link(page = "hello", app.base.url = app.base.url)
cat(readLines(url, warn = FALSE), "\n")
## "Rook >= 1.1 + fork not available"
(warn = FALSE
is added to the readLines
call to suppress a warning
message about incompete final lines.)
That's "Hello, World!".
Astute observers will note that the greeting string was quoted in the response, even though it was
displayed with cat
rather than just normal R output. The reason for this is
that all responses are automatically JSON-encoded. More on this later, and also how to turn it off.
kill.process(server)
"Rook >= 1.1 + fork not available"
Let's put two pages in the same application. We'll re-use the hello
page but also have a page which calculates the sine of an argument:
sine <- build.service(function(theta) sin(theta),
name = "sine")
reg <- new.registry(sine, hello)
server <- startRookAnalysisPageServer(reg, port = port)
As mentioned above, all of the return values are by default
JSON-encoded. We can use the fromJSON
function in R to turn them
back into R objects. For demonstration purposes within this document
we'll wrap up the steps of constructing the URL, calling the server,
and decoding the response into a function:
show.analysis <- function(page, params = list(), show.url = FALSE) {
url <- service.link(page, params, app.base.url)
if(show.url) cat(url, "\n")
response <- readLines(url, warn = FALSE)
rjson::fromJSON(response)
}
show.analysis("hello", show.url = TRUE)
## http://127.0.0.1:3324/custom/RAPS/R/analysis?page=%22hello%22
## [1] "Rook >= 1.1 + fork not available"
show.analysis("sine", list(theta = 3.141592653), show.url = TRUE)
## http://127.0.0.1:3324/custom/RAPS/R/analysis?page=%22sine%22&theta=3%2E141592653
## [1] "Rook >= 1.1 + fork not available"
kill.process(server)
"Rook >= 1.1 + fork not available"
Whatever your function returns will be JSON encoded with toJSON
.
cx.return <- build.service(function() {
list(A = 1:5,
B = "Able was I",
C = list(x = 1, y = c(TRUE, FALSE, TRUE)))
}, name = "complex")
reg <- new.registry(cx.return)
server <- startRookAnalysisPageServer(reg, port = port)
show.analysis("complex")
## [1] "Rook >= 1.1 + fork not available"
kill.process(server)
"Rook >= 1.1 + fork not available"
The parameters are URL decoded then JSON decoded and then passed to your function. This handler takes a complicated structure and an integer as parameters. The structure should be a hash with an array element keyed by "A". The element of that array indexed by the integer is returned.
cx.param <- build.service(function(struct, n) {
## In Javascript:
## struct["A"][n - 1]
struct$A[n]
}, name = "cxpar")
reg <- new.registry(cx.param)
server <- startRookAnalysisPageServer(reg, port = port)
show.analysis("cxpar", list(struct = list(A = c(3,1,4,1,5,9,2,6,5,3)), n = 6), show.url = TRUE)
## http://127.0.0.1:3324/custom/RAPS/R/analysis?page=%22cxpar%22&struct=%7B%22A%22%3A%5B3%2C1%2C4%2C1%2C5%2C9%2C2%2C6%2C5%2C3%5D%7D&n=6
## [1] "Rook >= 1.1 + fork not available"
kill.process(server)
"Rook >= 1.1 + fork not available"
Sometimes the automatic JSON encoding of the response is a hassle. Or,
you might want to set the MIME type of the response. If you explicitly
return an AnalysisPageResponse
object then you can have finer
control. This is done with the new.response
constructor.
In this example we return a complete HTML page.
poem.file <- system.file("examples/in-a-station-of-the-metro.html", package="AnalysisPageServer")
poem.html <- readLines(poem.file, warn = FALSE)
poem <- build.service(function() {
new.response(paste0(poem.html, "\n"),
content.type = "text/html")
}, name = "poem")
reg <- new.registry(poem)
server <- startRookAnalysisPageServer(reg, port = port)
url <- service.link("poem", app.base.url = app.base.url)
readLines(url, warn = FALSE)
"Rook >= 1.1 + fork not available"
Or open it in the browser:
kill.process(server)
"Rook >= 1.1 + fork not available"
The new.response
constructor lets us return other things. For
example, we could even put a plot into the response. Let's encode the
Maunga Whau Volcano plot that we did in the static content example page. We'll use
R's readBin
to read it into a raw vector since it is binary data.
The single parameter to the function will be an array of colors to use for the color ramp.
volcano <- build.service(function(colors = c("red","white","purple")) {
plotfile <- tempfile(fileext = ".png")
png(plotfile, width = 600, height = 400)
par(mar=c(1, 1, 4, 1))
col <- colorRampPalette(colors)(50)
image(datasets::volcano, xaxt = "n", yaxt = "n", main = "Maunga Whau Volcano",
col = col, cex.main = 2)
dev.off()
plot.data <- readBin(plotfile, "raw", n = file.info(plotfile)[, "size"])
new.response(body = plot.data,
content.type = "image/png")
}, name = "volcano")
reg <- new.registry(volcano)
server <- startRookAnalysisPageServer(reg, port = port)
A four-colored volcano:
If the colors
parameter is omitted then the defaults from the
function definition will be used (in this case red, white and purple):
kill.process(server)