{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.proddepuis 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 mvback 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.


