library(storr)
storr
provides very simple key/value stores for R. They attempt to provide the most basic set of key/value lookup functionality that is completely consistent across a range of different underlying storage drivers (in memory storage, filesystem and proper database). All the storage is content addressable, so keys map onto hashes and hashes map onto data.
The rds
driver stores contents at some path by saving out to rds files. Here I’m using a temporary directory for the path; the driver will create a number of subdirectories here.
path <- tempfile("storr_")
st <- storr::storr_rds(path)
Alternatively you can create the driver explicitly:
dr <- storr::driver_rds(path)
With this driver object we can create the storr
object which is what we actually interact with:
st <- storr::storr(dr)
The main way of interacting with a storr
object is get
/set
/del
for getting, setting and deleting data stored at some key. To store data:
st$set("mykey", mtcars)
To get the data back
head(st$get("mykey"))
## mpg cyl disp hp drat wt qsec vs am gear carb
## Mazda RX4 21.0 6 160 110 3.90 2.620 16.46 0 1 4 4
## Mazda RX4 Wag 21.0 6 160 110 3.90 2.875 17.02 0 1 4 4
## Datsun 710 22.8 4 108 93 3.85 2.320 18.61 1 1 4 1
## Hornet 4 Drive 21.4 6 258 110 3.08 3.215 19.44 1 0 3 1
## Hornet Sportabout 18.7 8 360 175 3.15 3.440 17.02 0 0 3 2
## Valiant 18.1 6 225 105 2.76 3.460 20.22 1 0 3 1
What is in the storr
?
st$list()
## [1] "mykey"
Or, much faster, test for existance of a particular key:
st$exists("mykey")
## [1] TRUE
st$exists("another_key")
## [1] FALSE
To delete a key:
st$del("mykey")
It’s gone!
st$list()
## character(0)
though the actual data is still stored in the database:
st$list_hashes()
## [1] "a63c70e73b58d0823ab3bcbd3b543d6f"
head(st$get_value(digest::digest(mtcars)))
## mpg cyl disp hp drat wt qsec vs am gear carb
## Mazda RX4 21.0 6 160 110 3.90 2.620 16.46 0 1 4 4
## Mazda RX4 Wag 21.0 6 160 110 3.90 2.875 17.02 0 1 4 4
## Datsun 710 22.8 4 108 93 3.85 2.320 18.61 1 1 4 1
## Hornet 4 Drive 21.4 6 258 110 3.08 3.215 19.44 1 0 3 1
## Hornet Sportabout 18.7 8 360 175 3.15 3.440 17.02 0 0 3 2
## Valiant 18.1 6 225 105 2.76 3.460 20.22 1 0 3 1
though now that there are no keys pointing at the data is is subject to garbage collection:
del <- st$gc()
del
## [1] "a63c70e73b58d0823ab3bcbd3b543d6f"
st$list_hashes()
## character(0)
Objects can be imported in and exported out of a storr
;
Import from a list, environment or another storr
st$import(list(a=1, b=2))
## [1] "a" "b"
st$list()
## [1] "a" "b"
st$get("a")
## [1] 1
Export to an environment (or another storr
)
e <- st$export(new.env(parent=emptyenv()))
ls(e)
## [1] "a" "b"
e$a
## [1] 1
st_copy <- st$export(storr_environment())
st_copy$list()
## [1] "a" "b"
st$get("a")
## [1] 1
st2 <- storr::storr(driver=storr::driver_rds(tempfile("storr_")))
st2$list()
## character(0)
st2$import(st)
## [1] "a" "b"
st2$list()
## [1] "a" "b"
driver_environment
) - mostly for debugging and transient storage, but by far the fastest.driver_rds
) - zero dependencies, quite fast, will suffer under high concurrency because there is no file locking.driver_redis
) - uses redux
to store the data in a Redis (http://redis.io
) database. About the same speed as rds (faster write, slower read at present), but can allow multiple R processes to share the same set of objects.driver_rlite
) - stores data in an rlite database using rrlite
. This is the slowest at present but does also support concurrency. But rlite has the potential to be as useful as SQLite is so this will improve.storr
includes a few useful features that are common to all drivers.
The only thing that is stored against a key is the hash of some object. Each driver does this a different way, but for the rds driver it stores small text files that list the hash in them. So:
dir(file.path(path, "keys", "objects"))
## [1] "a" "b"
readLines(file.path(path, "keys", "objects", "a"))
## [1] "6717f2823d3202449301145073ab8719"
st$get_hash("a")
## [1] "6717f2823d3202449301145073ab8719"
Then there is one big pool of hash / value pairs:
st$list_hashes()
## [1] "6717f2823d3202449301145073ab8719" "db8e490a925a60e62212cefc7674ca02"
in the rds driver these are stored like so:
dir(file.path(path, "data"))
## [1] "6717f2823d3202449301145073ab8719.rds"
## [2] "db8e490a925a60e62212cefc7674ca02.rds"
Every time data passes across a get
or set
method, storr
stores the data in an environment within the storr
object. Because we store the content against its hash, it’s always in sync with what is saved to disk. That means that the look up process goes like this:
Because looking up data in the environment is likely to be orders of magnitide faster than reading from disks or databases, this means that commonly accessed data will be accessed at a similar speed to native R objects, while still immediately reflecting changes to the content (because that would mean the hash changes)
To demonstrate:
st <- storr::storr(driver=storr::driver_rds(tempfile("storr_")))
This is the caching environent; currently empty
ls(st$envir)
## character(0)
Set some key to some data:
set.seed(2)
st$set("mykey", runif(100))
The environment now includes an object with a name that is the same as the hash of its contents:
ls(st$envir)
## [1] "3386dd0f1a8a3fe4ed209420ea23c8eb"
Extract the object from the environment and hash it
storr:::hash_object(st$envir[[ls(st$envir)]])
## [1] "3386dd0f1a8a3fe4ed209420ea23c8eb"
When we look up the value stored against key mykey
, the first step is to check the key/hash map; this returns the key above (this step does involve reading from disk)
st$get_hash("mykey")
## [1] "3386dd0f1a8a3fe4ed209420ea23c8eb"
It then calls $get_value
to extract the value associated with that hash - the first thing that function does is try to locate the hash in the environment, otherwise it reads the data from wherever the driver stores it.
st$get_value
## function (hash, use_cache = TRUE)
## {
## envir <- self$envir
## if (use_cache && exists0(hash, envir)) {
## value <- envir[[hash]]
## }
## else {
## if (self$traits$throw_missing) {
## value <- tryCatch(self$driver$get_object(hash), error = function(e) stop(HashError(hash)))
## }
## else {
## if (!self$driver$exists_object(hash)) {
## stop(HashError(hash))
## }
## value <- self$driver$get_object(hash)
## }
## if (use_cache) {
## envir[[hash]] <- value
## }
## }
## value
## }
## <environment: 0x555ea721d718>
The speed up is going to be fairly context dependent, but 10x seems pretty good in this case (some of the overhead is simply a longer code path as we call out to the driver).
hash <- st$get_hash("mykey")
microbenchmark::microbenchmark(st$get_value(hash, use_cache=TRUE),
st$get_value(hash, use_cache=FALSE))
## Unit: microseconds
## expr min lq mean median
## st$get_value(hash, use_cache = TRUE) 4.371 5.1445 6.20214 6.317
## st$get_value(hash, use_cache = FALSE) 51.581 52.8435 55.84353 53.866
## uq max neval cld
## 6.6905 28.112 100 a
## 55.2065 142.021 100 b
storr uses R’s exception handling system and errors inspired from Python to make it easy to program with tryCatch
.
If a key is not in the database, storr will return a KeyError
(not NULL
because storing a NULL
value is a perfectly reasonable thing to do).
If you did want to return NULL
when a key is requested but not present, use tryCatch in this way:
tryCatch(st$get("no_such_key"),
KeyError=function(e) NULL)
## NULL
See ?tryCatch
for details. The idea is that key lookup errors will have the class KeyError
so will be caught here and run the given function (the argument e
is the actual error object). Other errors will not be caught and will still throw.
HashErrors
will be rarer, but could happen (they might occur if your driver supports expiry of objects). We can simulate that by setting a hash and deleting it:
st$set("foo", letters)
ok <- st$driver$del_object(st$get_hash("foo"))
st$flush_cache()
tryCatch(st$get("foo"),
KeyError=function(e) NULL,
HashError=function(e) message("Data is deleted"))
## Data is deleted
Here the HashError
is triggered.
KeyError
objects include key
and namespace
elements, HashError
objects include a hash
element. They both inherit from c("error", "condition")
.
Finally, when using an external storr (see ?driver_external) storr will throw a KeyErrorExternal
if the fetch_hook
function errors while trying to retrieve an external resource.