blob: 0a57401b1e39025d9a7f8f1d5f06c21281ea9c88 [file] [log] [blame]
/* Copyright (C) 2011 SpringSource
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package grails.plugin.redis
import com.google.gson.Gson
import grails.util.Holders
import org.codehaus.groovy.grails.commons.GrailsApplication
import redis.clients.jedis.Jedis
import redis.clients.jedis.Pipeline
import redis.clients.jedis.Transaction
import redis.clients.jedis.exceptions.JedisConnectionException
class RedisService {
public static final int NO_EXPIRATION_TTL = -1
public static final int KEY_DOES_NOT_EXIST = -2 // added in redis 2.8
def redisPool
GrailsApplication grailsApplication = Holders.grailsApplication
boolean transactional = false
RedisService withConnection(String connectionName){
if(grailsApplication.mainContext.containsBean("redisService${connectionName.capitalize()}")){
return (RedisService)grailsApplication.mainContext.getBean("redisService${connectionName.capitalize()}")
}
if (log.errorEnabled) log.error("Connection with name redisService${connectionName.capitalize()} could not be found, returning default redis instead")
return this
}
def withPipeline(Closure closure, Boolean returnAll=false) {
withRedis { Jedis redis ->
Pipeline pipeline = redis.pipelined()
closure(pipeline)
returnAll ? pipeline.syncAndReturnAll() : pipeline.sync()
}
}
def withOptionalPipeline(Closure clos, Boolean returnAll = false) {
withOptionalRedis { Jedis redis ->
if (redis) {
Pipeline pipeline = redis.pipelined()
clos(pipeline)
returnAll ? pipeline.syncAndReturnAll() : pipeline.sync()
}
else {
return clos()
}
}
}
def withTransaction(Closure closure) {
withRedis { Jedis redis ->
Transaction transaction = redis.multi()
try {
closure(transaction)
transaction.exec()
} catch(Exception exception) {
transaction.discard()
throw exception
}
}
}
def methodMissing(String name, args) {
if (log.debugEnabled) log.debug "methodMissing $name"
withRedis { Jedis redis ->
redis.invokeMethod(name, args)
}
}
void propertyMissing(String name, Object value) {
withRedis { Jedis redis ->
redis.set(name, value.toString())
}
}
Object propertyMissing(String name) {
withRedis { Jedis redis ->
redis.get(name)
}
}
def withRedis(Closure closure) {
Jedis redis = redisPool.resource
try {
def ret = closure(redis)
redisPool.returnResource(redis)
return ret
} catch(JedisConnectionException jce) {
redisPool.returnBrokenResource(redis)
throw jce
} catch(Exception e) {
redisPool.returnResource(redis)
throw e
}
}
/**
* An implementation of withRedis that suppresses JedisConnectException to support the memoization model
* @param clos
* @return
*/
def withOptionalRedis(Closure clos) {
Jedis redis
try {
redis = redisPool.resource
}
catch (JedisConnectionException jce) {
log.info('Unreachable redis store trying to retrieve redis resource. Please check redis server and/or config!')
}
try {
def ret = clos(redis)
if (redis) redisPool.returnResource(redis)
ret
}
catch (JedisConnectionException jce) {
log.error('Unreachable redis store trying to return redis pool resource. Please check redis server and/or config!', jce)
if (redis) redisPool.returnBrokenResource(redis)
}
catch (Throwable t) {
if (redis) redisPool.returnResource(redis)
throw t
}
}
def memoize(String key, Integer expire, Closure closure) {
memoize(key, [expire: expire], closure)
}
// SET/GET a value on a Redis key
def memoize(String key, Map options = [:], Closure closure) {
if (log.debugEnabled) log.debug "using key $key"
def result = withOptionalRedis { Jedis redis ->
if (redis) return redis.get(key)
}
if(!result) {
if (log.debugEnabled) log.debug "cache miss: $key"
result = closure()
if(result) withOptionalRedis { Jedis redis ->
if (redis) {
if(options?.expire) {
redis.setex(key, options.expire, result as String)
} else {
redis.set(key, result as String)
}
}
}
} else {
if (log.debugEnabled) log.debug "cache hit : $key = $result"
}
result
}
def memoizeHash(String key, Integer expire, Closure closure) {
memoizeHash(key, [expire: expire], closure)
}
def memoizeHash(String key, Map options = [:], Closure closure) {
def hash = withOptionalRedis { Jedis redis ->
if (redis) return redis.hgetAll(key)
}
if(!hash) {
if (log.debugEnabled) log.debug "cache miss: $key"
hash = closure()
if(hash) withOptionalRedis { Jedis redis ->
if (redis) {
redis.hmset(key, hash)
if(options?.expire) redis.expire(key, options.expire)
}
}
} else {
if (log.debugEnabled) log.debug "cache hit : $key = $hash"
}
hash
}
def memoizeHashField(String key, String field, Integer expire, Closure closure) {
memoizeHashField(key, field, [expire: expire], closure)
}
// HSET/HGET a value on a Redis hash at key.field
// if expire is not null it will be the expire for the whole hash, not this value
// and will only be set if there isn't already a TTL on the hash
def memoizeHashField(String key, String field, Map options = [:], Closure closure) {
def result = withOptionalRedis { Jedis redis ->
if (redis) return redis.hget(key, field)
}
if(!result) {
if (log.debugEnabled) log.debug "cache miss: $key.$field"
result = closure()
if(result) withOptionalRedis { Jedis redis ->
if (redis) {
redis.hset(key, field, result as String)
if(options?.expire && redis.ttl(key) == NO_EXPIRATION_TTL) redis.expire(key, options.expire)
}
}
} else {
if (log.debugEnabled) log.debug "cache hit : $key.$field = $result"
}
result
}
def memoizeScore(String key, String member, Integer expire, Closure closure) {
memoizeScore(key, member, [expire: expire], closure)
}
// set/get a 'double' score within a sorted set
// if expire is not null it will be the expire for the whole zset, not this value
// and will only be set if there isn't already a TTL on the zset
def memoizeScore(String key, String member, Map options = [:], Closure closure) {
def score = withOptionalRedis { Jedis redis ->
if (redis) redis.zscore(key, member)
}
if(!score) {
if (log.debugEnabled) log.debug "cache miss: $key.$member"
score = closure()
if(score) withOptionalRedis { Jedis redis ->
if (redis) {
redis.zadd(key, score, member)
if(options?.expire && redis.ttl(key) == NO_EXPIRATION_TTL) redis.expire(key, options.expire)
}
}
} else {
if (log.debugEnabled) log.debug "cache hit : $key.$member = $score"
}
score
}
List memoizeDomainList(Class domainClass, String key, Integer expire, Closure closure) {
memoizeDomainList(domainClass, key, [expire: expire], closure)
}
List memoizeDomainList(Class domainClass, String key, Map options = [:], Closure closure) {
List<Long> idList = getIdListFor(key)
if(idList) return hydrateDomainObjectsFrom(domainClass, idList)
def domainList = withOptionalRedis { Jedis redis ->
closure(redis)
}
saveIdListTo(key, domainList, options.expire)
domainList
}
List<Long> memoizeDomainIdList(Class domainClass, String key, Integer expire, Closure closure) {
memoizeDomainIdList(domainClass, key, [expire: expire], closure)
}
// used when we just want the list of Ids back rather than hydrated objects
List<Long> memoizeDomainIdList(Class domainClass, String key, Map options = [:], Closure closure) {
List<Long> idList = getIdListFor(key)
if(idList) return idList
def domainList = closure()
saveIdListTo(key, domainList, options.expire)
getIdListFor(key)
}
protected List<Long> getIdListFor(String key) {
List<String> idList = withOptionalRedis { Jedis redis ->
if (redis) return redis.lrange(key, 0, -1)
}
if(idList) {
if (log.debugEnabled) log.debug "$key cache hit, returning ${idList.size()} ids"
List<Long> idLongList = idList*.toLong()
return idLongList
}
}
protected void saveIdListTo(String key, List domainList, Integer expire = null) {
if (log.debugEnabled) log.debug "$key cache miss, memoizing ${domainList?.size() ?: 0} ids"
withOptionalPipeline { pipeline ->
if (pipeline) {
for(domain in domainList) {
pipeline.rpush(key, domain.id as String)
}
if(expire) pipeline.expire(key, expire)
}
}
}
protected List hydrateDomainObjectsFrom(Class domainClass, List<Long> idList) {
if(domainClass && idList) {
//return domainClass.findAllByIdInList(idList, [cache: true])
return idList.collect { id -> domainClass.load(id) }
}
[]
}
def memoizeDomainObject(Class domainClass, String key, Integer expire, Closure closure) {
memoizeDomainObject(domainClass, key, [expire: expire], closure)
}
// closure can return either a domain object or a Long id of a domain object
// it will be persisted into redis as the Long
def memoizeDomainObject(Class domainClass, String key, Map options = [:], Closure closure) {
Long domainId = withOptionalRedis { redis ->
redis?.get(key)?.toLong()
}
if(!domainId) domainId = persistDomainId(closure()?.id as Long, key, options.expire)
domainClass.load(domainId)
}
Long persistDomainId(Long domainId, String key, Integer expire) {
if(domainId) {
withOptionalPipeline { pipeline ->
if (pipeline) {
pipeline.set(key, domainId.toString())
if(expire) pipeline.expire(key, expire)
}
}
}
domainId
}
def memoizeObject(Class clazz, String key, Integer expire, Closure closure) {
memoizeObject(clazz, key, [expire: expire], closure)
}
def memoizeObject(Class clazz, String key, Map options = [:], Closure closure) {
Gson gson = new Gson()
String memoizedJson = memoize(key, options) { ->
def original = closure()
if (original == null && options.cacheNull == false) return null
gson.toJson(original)
}
gson.fromJson((String)memoizedJson, clazz)
}
// deletes all keys matching a pattern (see redis "keys" documentation for more)
// OK for low traffic methods, but expensive compared to other redis commands
// perf test before relying on this rather than storing your own set of keys to
// delete
void deleteKeysWithPattern(keyPattern) {
if (log.infoEnabled) log.info("Cleaning all redis keys with pattern [${keyPattern}]")
withRedis { Jedis redis ->
String[] keys = redis.keys(keyPattern)
if(keys) redis.del(keys)
}
}
def memoizeList(String key, Integer expire, Closure closure) {
memoizeList(key, [expire: expire], closure)
}
def memoizeList(String key, Map options = [:], Closure closure) {
List list = withOptionalRedis { Jedis redis ->
if (redis) return redis.lrange(key, 0, -1)
}
if(!list) {
if (log.debugEnabled) log.debug "cache miss: $key"
list = closure()
if(list) withOptionalPipeline { pipeline ->
if (pipeline) {
for(obj in list) { pipeline.rpush(key, obj) }
if(options?.expire) pipeline.expire(key, options.expire)
}
}
} else {
if (log.debugEnabled) log.debug "cach hit: $key"
}
list
}
def memoizeSet(String key, Integer expire, Closure closure) {
memoizeSet(key, [expire: expire], closure)
}
def memoizeSet(String key, Map options = [:], Closure closure) {
def set = withOptionalRedis { Jedis redis ->
if (redis) return redis.smembers(key)
}
if(!set) {
if (log.debugEnabled) log.debug "cache miss: $key"
set = closure()
if(set) withOptionalPipeline { pipeline ->
if (pipeline) {
for(obj in set) { pipeline.sadd(key, obj) }
if(options?.expire) pipeline.expire(key, options.expire)
}
}
} else {
if (log.debugEnabled) log.debug "cache hit: $key"
}
set
}
// should ONLY Be used from tests unless we have a really good reason to clear out the entire redis db
def flushDB() {
if (log.warnEnabled) log.warn('flushDB called!')
withRedis { Jedis redis ->
redis.flushDB()
}
}
}