{golem}
, reflétant une discussion que nous avons eue précédemment au sein de l’équipe. Aussi, il y a deux semaines, j’ai reçu un tweet sur le même sujet. Sujet qui peut être résumé comme tel : « Devrions-nous utiliser shinyApp()
ou runApp()
lors du déploiement en production ? »
{golem}
possède une fonction appelée run_app()
, qui, dans son implémentation originale par Vincent, repose sur l’appel de runApp()
sur les deux fichiers contenus dans le dossier inst/
. Mais voici le problème — runApp()
ne peut pas être utilisé avec les produits RStudio (Shiny Server, Connect & ; Shinyapps.io), car il renvoie une erreur Can't call runApp()
from within runApp()
.
Alors, pourquoi utilier runApp()
? Vous verrez dans une minute pourquoi on avait choisit cette fonction 🙂 Mais, TL;DR, ces trois fonctions n’ont pas le même comportement, selon l’endroit où elles sont utilisées :
runApp()
ne fonctionne pas sur les produits RStudio, mais c’est la seule façon pour Docker et les appels locaux d’accéder aux options passées à la fonctionrun_app()
.- Aucun produit RStudio ne gère pas les options « locales » utilisées dans le
run_app()
, selon notre implémentation originale. {golem}
possède aujourd’hui sa propre implémentation de lancement d’application, pour pouvoir marcher partout !
Il y a plusieurs façons de lancer une application Shiny App
Table des matières
runApp()
(implémentation de départ de{golem}
), qui est un wrapper autourshiny::runApp(system.file("app", package = "aaaaaaa"))
.shinyApp()
, qui estshiny::shinyApp(ui = app_ui(), server = app_server)
, la solution créée par l’ancienne version degolem::add_rstudioconnect_file()
et friends.shinyAppDir()
, qui estshinyAppDir( system.file("app", package = "aaaaaaa") )
– une solution nécessaire pour Shiny server si vous voulez appeler le dossierapp/
via{golem}
.- La version actuelle avec
{golem}
.
shinyAppFile()
, mais son comportement interne est le même que shinyAppDir()
, donc il n’est pas comparé ici.
Une implémentation naïve
Donc, ce que nous pourrions faire, c’est de simplement laisser ouvert pour l’utilisateur final, de sorte que c’est à lui de choisir la meilleure implémentation pour son cas d’utilisation de déploiement. En faisant quelque chose comme :run_app <- fonction(
with = c("shinyApp", "runApp", "shinyAppDir")
) {
avec <- match.arg(with)
if (with == "shinyApp"){
shiny::shinyApp(ui = app_ui(), server = app_server)
} else if (with == "runApp") {
shiny::runApp(system.file("app", package = "aaaaa"))
} else if (with == "shinyAppDir") {
shiny::shinyAppDir(system.file("app", package = "aaaaa"))
}
)
C’est peut-être la meilleure réponse car elle laisse le choix à l’utilisateur, mais la question reste ouverte : quelle fonction dois-je utiliser pour mon déploiement ?
Nous allons conserver cette fonction et l’utiliser pour notre benchmark.
Side node
Si je me réfère à?shinyApp
:
You generally shouldn’t need to use these functions to create/run applications; they are intended for interoperability purposes. https://shiny.rstudio.com/reference/shiny/1.3.2/shinyApp.htmlDonc, d’après la doc, nous ne devrions jamais (ou presque) appeler
shinyApp()
directement, et utiliser seulement runApp()
à la place. Mais utiliser runApp()
est impossible avec les plates-formes RStudio, car elles impriment une erreur qui ressemble à ceci :
Loading aaaa
Error in shiny::runApp(system.file("app", package = "aaaa")) :
Can't call `runApp()` from within `runApp()`. If your application code contains `runApp()`, please remove it.
Calls: runApp ... eval -> eval -> ..stacktraceon.. -> run_app -> <Anonymous>
Side note 2
J’ai essayé de rendre ce benchmark aussi reproductible que possible, donc n’hésitez pas à l’exécuter et voir si vous obtenez les mêmes résultats 🙂 Le paquet est nommé « aaaaa » (donc il n’entrera en conflit avec aucun autre paquet (j’espère), et peut être trouvé ici. Il contient le squelette du golem avec les fonctions listées ci-dessous.Recherche de la meilleure implémentation
Essayons de trouver la meilleure implémentation. L’idée est que notre implémentation derun_app()
devrait :
- Marcher sur le nombre maximum de services (local + Docker + RStudio), et ce avec un minimum d’ajustements (une implémentation pour les régler tous serait préférable).
- Pour pouvoir lire les options de l’environnement global, par exemple on peut utiliser la variable globale
golem.app.prod
depuis le serveur ou l’interface utilisateur. - Pouvoir lire des options locales, passées à
run_app()
.
Conditions de benchmark
- Nouveau golem avec la version précédent (0.0.1.700)
Contenu de la fonction app_ui:
app_ui <- fonction() {)
tagList(
fluidPage(
h1 ("aaaaa"),
h3("options globales :"),
verbatimTextOutput("global"),
h3("options de fonction :"),
verbatimTextOutput("shinycall")
)
)
Contenu de la fonction app_server :
app_server <- fonction(entrée, sortie,session) {
output$global <- renderPrint({ {
# Options globales
getOption('golem.pkg.name')
})
output$shinycall <- renderPrint({ { {
# Options locales
getOption('shinycall')
})
Diverses options de run_app
Nous utiliserons cette fonction pour comparer les trois fonctions (shinyApp()
, runApp()
et shinyAppDir()
) :
run_app <- fonction(
avec = c("shinyApp", "runApp", "shinyAppDir")
) {
with <- match.arg(with)
# Ici, on définit les options locales pour pouvoir passer
# des arguments à la fonction run_app()
options("shinycall" = avec)
on.exit(
options("shinycall" = NULL)
)
if (with == "shinyApp"){
shiny::shinyApp(ui = app_ui(), server = app_server)
else if (with == "runApp") {
shiny::runApp(system.file("app", package = "aaaaa"))
else {
shiny::shinyAppDir(system.file("app", package = "aaaaa"))
}
Dans nos résultats, runApp est:
run_app("runApp" )
shinyApp est :
run_app("shinyApp" )
shinyAppDir est
run_app("shinyAppDir" )
Contextes de lancement
Lancement local
Vous pouvez l’exécuter dans votre console, dans RStudio.# Définir les options ici
options("golem.pkg.name" = "aaa")
# Détachez tous les paquets chargés et nettoyez votre environnement
golem::detach_all_attached()
# rm(list=ls(all.names = TRUE)))
# Documentez et rechargez votre colis
golem::document_et_reload()
# Exécutez l'application
aaaaa::run_app(avec = "runApp")
La Dockerfile locale est
FROM rocker/tidyverse:3.6.0
RUN R -e 'install.packages("remotes")'
RUN R -e 'remotes::install_github("r-lib/remotes", ref = "97bbf81")'
RUN R -e 'remotes::install_cran("shiny")'
COPY aaaa_*.tar.gz /app.tar.gz
RUN R -e 'remotes::install_local("/app.tar.gz")'
EXPOSE 80
CMD R -e "options('shiny.port'=1234,shiny.host='0.0.0.0', 'golem.pkg.name' = 'aaa');aaaa::run_app( 'runApp' )" # also with shinyApp & shinyAppDir
Vous pouvez trouver ce Dockerfile
dans le inst/dockerfilelocal
dossier du repo golem4bench
repo. Avant de le lancer, vous devez documenter, et compiler votre paquet avec devtools::build(chemin = "inst/dockerfilelocal/")
.
Pour que la chose entière puisse être lancée avec:
R -e "devtools::build(path = 'inst/dockerfilelocal/')" \
&& cd inst/dockerfilelocal/ \
&& docker build -t aaa . \
&& docker run --name aaaa -p 1234:1234 -d aaa \
&& sleep 2 \
&& open http://0.0.0.0:1234
Ensuite, restez dans le dossier, changez le "runApp"
arg dans le Dockerfile en "shinyApp"
, rebuild et relancez depuis la ligne docker build
. Puis avec "shinyAppDir"
. Bien sûr, n’oubliez pas de docker kill aaa & docker rm aaaa
entre chaque itération.
Produits RStudio 1/2 : le script app.R
pkgload::load_all()
options("golem.pkg.name" = "aaaa"))
run_app( "runApp" ) # aussi avec shinyApp & ; shinyAppDir
Chaque trois versions (c’est-à-dire les trois versions de run_app()
) de ce fichier seront déployées sur :
- un Shiny Server local (via Docker)
- Le RStudio Connect de ThinkR (envoyé avec
rsconnect::deployApp()
) - Le compte shinyapps de ThinkR (envoyé avec
rsconnect::deployApp()
)
Produits RStudio 2/2 : Configurer un serveur Shiny pour tester
Ce Dockerfile se trouve dans le dossierinst/dockerfileshinyserver
du paquet.
FROM rocker/shiny:3.6.0
RUN R -e 'install.packages("remotes")'
RUN R -e 'remotes::install_github("r-lib/remotes", ref = "97bbf81")'
RUN R -e 'remotes::install_cran("shiny")'
RUN apt-get update && apt-get install libssl-dev libxml2-dev -y
RUN R -e 'remotes::install_cran("attachment")'
RUN R -e 'remotes::install_github("thinkr-open/golem")'
COPY . /srv/shiny-server/aaaa
RUN cd /srv/shiny-server/aaaa && R -e "attachment::install_from_description()"
Depuis la racine du package:
mv inst/dockerfileshinyserver/Dockerfile Dockerfile \
&& docker build -t plop . \
&& docker run --name plop -p 3838:3838 -d plop \
&& sleep 2 \
&& open http://0.0.0.0:3838/aaaa
Ensuite, changez pour "shinyApp"
et "shinyAppDir"
dans le fichier app.R
, puis relancez le docker build
. N’oubliez pas de kill & rm entre chaque itération, et de mv
back la Dockerfile à sa place
Résultats
Les options globales sont celles définies en dehors derun_app()
, les options locales sont celles définies dans le run_app()
.
🚀 : l’application lance
💥 : l’application ne lance pas
📗 : les options globales sont lues
❌ : les options globales ne sont pas lues
📒 : les options fonction sont lues
⛔️ : les options fonction ne sont pas lues
Ou | runApp | shinyApp | shinyAppDir |
---|---|---|---|
Local | 🚀📗📒 | 🚀📗⛔️ | 🚀📗⛔️ |
Docker | 🚀📗📒 | 🚀📗⛔️ | 🚀📗⛔️ |
Connect | 💥❌ ⛔️ | 🚀📗⛔️ | 🚀📗⛔️ |
shinyApps.io | 💥❌ ⛔️ | 🚀📗⛔️ | 🚀📗⛔️ |
ShinyServer | 💥❌ ⛔️ | 🚀📗⛔️ | 🚀📗⛔️ |
- Les conteneurs Docker ne lisent pas d’options locales des fonctions à moins d’être appelés avec
runApp()
. Ce que vous pouvez vérifier en utilisant n’importe quel terminal :R -e "options('shiny.port'=1234,shiny.host='0.0.0.0','golem.pkg.name' ='aaaa');aaaa::run_app('runApp')"
runApp()
échoue sur les produits RStudio.- Les services RStudio n’ont pas d’options locales avec aucune solution (nous verrons dans une minute que c’est parce que nous ne pouvons pas utiliser
runApp())
.
Où allons-nous à partir de là ?
Alors, pourquoi ces différents comportements ? En fait, c’est à cause de ce queshinyApp()
et shinyAppDir()
retournent, comparés à runApp()
. Si nous regardons le code source de shinyApp()
, notre dernière ligne de code ressemble à ceci:
> shiny::shinyApp
function (ui = NULL, server = NULL, onStart = NULL, options = list(),
uiPattern = "/", enableBookmarking = NULL)
{
[...]
structure(list(httpHandler = httpHandler, serverFuncSource = serverFuncSource,
onStart = onStart, options = options, appOptions = appOptions),
class = "shiny.appobj")
}
<bytecode: 0x7fb1714fa6d0>
<environment: namespace:shiny>
Nous pouvons voir que la dernière chose retournée par la fonction est une structure de classe shiny.appobj
, alors que le runApp()
renvoie un processus lancé.
Ainsi le « lancement » de l’application avec shinyApp()
n’est pas le même que celui de runApp()
– le premier renvoie un objet, le second renvoie un processus. En fait, le lancement de l’application avec shinyApp()
se fait avec print.shiny.appobj
. C’est pourquoi si vous faites a <- shinyAppDir(appDir = "inst/app/")
, vous ne pourrez pas lancer l’application tant que vous n’aurez pas essayé d’imprimer a
.
Ce qui explique aussi pourquoi les options locales (définies dans la fonction) ne sont pas lues : avec shinyApp()
, la fonction renvoie un objet, donc la fonction est terminée, et les options définies ne sont plus accessibles.
Pourquoi est-ce une bonne nouvelle ? Regardons shiny:::::print.shiny.appobj
:
> shiny:::print.shiny.appobj
function (x, ...)
{
opts <- x$options %OR% list()
opts <- opts[names(opts) %in% c("port", "launch.browser",
"host", "quiet", "display.mode", "test.mode")]
args <- c(list(quote(x)), opts)
do.call("runApp", args)
}
<bytecode: 0x7fb16e2809f8>
<environment: namespace:shiny>
Donc ici la chose cool est que l’on peut hacker le x
passé à la méthode print
pour ajouter golem.options
à l’intérieur, c’est-à-dire dans le appOptions
de l’objet app. Et donc :
with_golem_options <- function(app, golem_opts){
app$appOptions$golem_options <- golem_opts
app
}
et
run_app <- function(...) {
with_golem_options(
app = shinyApp(ui = app_ui(), server = app_server),
golem_opts = list(...)
)
}
Soit, dans une app entière :
library(shiny)
options("golem.app.name" = "aaa")
get_golem_options <- function(which = NULL){
if (is.null(which)){
getShinyOption("golem_options")
} else {
getShinyOption("golem_options")[[which]]
}
}
with_golem_options <- function(app, golem_opts){
app$appOptions$golem_options <- golem_opts
app
}
app_ui <- function() {
tagList(
fluidPage(
verbatimTextOutput("all"),
verbatimTextOutput("opt"),
verbatimTextOutput("glob")
)
)
}
app_server <- function(input, output,session) {
output$all <- renderPrint({ get_golem_options() })
output$opt <- renderPrint({ get_golem_options("a") })
output$glob <- renderPrint({ getOption("golem.app.name") })
}
run_app <- function(...) {
with_golem_options(
app = shinyApp(ui = app_ui, server = app_server),
golem_opts = list(...)
)
}
run_app(a = "pouet", b = "bing")
Et ça marche partout 🤘
Donc grâce à ce petit hack, vous pouvez maintenant utiliser la fonction run_app()
de {golem}
partout. Et maintenant, quand vous construisez votre package, vous pouvez utiliser des arguments avec la fonction run_app()
, et les utiliser avec get_golem_options()
. Les options globales sont, comme d’habitude, disponibles avec getOptions()
.
L’autre hack un peu cool? Lorsque vous générez un fichier app.R
pour les produits RStudio avec golem::add_rstudioconnect_file
, golem::add_shinyappsio_file
, ou golem::add_shinyserver_file
, on a laissé trainer un petit ShinyApp
en commentaire, ce qui fait que vous pouvez utiliser le fameux bouton bleu 😉
Session info de tous les tests sur GitHub.