I use redux::hiredis
over ssh port-forwarding for some projects, and my network link occasionally glitches and the connection object fails (for known reasons). redux
provides R$reconnect()
for this, I'm trying to find a way to automate reconnecting (since it is only a manual process).
The most direct way is to use something like this, everywhere:
R <- redux::hiredis()
res <- tryCatch(
R$GET("quux"),
error = function(e) {
if (grepl("Failure communicating", conditionMessage(e))) {
R$reconnect()
R$GET("quux")
} else stop(e)
})
but that gets a bit onerous when used in many places (some I don't have easy access to change).
One thought I had is to super-class it and add an $
-method for the new class. It takes some trickery to not be recursive, but something like this works, though it adds a PING
to each call:
redis_retry <- function(..., verbose = FALSE) {
R <- redux::hiredis(...)
class(R) <- c("redis_retry", class(R))
R
}
`$.redis_retry` <- function(x, val) {
val <- substitute(val)
ocls <- class(x)
on.exit(class(x) <- ocls, add = TRUE)
class(x) <- setdiff(class(x), "redis_retry")
res <- tryCatch(x$PING(), error = function(e) e)
if (inherits(res, "error")) {
if (grepl("Failure communicating with", conditionMessage(res))) {
x$reconnect()
res <- tryCatch(x$PING(), error = function(e) e)
if (inherits(res, "error")) stop(res)
return(x[[val]])
} else stop(res)
}
x[[val]]
}
R2 <- redis_retry()
R2$SET("quux", 42)
# [Redis: OK]
R2$GET("quux")
# [1] "42"
### disconnect ssh port-forwarding
R2$GET("quux")
# Error in redis_connect_tcp(config$host, config$port, config$timeout) :
# Failed to create context: Unknown error
### reconnect ssh port-forwarding
R2$GET("quux")
# [1] "42"
I'd like to be able to skip the need for x$PING()
internally. However, the intent of the $
-accessor is only to return the function, so the following ("quux")
is unknown to $
. For example, if I debug it,
Browse[1]> debug at #3: val <- substitute(val)
Browse[2]> match.call()
`$.redis_retry`(x = R2, val = GET)
Browse[2]> where
where 1: `$.redis_retry`(R2, GET)
where 2: R2$GET
Browse[2]> sys.calls()
[[1]]
R2$GET
[[2]]
`$.redis_retry`(R2, GET)
and I don't see an easy way to determine the rest of the expression.
Perhaps I'm being dim about this, is there an easy way to wrap the R2
object such that the original named-function is tried and, if it fails, $reconnect()
and try again?
Fake redis
If you don't have redux
or an instance of redis available, here's a fake version that (I think) adequately resembles the redis::hiredis
methods I'm demonstrating above:
fake_redis <- function() {
.counter <- 0L
env <- new.env(parent = emptyenv())
env$GET <- function(ign) {
if (.counter > 2) stop("counter is high")
.counter <<- .counter + 1L
"42"
}
env$reconnect <- function() .counter <<- 0L
env
}
fakeR <- fake_redis()
fakeR$GET("quux")
# [1] "42"
fakeR$GET("quux")
# [1] "42"
fakeR$GET("quux")
# [1] "42"
fakeR$GET("quux")
# Error in fakeR$GET("quux") : counter is high
fakeR$reconnect()
fakeR$GET("quux")
# [1] "42"