Skip to content

Commit 1b4bb50

Browse files
gogonzom7prgithub-actions[bot]pawelrudependabot-preview[bot]
authored
479 mirai lockfile@main (#1263)
#1276 @pawelru @m7pr This is a `mirai` alternative. Package seems to address the biggest issues reported during a research: - `mirai` has a native support of `ExtendedTask` - `mirai` is not being killed when `runApp` is executed (like `callr` does) - `mirai` by default opens a deamon in parallel R session without a need to handle the `future::plan`. - `mirai` has only one dependency in the whole dependency tree. #### Disadvantages so far: - ~~we need to pass and set `options`, system vars, working directory and `.libPaths` r-lib/mirai#122 #### How does it work: - lockfile creation is invoked in `init` before application starts. This prevents to start the process each time when a new shiny session starts. Process is invoked as a promise and eventually `teal_app.lock` will be created - When shiny session starts `download lockfile` button is hidden by default. If promise is eventually resolved and lockfile is created then download button is shown. - alternatively, app developer can pre-compute lockfile and provide its path in `teal.renv.lockfile` option. In such case `renv::snapshot` will be skipped and user lockfile will be used in an app. #### Logs and notifications Logs are printed for app developer while notifications are presented to the app user: 1. When app uses precomputed file: - log in init: `Lockfile set using option "teal.renv.lockfile" - skipping automatic creation.` - no notification to the app user. 2. When app automatically determines snapshot: - log in init: `Lockfile creation started based on { getwd() }.` - log If lockfile created: `Lockfile {path} containing { n-pkgs } packages created{ with errors or warnings }.` - notification if lockfile created: `Lockfile available to download` - log if lockfile not created: `Lockfile creation failed.` - notification if lockfile not created: `Lockfile creation failed.` --------- Signed-off-by: Marcin <[email protected]> Signed-off-by: Pawel Rucki <[email protected]> Co-authored-by: m7pr <[email protected]> Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Marcin <[email protected]> Co-authored-by: Pawel Rucki <[email protected]> Co-authored-by: 27856297+dependabot-preview[bot]@users.noreply.github.com <27856297+dependabot-preview[bot]@users.noreply.github.com> Co-authored-by: Aleksander Chlebowski <[email protected]>
1 parent 19cda80 commit 1b4bb50

File tree

12 files changed

+336
-206
lines changed

12 files changed

+336
-206
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,13 @@ repos:
1717
additional_dependencies:
1818
- davidgohel/flextable # Error: package 'flextable' is not available
1919
- davidgohel/gdtools # for flextable
20+
- mirai
2021
- checkmate
21-
- future
2222
- jsonlite
2323
- lifecycle
2424
- logger
2525
- magrittr
2626
- methods
27-
- promises
2827
- renv
2928
- rlang
3029
- shiny

DESCRIPTION

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,10 @@ Depends:
4141
teal.slice (>= 0.5.1.9009)
4242
Imports:
4343
checkmate (>= 2.1.0),
44-
future (>= 1.33.2),
4544
jsonlite,
4645
lifecycle (>= 0.2.0),
4746
logger (>= 0.2.0),
4847
methods,
49-
promises (>= 1.3.0),
50-
renv (>= 1.0.7),
5148
rlang (>= 1.0.0),
5249
shinyjs,
5350
stats,
@@ -59,8 +56,10 @@ Imports:
5956
Suggests:
6057
bslib,
6158
knitr (>= 1.42),
59+
mirai (>= 1.1.1),
6260
MultiAssayExperiment,
6361
R6,
62+
renv (>= 1.0.7),
6463
rmarkdown (>= 2.23),
6564
rvest,
6665
shinytest2,
@@ -74,8 +73,9 @@ RdMacros:
7473
lifecycle
7574
Config/Needs/verdepcheck: rstudio/shiny, insightsengineering/teal.data,
7675
insightsengineering/teal.slice, mllg/checkmate,
77-
HenrikBengtsson/future, jeroen/jsonlite, r-lib/lifecycle,
78-
daroczig/logger, rstudio/promises, rstudio/renv, r-lib/rlang,
76+
jeroen/jsonlite, r-lib/lifecycle,
77+
daroczig/logger, shikokuchuo/mirai, shikokuchuo/nanonext,
78+
rstudio/renv, r-lib/rlang,
7979
daattali/shinyjs, insightsengineering/teal.code,
8080
insightsengineering/teal.logger, insightsengineering/teal.reporter,
8181
insightsengineering/teal.widgets, rstudio/bslib, yihui/knitr,
@@ -106,6 +106,7 @@ Collate:
106106
'module_snapshot_manager.R'
107107
'module_teal.R'
108108
'module_teal_data.R'
109+
'module_teal_lockfile.R'
109110
'module_teal_with_splash.R'
110111
'module_transform_data.R'
111112
'reporter_previewer_module.R'
@@ -116,7 +117,6 @@ Collate:
116117
'teal_data_module-eval_code.R'
117118
'teal_data_module-within.R'
118119
'teal_data_utils.R'
119-
'teal_lockfile.R'
120120
'teal_reporter.R'
121121
'teal_slices-store.R'
122122
'teal_slices.R'

R/init.R

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,6 @@ init <- function(data,
153153
# log
154154
teal.logger::log_system_info()
155155

156-
# invoke lockfile creation
157-
teal_lockfile()
158-
159156
# argument transformations
160157
## `modules` - landing module
161158
landing <- extract_module(modules, "teal_module_landing")

R/module_teal.R

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ ui_teal <- function(id,
138138
footer,
139139
teal.widgets::verbatim_popup_ui(ns("sessionInfo"), "Session Info", type = "link"),
140140
br(),
141-
downloadLink(ns("lockFile"), "Download .lock file"),
141+
ui_teal_lockfile(ns("lockfile")),
142142
textOutput(ns("identifier"))
143143
)
144144
)
@@ -156,6 +156,8 @@ srv_teal <- function(id, data, modules, filter = teal_slices()) {
156156
moduleServer(id, function(input, output, session) {
157157
logger::log_debug("srv_teal initializing.")
158158

159+
srv_teal_lockfile("lockfile")
160+
159161
output$identifier <- renderText(
160162
paste0("Pid:", Sys.getpid(), " Token:", substr(session$token, 25, 32))
161163
)
@@ -166,8 +168,6 @@ srv_teal <- function(id, data, modules, filter = teal_slices()) {
166168
title = "SessionInfo"
167169
)
168170

169-
output$lockFile <- teal_lockfile_downloadhandler()
170-
171171
# `JavaScript` code
172172
run_js_files(files = "init.js")
173173

R/module_teal_lockfile.R

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
#' Generate lockfile for application's environment reproducibility
2+
#'
3+
#' @param lockfile_path (`character`) path to the lockfile.
4+
#'
5+
#' @section Different ways of creating lockfile:
6+
#' `teal` leverages [renv::snapshot()], which offers multiple methods for lockfile creation.
7+
#'
8+
#' - **Working directory lockfile**: `teal`, by default, will create an `implicit` type lockfile that uses
9+
#' `renv::dependencies()` to detect all R packages in the current project's working directory.
10+
#' - **`DESCRIPTION`-based lockfile**: To generate a lockfile based on a `DESCRIPTION` file in your working
11+
#' directory, set `renv::settings$snapshot.type("explicit")`. The naming convention for `type` follows
12+
#' `renv::snapshot()`. For the `"explicit"` type, refer to `renv::settings$package.dependency.fields()` for the
13+
#' `DESCRIPTION` fields included in the lockfile.
14+
#' - **Custom files-based lockfile**: To specify custom files as the basis for the lockfile, set
15+
#' `renv::settings$snapshot.type("custom")` and configure the `renv.snapshot.filter` option.
16+
#'
17+
#' @section lockfile usage:
18+
#' After creating the lockfile, you can restore the application's environment using `renv::restore()`.
19+
#'
20+
#' @seealso [renv::snapshot()], [renv::restore()].
21+
#'
22+
#' @return `NULL`
23+
#'
24+
#' @name module_teal_lockfile
25+
#' @rdname module_teal_lockfile
26+
#'
27+
#' @keywords internal
28+
NULL
29+
30+
#' @rdname module_teal_lockfile
31+
ui_teal_lockfile <- function(id) {
32+
ns <- NS(id)
33+
shiny::tagList(
34+
tags$span("", id = ns("lockFileStatus")),
35+
shinyjs::disabled(downloadLink(ns("lockFileLink"), "Download lockfile"))
36+
)
37+
}
38+
39+
#' @rdname module_teal_lockfile
40+
srv_teal_lockfile <- function(id) {
41+
moduleServer(id, function(input, output, session) {
42+
logger::log_debug("Initialize srv_teal_lockfile.")
43+
enable_lockfile_download <- function() {
44+
shinyjs::html("lockFileStatus", "Application lockfile ready.")
45+
shinyjs::hide("lockFileStatus", anim = TRUE)
46+
shinyjs::enable("lockFileLink")
47+
output$lockFileLink <- shiny::downloadHandler(
48+
filename = function() {
49+
"renv.lock"
50+
},
51+
content = function(file) {
52+
file.copy(lockfile_path, file)
53+
file
54+
},
55+
contentType = "application/json"
56+
)
57+
}
58+
disable_lockfile_download <- function() {
59+
warning("Lockfile creation failed.", call. = FALSE)
60+
shinyjs::html("lockFileStatus", "Lockfile creation failed.")
61+
shinyjs::hide("lockFileLink")
62+
}
63+
64+
shiny::onStop(function() {
65+
if (file.exists(lockfile_path) && !shiny::isRunning()) {
66+
logger::log_debug("Removing lockfile after shutting down the app")
67+
file.remove(lockfile_path)
68+
}
69+
})
70+
71+
lockfile_path <- "teal_app.lock"
72+
mode <- getOption("teal.lockfile.mode", default = "")
73+
74+
if (!(mode %in% c("auto", "enabled", "disabled"))) {
75+
stop("'teal.lockfile.mode' option can only be one of \"auto\", \"disabled\" or \"disabled\". ")
76+
}
77+
78+
if (mode == "disabled") {
79+
logger::log_debug("'teal.lockfile.mode' option is set to 'disabled'. Hiding lockfile download button.")
80+
shinyjs::hide("lockFileLink")
81+
return(NULL)
82+
}
83+
84+
if (file.exists(lockfile_path)) {
85+
logger::log_debug("Lockfile has already been created for this app - skipping automatic creation.")
86+
enable_lockfile_download()
87+
return(NULL)
88+
}
89+
90+
if (mode == "auto" && .is_disabled_lockfile_scenario()) {
91+
logger::log_debug(
92+
"Automatic lockfile creation disabled. Execution scenario satisfies teal:::.is_disabled_lockfile_scenario()."
93+
)
94+
shinyjs::hide("lockFileLink")
95+
return(NULL)
96+
}
97+
98+
if (!.is_lockfile_deps_installed()) {
99+
warning("Automatic lockfile creation disabled. `mirai` and `renv` packages must be installed.")
100+
shinyjs::hide("lockFileLink")
101+
return(NULL)
102+
}
103+
104+
# - Will be run only if the lockfile doesn't exist (see the if-s above)
105+
# - We render to the tempfile because the process might last after session is closed and we don't
106+
# want to make a "teal_app.renv" then. This is why we copy only during active session.
107+
process <- .teal_lockfile_process_invoke(lockfile_path)
108+
observeEvent(process$status(), {
109+
if (process$status() %in% c("initial", "running")) {
110+
shinyjs::html("lockFileStatus", "Creating lockfile...")
111+
} else if (process$status() == "success") {
112+
result <- process$result()
113+
if (any(grepl("Lockfile written to", result$out))) {
114+
logger::log_debug("Lockfile containing { length(result$res$Packages) } packages created.")
115+
if (any(grepl("(WARNING|ERROR):", result$out))) {
116+
warning("Lockfile created with warning(s) or error(s):", call. = FALSE)
117+
for (i in result$out) {
118+
warning(i, call. = FALSE)
119+
}
120+
}
121+
enable_lockfile_download()
122+
} else {
123+
disable_lockfile_download()
124+
}
125+
} else if (process$status() == "error") {
126+
disable_lockfile_download()
127+
}
128+
})
129+
130+
NULL
131+
})
132+
}
133+
134+
utils::globalVariables(c("opts", "sysenv", "libpaths", "wd", "lockfilepath", "run")) # needed for mirai call
135+
#' @rdname module_teal_lockfile
136+
.teal_lockfile_process_invoke <- function(lockfile_path) {
137+
mirai_obj <- NULL
138+
process <- shiny::ExtendedTask$new(function() {
139+
m <- mirai::mirai(
140+
{
141+
options(opts)
142+
do.call(Sys.setenv, sysenv)
143+
.libPaths(libpaths)
144+
setwd(wd)
145+
run(lockfile_path = lockfile_path)
146+
},
147+
run = .renv_snapshot,
148+
lockfile_path = lockfile_path,
149+
opts = options(),
150+
libpaths = .libPaths(),
151+
sysenv = as.list(Sys.getenv()),
152+
wd = getwd()
153+
)
154+
mirai_obj <<- m
155+
m
156+
})
157+
158+
shiny::onStop(function() {
159+
if (mirai::unresolved(mirai_obj)) {
160+
logger::log_debug("Terminating a running lockfile process...")
161+
mirai::stop_mirai(mirai_obj) # this doesn't stop running - renv will be created even if session is closed
162+
}
163+
})
164+
165+
suppressWarnings({ # 'package:stats' may not be available when loading
166+
process$invoke()
167+
})
168+
169+
logger::log_debug("Lockfile creation started based on { getwd() }.")
170+
171+
process
172+
}
173+
174+
#' @rdname module_teal_lockfile
175+
.renv_snapshot <- function(lockfile_path) {
176+
out <- utils::capture.output(
177+
res <- renv::snapshot(
178+
lockfile = lockfile_path,
179+
prompt = FALSE,
180+
force = TRUE,
181+
type = renv::settings$snapshot.type() # see the section "Different ways of creating lockfile" above here
182+
)
183+
)
184+
185+
list(out = out, res = res)
186+
}
187+
188+
#' @rdname module_teal_lockfile
189+
.is_lockfile_deps_installed <- function() {
190+
requireNamespace("mirai", quietly = TRUE) && requireNamespace("renv", quietly = TRUE)
191+
}
192+
193+
#' @rdname module_teal_lockfile
194+
.is_disabled_lockfile_scenario <- function() {
195+
identical(Sys.getenv("CALLR_IS_RUNNING"), "true") || # inside callr process
196+
identical(Sys.getenv("TESTTHAT"), "true") || # inside devtools::test
197+
!identical(Sys.getenv("QUARTO_PROJECT_ROOT"), "") || # inside Quarto process
198+
(
199+
("CheckExEnv" %in% search()) || any(c("_R_CHECK_TIMINGS_", "_R_CHECK_LICENSE_") %in% names(Sys.getenv()))
200+
) # inside R CMD CHECK
201+
}

0 commit comments

Comments
 (0)