{"id":20665610,"url":"https://github.com/thinkr-open/bank","last_synced_at":"2025-04-19T16:39:10.684Z","repository":{"id":45256907,"uuid":"343847693","full_name":"ThinkR-open/bank","owner":"ThinkR-open","description":"Alternative caching backends for `{memoise}` \u0026 `{shiny}`.","archived":false,"fork":false,"pushed_at":"2023-03-27T13:24:24.000Z","size":75,"stargazers_count":12,"open_issues_count":4,"forks_count":2,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-03-29T10:12:24.737Z","etag":null,"topics":["cache","golemverse","shiny"],"latest_commit_sha":null,"homepage":"","language":"R","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ThinkR-open.png","metadata":{"files":{"readme":"README.Rmd","changelog":"NEWS.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-03-02T16:57:00.000Z","updated_at":"2024-05-21T03:08:13.000Z","dependencies_parsed_at":"2024-08-13T07:11:58.314Z","dependency_job_id":"d06e2359-4f28-47e3-86c2-a59416bf5e9f","html_url":"https://github.com/ThinkR-open/bank","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ThinkR-open%2Fbank","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ThinkR-open%2Fbank/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ThinkR-open%2Fbank/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ThinkR-open%2Fbank/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ThinkR-open","download_url":"https://codeload.github.com/ThinkR-open/bank/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249739834,"owners_count":21318669,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cache","golemverse","shiny"],"created_at":"2024-11-16T19:32:43.706Z","updated_at":"2025-04-19T16:39:10.656Z","avatar_url":"https://github.com/ThinkR-open.png","language":"R","funding_links":[],"categories":[],"sub_categories":[],"readme":"---\noutput: github_document\n---\n\n\u003c!-- README.md is generated from README.Rmd. Please edit that file --\u003e\n\n```{r, include = FALSE, error = TRUE}\nknitr::opts_chunk$set(\n  collapse = TRUE,\n  comment = \"#\u003e\",\n  fig.path = \"man/figures/README-\",\n  out.width = \"100%\",\n  eval = FALSE\n)\n\nsystem(\"docker stop mongobank redisbank postgresbank\")\n```\n\n# bank\n\n\u003c!-- badges: start --\u003e\n[![R-CMD-check](https://github.com/ThinkR-open/bank/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/ThinkR-open/bank/actions/workflows/R-CMD-check.yaml)\n[![Lifecycle: experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html#experimental)\n\u003c!-- badges: end --\u003e\n\nThe goal of `{bank}` is to provide alternative backends for caching with `{memoise}` \u0026 `{shiny}`.\n\n## Installation\n\n```{r}\n# install.packages(\"remotes\")\nremotes::install_github(\"thinkr-open/bank\")\n```\n\n## About\n\nYou're reading the doc about version : `r pkgload::pkg_version()`\n\nThis README has been compiled on the\n\n```{r, eval = TRUE}\nSys.time()\n```\n\nHere are the test \u0026 coverage results :\n\n```{r, eval = TRUE}\ndevtools::check(quiet = TRUE)\n```\n\n```{r echo = FALSE, eval = TRUE}\nunloadNamespace(\"shinipsum\")\n```\n\n```{r, eval = TRUE}\ncovr::package_coverage()\n```\n\n## Some things to know before starting\n\n### Caching scope\n\nWhen using `{bank}` backends with `{shiny}`, caching will done at the app-level, in other words the cache is stored across sessions.\nBe aware of this behavior if you have sensitive data inside your app, as this might imply data leakage.\n\nSee `?shiny::bindCache`\n\n\u003e 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.\n\n### Cache flushing\n\nAs with any `{cachem}` compatible objects, the cache can be manually flushed using the `$reset()` method -- this will call `drop()` on MongoDb, `FLUSHALL` in Redis, \u0026 `DBI::dbRemoveTable()` + `DBI::dbCreateTable()` with Postgres.\n\n```{r}\nlibrary(bank)\nmongo_cache \u003c- cache_mongo$new(\n  db = \"bank\",\n  url = \"mongodb://localhost:27066\",\n  prefix = \"sn\"\n)\n\nmongo_cache$reset()\n```\n\n\nAs `{bank}` relies on external backends, it's probably better to let the DBMS handle the flushing of cache.\nFor example, in `redis.conf`, you can set :\n\n```\nmaxmemory 2mb\nmaxmemory-policy allkeys-lru\n```\n\nLRU (least recently used) will allow redis to flush the key based on when they were used.\nSee \u003chttps://redis.io/topics/lru-cache\u003e.\n\nMongoDB 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.\n\n`{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.\n\n### Postgre limitation\n\nPostgre `bytea` column can only store up to 1GB elements, so you can't write a cache that's \u003e 1GB.\n\n## Backends\n\nNote that if you want to use `{bank}` in a `{shiny}` app:\n\n- `renderCachedPlot()` require `{shiny}` version 1.5.0 or higher\n\n- `bindCache()` require `{shiny}` version 1.6.0 or higher\n\nFor now, the following backends are supported:\n\n+ [MongoDB](#mongo)\n\n+ [Redis](#redis)\n\n+ [Postgre](#postgres)\n\n### Mongo\n\nLaunching a container with mongo.\n\n```{bash}\ndocker run --rm --name mongobank -d -p 27066:27017 -e MONGO_INITDB_ROOT_USERNAME=bebop -e MONGO_INITDB_ROOT_PASSWORD=aloula mongo:4\n```\n\n```{r echo = FALSE, eval = TRUE}\nsystem(\"docker run --rm --name mongobank -d -p 27066:27017 -e MONGO_INITDB_ROOT_USERNAME=bebop -e MONGO_INITDB_ROOT_PASSWORD=aloula mongo:4\")\nSys.sleep(20)\n```\n\n\n#### With `{memoise}`\n\nFirst, the `cache_mongo` can be used\n\n```{r eval = TRUE}\nlibrary(memoise)\nlibrary(bank)\n# Create a mongo cache.\n# The arguments will be passed to mongo::gridfs\nmongo_cache \u003c- cache_mongo$new(\n  db = \"bank\",\n  url = \"mongodb://bebop:aloula@localhost:27066\",\n  prefix = \"sn\"\n)\n\nf \u003c- function(x) {\n  sample(1:1000, x)\n}\n\nmf \u003c- memoise(f, cache = mongo_cache)\nmf(5)\nmf(5)\n```\n\n#### Inside `{shiny}`\n\nHere is a first simple application that shows you the basics :\n\n```{r}\nlibrary(shiny)\nui \u003c- fluidPage(\n  # Creating a slider input that will be used as a cache key\n  sliderInput(\"nrow\", \"NROW\", 1, 32, 32),\n  # Plotting a piece of mtcars\n  plotOutput(\"plot\")\n)\n\nserver \u003c- function(input, output, session) {\n  output$plot \u003c- renderCachedPlot(\n    {\n      # Pretending this takes a long time\n      Sys.sleep(2)\n      plot(mtcars[1:input$nrow, ])\n    },\n    cacheKeyExpr = list(\n      # Defining the cache key\n      input$nrow\n    ),\n    # Using our mongo cache\n    cache = mongo_cache\n  )\n}\nshinyApp(ui, server)\n```\n\nAs you can see, the first time you set the slider to a given value, it takes a little bit to compute.\nThen it's almost instantaneous.\n\nLet's try a more complex application:\n\n\n```{r}\n# We'll put everything in a function so that it can later be reused with other backends\nlibrary(magrittr)\n\ngenerate_app \u003c- function(cache_object) {\n  ui \u003c- fluidPage(\n    h1(\n      sprintf(\n        \"Caching in an external DB using %s\",\n        deparse(\n          substitute(cache_object)\n        )\n      )\n    ),\n    sidebarLayout(\n      sidebarPanel = sidebarPanel(\n        # This sliderInput will be the cache key\n        # i.e we don't want to recompute the plot everytime\n        sliderInput(\"nrow\", \"Nrow\", 1, 32, 32),\n        # Allow to clear the cache\n        actionButton(\"clear\", \"Clear Cache\")\n      ),\n      mainPanel = mainPanel(\n        # Outputing the reactive and a plot\n        verbatimTextOutput(\"txt\"),\n        plotOutput(\"plot\"),\n        # If you care about listing the cache keys\n        uiOutput(\"keys\")\n      )\n    )\n  )\n\n  server \u003c- function(input,\n                     output,\n                     session) {\n\n    # Our plot, cached using the cache object and\n    # watching the nrow\n    output$plot \u003c- renderCachedPlot(\n      {\n        showNotification(\n          h2(\"I'm computing the plot\"),\n          type = \"message\"\n        )\n\n        # Fake long computation\n        Sys.sleep(2)\n\n        # Plot\n        plot(mtcars[1:input$nrow, ])\n      },\n      # We cache on the input$nrow\n      cacheKeyExpr = list(\n        input$nrow\n      ),\n      # The cache object is used here\n      cache = cache_object\n    )\n\n    rc \u003c- reactive({\n      showNotification(\n        h2(\"I'm computing the reactive()\"),\n        type = \"message\"\n      )\n      # Fake long computation\n      Sys.sleep(2)\n\n      input$nrow * 100\n    }) %\u003e%\n      # Using bindCache() require shiny \u003e 1.6.0\n      bindCache(\n        input$nrow,\n        cache = cache_object\n      )\n\n    output$txt \u003c- renderText({\n      rc()\n    })\n\n    keys \u003c- reactive({\n      # Listing the keys\n      invalidateLater(500)\n      cache_object$keys()\n    })\n\n    output$keys \u003c- renderUI({\n      tags$ul(\n        lapply(keys(), tags$li)\n      )\n    })\n\n    observeEvent(input$clear, {\n      # Sometime you might want to remove everything from the cache\n      cache_object$reset()\n\n      showNotification(\n        h2(\"Cache reset\"),\n        type = \"message\"\n      )\n    })\n  }\n\n  shinyApp(ui, server)\n}\n\ngenerate_app(mongo_cache)\n```\n\n#### Flushing MongoDB cache using LRU\n\nAll keys registered to MongoDB comes with a `metadata.lastAccessed` parameter.\nUsing this parameter, you'll be able to flush old cache if needed.\n\n```{r eval = TRUE}\nmongo \u003c- mongolite::gridfs(\n  db = \"bank\",\n  url = \"mongodb://bebop:aloula@localhost:27066\",\n  prefix = \"sn\"\n)\nget_metadata \u003c- function(mongo) {\n  purrr::map(mongo$find()$metadata, jsonlite::fromJSON)\n}\nSys.sleep(10)\nmf(5)\nget_metadata(mongo)\n\nSys.sleep(10)\nmf(5)\nget_metadata(mongo)\n```\n\n\n### Redis\n\nLaunching a container with redis.\n\n```{bash}\ndocker run --rm --name redisbank -d -p 6379:6379 redis:5.0.5 --requirepass bebopalula\n```\n\n```{r echo = FALSE, eval = TRUE}\nsystem(\"docker run --rm --name redisbank -d -p 6379:6379 redis:5.0.5 --requirepass bebopalula\")\nSys.sleep(20)\n```\n\n\n#### With `{memoise}`\n\n```{r eval = TRUE}\n# Create a redis cache.\n# The arguments will be passed to redux::hiredis\nredis_cache \u003c- cache_redis$new(password = \"bebopalula\")\n\nf \u003c- function(x) {\n  sample(1:1000, x)\n}\n\nmf \u003c- memoise(f, cache = redis_cache)\nmf(5)\nmf(5)\n```\n\n#### Inside `{shiny}`\n\nHere is a first simple application that shows you the basics :\n\n```{r}\nui \u003c- fluidPage(\n  # Creating a slider input that will be used as a cache key\n  sliderInput(\"nrow\", \"NROW\", 1, 32, 32),\n  # Plotting a piece of mtcars\n  plotOutput(\"plot\")\n)\n\nserver \u003c- function(input, output, session) {\n  output$plot \u003c- renderCachedPlot(\n    {\n      # Pretending this takes a long time\n      Sys.sleep(2)\n      plot(mtcars[1:input$nrow, ])\n    },\n    cacheKeyExpr = list(\n      # Defining the cache key\n      input$nrow\n    ),\n    # Using our redis cache\n    cache = redis_cache\n  )\n}\nshinyApp(ui, server)\n```\n\nFor the larger app:\n\n```{r}\ngenerate_app(redis_cache)\n```\n\n### Postgres\n\nLaunching a container with postgres.\n\n```{bash}\ndocker run --rm --name some-postgres -e POSTGRES_PASSWORD=mysecretpassword -d -p 5433:5432 postgres\n```\n\n```{r echo = FALSE, eval = TRUE}\nsystem(\"docker run --rm --name postgresbank -e POSTGRES_PASSWORD=mysecretpassword -d -p 5433:5432 postgres\")\nSys.sleep(20)\n```\n\n#### With `{memoise}`\n\n```{r eval = TRUE}\n# Create a postgres cache.\n# The arguments will be passed to DBI::dbConnect(RPostgres::Postgres(), ...)\npostgres_cache \u003c- cache_postgres$new(\n  dbname = \"postgres\",\n  host = \"localhost\",\n  port = 5433,\n  user = \"postgres\",\n  password = \"mysecretpassword\"\n)\n\nf \u003c- function(x) {\n  sample(1:1000, x)\n}\n\nmf \u003c- memoise(f, cache = postgres_cache)\nmf(5)\nmf(5)\n```\n\n#### Inside `{shiny}`\n\nHere is a first simple application that shows you the basics :\n\n```{r}\nui \u003c- fluidPage(\n  # Creating a slider input that will be used as a cache key\n  sliderInput(\"nrow\", \"NROW\", 1, 32, 32),\n  # Plotting a piece of mtcars\n  plotOutput(\"plot\")\n)\n\nserver \u003c- function(input, output, session) {\n  output$plot \u003c- renderCachedPlot(\n    {\n      # Pretending this takes a long time\n      Sys.sleep(2)\n      plot(mtcars[1:input$nrow, ])\n    },\n    cacheKeyExpr = list(\n      # Defining the cache key\n      input$nrow\n    ),\n    # Using our postgres cache\n    cache = postgres_cache\n  )\n}\nshinyApp(ui, server)\n```\n\nFor the larger app:\n\n```{r}\ngenerate_app(postgres_cache)\n```\n\n\n## Chosing a cache method\n\n### Benchmark\n\nAs we are deporting the caching to an external DBMS, the query will of course be slower than using memory cache of disk cache.\nBut 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.\n\n```{r eval = TRUE}\nlibrary(magrittr)\nlibrary(bank)\nbig_iris \u003c- purrr::rerun(100, iris) %\u003e% data.table::rbindlist()\n\nnrow(big_iris)\npryr::object_size(big_iris)\n\nlibrary(cachem)\n\nmem_cache \u003c- cache_mem()\n\ndisk_cache \u003c- cache_disk()\n\nmongo_cache \u003c- cache_mongo$new(\n  db = \"bank\",\n  url = \"mongodb://bebop:aloula@localhost:27066\",\n  prefix = \"sn\"\n)\n\nredis_cache \u003c- cache_redis$new(password = \"bebopalula\")\n\npostgres_cache \u003c- cache_postgres$new(\n  dbname = \"postgres\",\n  host = \"localhost\",\n  port = 5433,\n  user = \"postgres\",\n  password = \"mysecretpassword\"\n)\n\nbench::mark(\n  mem_cache = mem_cache$set(\"iris\", big_iris),\n  disk_cache = disk_cache$set(\"iris\", big_iris),\n  mongo_cache = mongo_cache$set(\"iris\", big_iris),\n  redis_cache = redis_cache$set(\"iris\", big_iris),\n  postgres_cache = postgres_cache$set(\"iris\", big_iris),\n  check = FALSE,\n  iterations = 100\n)\n\nbench::mark(\n  mem_cache = mem_cache$get(\"iris\"),\n  disk_cache = disk_cache$get(\"iris\"),\n  mongo_cache = mongo_cache$get(\"iris\"),\n  redis_cache = redis_cache$get(\"iris\"),\n  postgres_cache = postgres_cache$get(\"iris\"),\n  iterations = 100\n)\n```\n\n```{bash}\ndocker stop mongobank redisbank postgresbank\n```\n\n```{r echo = FALSE, eval = TRUE}\nsystem(\"docker stop mongobank redisbank postgresbank\")\n```\n\n## You want another backend?\n\nIf 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}`.\n\n## Code of Conduct\n\nPlease 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.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthinkr-open%2Fbank","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fthinkr-open%2Fbank","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthinkr-open%2Fbank/lists"}