https://github.com/ThinkR-open/bank
Alternative caching backends for `{memoise}` & `{shiny}`.
https://github.com/ThinkR-open/bank
cache golemverse shiny
Last synced: 4 months ago
JSON representation
Alternative caching backends for `{memoise}` & `{shiny}`.
- Host: GitHub
- URL: https://github.com/ThinkR-open/bank
- Owner: ThinkR-open
- License: other
- Created: 2021-03-02T16:57:00.000Z (about 4 years ago)
- Default Branch: main
- Last Pushed: 2023-03-27T13:24:24.000Z (about 2 years ago)
- Last Synced: 2024-11-28T21:53:02.158Z (5 months ago)
- Topics: cache, golemverse, shiny
- Language: R
- Homepage:
- Size: 73.2 KB
- Stars: 12
- Watchers: 5
- Forks: 2
- Open Issues: 4
-
Metadata Files:
- Readme: README.Rmd
- Changelog: NEWS.md
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
Awesome Lists containing this project
- jimsghstars - ThinkR-open/bank - Alternative caching backends for `{memoise}` & `{shiny}`. (R)
README
---
output: github_document
---```{r, include = FALSE, error = TRUE}
knitr::opts_chunk$set(
collapse = TRUE,
comment = "#>",
fig.path = "man/figures/README-",
out.width = "100%",
eval = FALSE
)system("docker stop mongobank redisbank postgresbank")
```# bank
[](https://github.com/ThinkR-open/bank/actions/workflows/R-CMD-check.yaml)
[](https://lifecycle.r-lib.org/articles/stages.html#experimental)The goal of `{bank}` is to provide alternative backends for caching with `{memoise}` & `{shiny}`.
## Installation
```{r}
# install.packages("remotes")
remotes::install_github("thinkr-open/bank")
```## About
You're reading the doc about version : `r pkgload::pkg_version()`
This README has been compiled on the
```{r, eval = TRUE}
Sys.time()
```Here are the test & coverage results :
```{r, eval = TRUE}
devtools::check(quiet = TRUE)
``````{r echo = FALSE, eval = TRUE}
unloadNamespace("shinipsum")
``````{r, eval = TRUE}
covr::package_coverage()
```## Some things to know before starting
### Caching scope
When using `{bank}` backends with `{shiny}`, caching will done at the app-level, in other words the cache is stored across sessions.
Be aware of this behavior if you have sensitive data inside your app, as this might imply data leakage.See `?shiny::bindCache`
> With an app-level cache scope, one user can benefit from the work done for another user's session. In most cases, this is the best way to get performance improvements from caching. However, in some cases, this could leak information between sessions. For example, if the cache key does not fully encompass the inputs used by the value, then data could leak between the sessions. Or if a user sees that a cached reactive returns its value very quickly, they may be able to infer that someone else has already used it with the same values.
### Cache flushing
As with any `{cachem}` compatible objects, the cache can be manually flushed using the `$reset()` method -- this will call `drop()` on MongoDb, `FLUSHALL` in Redis, & `DBI::dbRemoveTable()` + `DBI::dbCreateTable()` with Postgres.
```{r}
library(bank)
mongo_cache <- cache_mongo$new(
db = "bank",
url = "mongodb://localhost:27066",
prefix = "sn"
)mongo_cache$reset()
```As `{bank}` relies on external backends, it's probably better to let the DBMS handle the flushing of cache.
For example, in `redis.conf`, you can set :```
maxmemory 2mb
maxmemory-policy allkeys-lru
```LRU (least recently used) will allow redis to flush the key based on when they were used.
See .MongoDB doesn't come with a LRU mechanism, but you can set data to be ephemeral with [TTL index](https://docs.mongodb.com/manual/core/index-ttl/) inside your collection.
`{bank}` also tries to help with that by updating a `lastAccessed` date metadata field in Mongo whenever you `$get()` the key, meaning that you can implement your own caching strategy to evict least recently used cached objects.
### Postgre limitation
Postgre `bytea` column can only store up to 1GB elements, so you can't write a cache that's > 1GB.
## Backends
Note that if you want to use `{bank}` in a `{shiny}` app:
- `renderCachedPlot()` require `{shiny}` version 1.5.0 or higher
- `bindCache()` require `{shiny}` version 1.6.0 or higher
For now, the following backends are supported:
+ [MongoDB](#mongo)
+ [Redis](#redis)
+ [Postgre](#postgres)
### Mongo
Launching a container with mongo.
```{bash}
docker run --rm --name mongobank -d -p 27066:27017 -e MONGO_INITDB_ROOT_USERNAME=bebop -e MONGO_INITDB_ROOT_PASSWORD=aloula mongo:4
``````{r echo = FALSE, eval = TRUE}
system("docker run --rm --name mongobank -d -p 27066:27017 -e MONGO_INITDB_ROOT_USERNAME=bebop -e MONGO_INITDB_ROOT_PASSWORD=aloula mongo:4")
Sys.sleep(20)
```#### With `{memoise}`
First, the `cache_mongo` can be used
```{r eval = TRUE}
library(memoise)
library(bank)
# Create a mongo cache.
# The arguments will be passed to mongo::gridfs
mongo_cache <- cache_mongo$new(
db = "bank",
url = "mongodb://bebop:aloula@localhost:27066",
prefix = "sn"
)f <- function(x) {
sample(1:1000, x)
}mf <- memoise(f, cache = mongo_cache)
mf(5)
mf(5)
```#### Inside `{shiny}`
Here is a first simple application that shows you the basics :
```{r}
library(shiny)
ui <- fluidPage(
# Creating a slider input that will be used as a cache key
sliderInput("nrow", "NROW", 1, 32, 32),
# Plotting a piece of mtcars
plotOutput("plot")
)server <- function(input, output, session) {
output$plot <- renderCachedPlot(
{
# Pretending this takes a long time
Sys.sleep(2)
plot(mtcars[1:input$nrow, ])
},
cacheKeyExpr = list(
# Defining the cache key
input$nrow
),
# Using our mongo cache
cache = mongo_cache
)
}
shinyApp(ui, server)
```As you can see, the first time you set the slider to a given value, it takes a little bit to compute.
Then it's almost instantaneous.Let's try a more complex application:
```{r}
# We'll put everything in a function so that it can later be reused with other backends
library(magrittr)generate_app <- function(cache_object) {
ui <- fluidPage(
h1(
sprintf(
"Caching in an external DB using %s",
deparse(
substitute(cache_object)
)
)
),
sidebarLayout(
sidebarPanel = sidebarPanel(
# This sliderInput will be the cache key
# i.e we don't want to recompute the plot everytime
sliderInput("nrow", "Nrow", 1, 32, 32),
# Allow to clear the cache
actionButton("clear", "Clear Cache")
),
mainPanel = mainPanel(
# Outputing the reactive and a plot
verbatimTextOutput("txt"),
plotOutput("plot"),
# If you care about listing the cache keys
uiOutput("keys")
)
)
)server <- function(input,
output,
session) {# Our plot, cached using the cache object and
# watching the nrow
output$plot <- renderCachedPlot(
{
showNotification(
h2("I'm computing the plot"),
type = "message"
)# Fake long computation
Sys.sleep(2)# Plot
plot(mtcars[1:input$nrow, ])
},
# We cache on the input$nrow
cacheKeyExpr = list(
input$nrow
),
# The cache object is used here
cache = cache_object
)rc <- reactive({
showNotification(
h2("I'm computing the reactive()"),
type = "message"
)
# Fake long computation
Sys.sleep(2)input$nrow * 100
}) %>%
# Using bindCache() require shiny > 1.6.0
bindCache(
input$nrow,
cache = cache_object
)output$txt <- renderText({
rc()
})keys <- reactive({
# Listing the keys
invalidateLater(500)
cache_object$keys()
})output$keys <- renderUI({
tags$ul(
lapply(keys(), tags$li)
)
})observeEvent(input$clear, {
# Sometime you might want to remove everything from the cache
cache_object$reset()showNotification(
h2("Cache reset"),
type = "message"
)
})
}shinyApp(ui, server)
}generate_app(mongo_cache)
```#### Flushing MongoDB cache using LRU
All keys registered to MongoDB comes with a `metadata.lastAccessed` parameter.
Using this parameter, you'll be able to flush old cache if needed.```{r eval = TRUE}
mongo <- mongolite::gridfs(
db = "bank",
url = "mongodb://bebop:aloula@localhost:27066",
prefix = "sn"
)
get_metadata <- function(mongo) {
purrr::map(mongo$find()$metadata, jsonlite::fromJSON)
}
Sys.sleep(10)
mf(5)
get_metadata(mongo)Sys.sleep(10)
mf(5)
get_metadata(mongo)
```### Redis
Launching a container with redis.
```{bash}
docker run --rm --name redisbank -d -p 6379:6379 redis:5.0.5 --requirepass bebopalula
``````{r echo = FALSE, eval = TRUE}
system("docker run --rm --name redisbank -d -p 6379:6379 redis:5.0.5 --requirepass bebopalula")
Sys.sleep(20)
```#### With `{memoise}`
```{r eval = TRUE}
# Create a redis cache.
# The arguments will be passed to redux::hiredis
redis_cache <- cache_redis$new(password = "bebopalula")f <- function(x) {
sample(1:1000, x)
}mf <- memoise(f, cache = redis_cache)
mf(5)
mf(5)
```#### Inside `{shiny}`
Here is a first simple application that shows you the basics :
```{r}
ui <- fluidPage(
# Creating a slider input that will be used as a cache key
sliderInput("nrow", "NROW", 1, 32, 32),
# Plotting a piece of mtcars
plotOutput("plot")
)server <- function(input, output, session) {
output$plot <- renderCachedPlot(
{
# Pretending this takes a long time
Sys.sleep(2)
plot(mtcars[1:input$nrow, ])
},
cacheKeyExpr = list(
# Defining the cache key
input$nrow
),
# Using our redis cache
cache = redis_cache
)
}
shinyApp(ui, server)
```For the larger app:
```{r}
generate_app(redis_cache)
```### Postgres
Launching a container with postgres.
```{bash}
docker run --rm --name some-postgres -e POSTGRES_PASSWORD=mysecretpassword -d -p 5433:5432 postgres
``````{r echo = FALSE, eval = TRUE}
system("docker run --rm --name postgresbank -e POSTGRES_PASSWORD=mysecretpassword -d -p 5433:5432 postgres")
Sys.sleep(20)
```#### With `{memoise}`
```{r eval = TRUE}
# Create a postgres cache.
# The arguments will be passed to DBI::dbConnect(RPostgres::Postgres(), ...)
postgres_cache <- cache_postgres$new(
dbname = "postgres",
host = "localhost",
port = 5433,
user = "postgres",
password = "mysecretpassword"
)f <- function(x) {
sample(1:1000, x)
}mf <- memoise(f, cache = postgres_cache)
mf(5)
mf(5)
```#### Inside `{shiny}`
Here is a first simple application that shows you the basics :
```{r}
ui <- fluidPage(
# Creating a slider input that will be used as a cache key
sliderInput("nrow", "NROW", 1, 32, 32),
# Plotting a piece of mtcars
plotOutput("plot")
)server <- function(input, output, session) {
output$plot <- renderCachedPlot(
{
# Pretending this takes a long time
Sys.sleep(2)
plot(mtcars[1:input$nrow, ])
},
cacheKeyExpr = list(
# Defining the cache key
input$nrow
),
# Using our postgres cache
cache = postgres_cache
)
}
shinyApp(ui, server)
```For the larger app:
```{r}
generate_app(postgres_cache)
```## Chosing a cache method
### Benchmark
As we are deporting the caching to an external DBMS, the query will of course be slower than using memory cache of disk cache.
But this difference in speed comes with a simpler scalability of the caching, as several instances of the app can rely on the same caching backend without the need to be on the same machine.```{r eval = TRUE}
library(magrittr)
library(bank)
big_iris <- purrr::rerun(100, iris) %>% data.table::rbindlist()nrow(big_iris)
pryr::object_size(big_iris)library(cachem)
mem_cache <- cache_mem()
disk_cache <- cache_disk()
mongo_cache <- cache_mongo$new(
db = "bank",
url = "mongodb://bebop:aloula@localhost:27066",
prefix = "sn"
)redis_cache <- cache_redis$new(password = "bebopalula")
postgres_cache <- cache_postgres$new(
dbname = "postgres",
host = "localhost",
port = 5433,
user = "postgres",
password = "mysecretpassword"
)bench::mark(
mem_cache = mem_cache$set("iris", big_iris),
disk_cache = disk_cache$set("iris", big_iris),
mongo_cache = mongo_cache$set("iris", big_iris),
redis_cache = redis_cache$set("iris", big_iris),
postgres_cache = postgres_cache$set("iris", big_iris),
check = FALSE,
iterations = 100
)bench::mark(
mem_cache = mem_cache$get("iris"),
disk_cache = disk_cache$get("iris"),
mongo_cache = mongo_cache$get("iris"),
redis_cache = redis_cache$get("iris"),
postgres_cache = postgres_cache$get("iris"),
iterations = 100
)
``````{bash}
docker stop mongobank redisbank postgresbank
``````{r echo = FALSE, eval = TRUE}
system("docker stop mongobank redisbank postgresbank")
```## You want another backend?
If you have any other backend in mind, feel free to open an issue here and we'll discuss the possibility of implementing it in `{bank}`.
## Code of Conduct
Please note that the bank project is released with a [Contributor Code of Conduct](https://contributor-covenant.org/version/2/0/CODE_OF_CONDUCT.html). By contributing to this project, you agree to abide by its terms.