Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First draft of auto-reload functionality #246

Draft
wants to merge 14 commits into
base: main
Choose a base branch
from
Draft

Conversation

klmr
Copy link
Owner

@klmr klmr commented Sep 17, 2021

Work in progress implementation of #234.

The first draft includes auto-reloading for subsequent box::use calls:

  • box::enable_autoreload() enables auto-reloading for box::use calls only. This solution should work e.g. for Shiny applications. But it won’t work well inside scripts and R Markdown documents, since box::use is required to trigger a reload.
  • box::disable_autoreload() disables auto-reloading.
  • box::enable_autoreload accepts include and exclude arguments to limit which modules get auto-reloaded:
    • box::enable_autoreload(include = c(./a, my/mod, foo/bar))
    • or a single module, box::enable_autoreload(include = foo/bar)
    • same for exclude
  • box::autoreload_include(...) and box::autoreload_exclude(...) add includes or excludes after auto-reloading has been enabled
  • only “top-level” changes are tracked, not module dependencies; this means that, if ./a imports ./b and b.r changes, then box::use(./a) won’t reload either a.r or b.r, since only the (unchanged) timestamp of a.r is checked.

Well, that’s about it for now.

Still to be done:

  • implement tests
  • implement reloading for module dependency changes
  • implement auto-reloading for qualified module access (mod$x)
  • implement auto-reloading for access of attached module objects
  • finalise API design
  • handle module namespace hooks (how?) — the current draft only runs .on_load

/cc @grst for feedback

@grst
Copy link

grst commented Sep 17, 2021

Nice! Looks really clean, although I feel I don't know enough low-level R to properly review this.

I think I can follow the code for tracking the file changes, I don't get where this actually hooks into the box::use call.

@klmr
Copy link
Owner Author

klmr commented Sep 17, 2021

@grst The relevant changes are in loaded.r. is_mod_loaded and register_mod are invoked during the actual module loading, which is one of the steps invoked for each module loaded by box::use:

box/R/use.r

Lines 372 to 392 in ac3f691

`load_mod.box$mod_info` = function (info) {
if (is_mod_loaded(info)) return(loaded_mod(info))
# Load module/package and dependencies; register the module now, to allow
# cyclic imports without recursing indefinitely — but deregister upon
# failure to load.
on.exit(deregister_mod(info))
mod_ns = make_namespace(info)
register_mod(info, mod_ns)
load_from_source(info, mod_ns)
mod_loading_finished(info, mod_ns)
# Call `.on_load` hook just after loading is finished but before exporting
# symbols, so that `.on_load` can modify these symbols.
call_hook(mod_ns, '.on_load', mod_ns)
lockEnvironment(mod_ns, bindings = TRUE)
on.exit()
mod_ns
}

@klmr
Copy link
Owner Author

klmr commented Sep 19, 2021

@grst I’d be grateful if you could test-drive the pull request. The implementation is pretty crude, I’m sure there are situations which I’ve overlooked in my tests, and real-world usage is necessary to improve the implementation.

To install, open R and run

install.packages('https://github.com/klmr/box/files/7192969/box_1.1.9000.tar.gz', type = 'source', repos = NULL)

… or download the archive and run the command locally.

As far as I’m aware, the code is feature complete — except no unload hook is being run. I haven’t decided yet whether to do this. Manual reload does, but honestly if a module relies on that hook then dynamic reloading isn’t going to work properly anyway in all situations.

The API is also still subject to change: I’m not happy with the current names, they’re too long for my taste. I might shorten enable_autoload to just autoload, and I would like to change on_access to something more explanatory, or shorter, or ideally both.

@grst
Copy link

grst commented Sep 20, 2021

I'll give it a try this week!

@klmr
Copy link
Owner Author

klmr commented Sep 20, 2021

@grst You may want to hold off on testing, Iʼve already found a bug in combination with Shiny, and I havenʼt got a fix yet.

@klmr
Copy link
Owner Author

klmr commented Sep 21, 2021

@grst OK, fixed that bug. New archive:

https://github.com/klmr/box/files/7204838/box_1.1.9000.tar.gz

@grst
Copy link

grst commented Sep 22, 2021

I currently get the following error when starting my shiny app:

> shiny::runApp()
Loading required package: shiny
Error in box::use(shiny[...], shinydashboard[...], ./modules/overview,  : 
  object 'c_strict_extract' not found
(inside “`$.box$ns`(ns, ".__module__.")”)

EDIT: I installed the latest version of this branch using remotes::install_github("klmr/box@feature/auto-reload")

@klmr
Copy link
Owner Author

klmr commented Sep 22, 2021

EDIT: I installed the latest version of this branch using remotes::install_github("klmr/box@feature/auto-reload")

Unfortunately that won’t work since I’m not checking generated code (NAMESPACE and man files) into development branches. However, the ‘remote’ (as well as ‘devtools’ and ‘pak’) packages require these files. I’m surprised the installation “worked” at all, but it seems the compiled files are missing. Please try installing the .tar.gz file instead, it contains the generated files.

@grst
Copy link

grst commented Sep 22, 2021

I see!

If I use the archive 7204838/box_1.1.9000.tar.gz from above, I can run the app, but autoreload doesnt work as expected.

For instance, in my app.R I have

box::use(
  shiny[...],
  shinydashboard[...],
  . / modules / overview,
  . / modules / gene_expression,
  . / modules / correlation,
  . / config[bar],
  ggpubr[mean_ci]
)
box::enable_autoreload()

ui <- [...]

server <- function(input, output) {
  callModule(overview$server, "overview")
  callModule(gene_expression$server, "gene_expression")
  callModule(correlation$server, "correlation")
}

shinyApp(ui = ui, server = server)

If I change something in server() of modules/gene_expression.r and click "Reload App", this doesn't change in the app.
If I restart the rsession and restart the app, it does.

@grst
Copy link

grst commented Sep 22, 2021

Actually, by using box::enable_autoreload(on_access=TRUE) instead, it works 🎉

Is that expected? I thought with shiny, we wouldn't even need the on_access feature.

@klmr
Copy link
Owner Author

klmr commented Sep 22, 2021

Ah yes, the current default mode of box::enable_autoreload() is only triggered when a box::use declaration is re-executed (that’s because this is vastly more efficient than capturing every single access).

This works if your box::use call is inside the server function, for instance, because reloading the Shiny application (via “Reload app” or by refreshing the browser window) re-executes the server function (even without restarting the session).

When it’s at file scope as in your case, you’ll need to pass on_access = TRUE to box::enable_autoreload(). This is also needed if you want to get the effect of auto-reload without reloading your Shiny application.

As mentioned, the only reason this isn’t the default currently is that it’s less efficient. However, the whole point of testing is to figure out what would be most convenient for the user: I’m more than happy to change the default behaviour if we find that that’s more appropriate. So please let me know what you think, and whether you have any ideas for a better syntax, or other configuration options you find worth having.


Incidentally, box::enable_autoreload() isn’t supposed to go into the code (that kind of defeats the purpose), but I understand that it’s convenient. I just execute it inside the console after starting the R session.

@klmr klmr mentioned this pull request Dec 7, 2021
@DavidJesse21
Copy link

DavidJesse21 commented Dec 7, 2021

EDIT:
Sorry about the previous reply, I didn't realize you already mentioned that the specific version I have downloaded has a bug and that you provided a more recent version.
I have downloaded the more recent archive file and now everything works as I was expecting :)

@klmr
Copy link
Owner Author

klmr commented Jan 2, 2022

How useful is it to be able to configure which modules are auto-reloaded (via box::autoreload_include and box::autorerload_exclude)? I am considering dropping this functionality, and/or removing the box::*_autoreload functions in favour of having configurable R options:

  • If getOption('box.autoreload') is TRUE (inside interactive sessions only), modules are auto-reloaded. Otherwise they aren’t.
  • Optionally, getOption('box.autoreload_mode') controls how auto-reloading works; the value could be either 'on_use' (corresponds to setting on_access = FALSE) or 'all' (corresponds to setting on_access = TRUE).

… as with the box.path option, these would also be settable via environment variables (R_BOX_AUTORELOAD and R_BOX_AUTORELOAD_MODE).

Using options rather than functions to configure auto-reloading is less powerful: there’s currently no provision for excluding modules from auto-reload, and no way to enable or disable auto-reload mode inside a running R session — the ‘box’ package would have to be unloaded and reloaded before changed options would take effect.

On the flip side, I think it would simplify documentation and usage.

@grst
Copy link

grst commented Jan 3, 2022

So far, I didn't run into any situation where I'd have had to exclude certain files from autoreloading. However, one situation where I believe this could be useful is if there is a short-running function that is called many times. In that case the overhead of checking if the file was modified could become significant.

But then again, I could simply not use the on_access feature in that case.

Lastly, I find the proposed naming of the options slightly confusing: At the first glance I thought on_use equals on_access=TRUE, because it reloads "on use" of the function (but ofc it refers to box::use instead). How about on_use and on_access to make the distinction clearer?

@DavZim
Copy link

DavZim commented Jun 30, 2022

slightly off-topic...

I love {box} and its functionality and the auto-reload/invalidate-cache functionality would help in my use-case a lot (I currently use box inside of shiny applications).
This PR would allow me to auto-reload the modules I import when the app is run (eg I change something in the module, then I would need to either refresh the session to invalidate the cache so that the latest version is loaded or use the following workaround).

A current workaround is taken from rhino where the following two-liner invalidates the whole box-cache:

loaded_mods <- loadNamespace("box")$loaded_mods
rm(list = ls(loaded_mods), envir = loaded_mods)

@klmr klmr force-pushed the feature/auto-reload branch from b67501d to 216f3ca Compare June 22, 2023 19:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants