cache.rb |
|
|---|---|
Hash extensions |
|
|
Creates a new class, BareHash, that is alike a Hash in every way except that it may be accessed by a symbol or a string for every key. Really the same thing as HashWithIndifferentAccess but without ActiveSupport |
class BareHash < Hash
def [](key)
if self.include? key
self.fetch(key)
else
key.class == String ? self.fetch(key.to_sym,nil) : self.fetch(key.to_s, nil)
end
end
end |
|
Monkey patches Hash to allow for conversion to a BareHash where all values are strings. This is good for mixing results with Redis results which are always stored as strings. |
class Hash
def to_bare
bhash = BareHash.new
self.each{|k,v| bhash[k] = v}
bhash
end
end |
Caching |
module Silver
class Cache
attr_reader :key, :time_field, :query |
|
Creates a new cached search object. Silver does not connect to a database, the query is passed as a block. This means you can use Silver to cache databases, REST APIs, or anything else that can be queried. key is string to identify this and successive queries to Redis time_field is the name of a field used to determine whether or not there are new entries that are not yet cached. query is a block that take a date and queries the database for all entries after that date. The results must be returned in descending order. Example to prepare a cache and query for the database of new stories in blog #2:
|
def initialize(key,time_field,redis_options={},&query)
@key = key
@time_field = time_field
@query = query
@r = Redis.new(redis_options)
@r.select 12
end |
|
Queries Redis, returns new entries and inserts them into Redis. update is an optional parameter that, if false, will just return cached results and not look for new ones. callback is block that gets called for every new results, receives the result and returns the hash to be cached. This can used to query associations. Example to cache and the query the database and include any categories the entry might have:
|
def find(update=true,&callback)
old_results = @r.lrange(@key,0,-1).map{|q| JSON.parse(q)}
if update
last_date = @r.get("#{@key}:last") || "1970-01-01"
new_results = @query.call(DateTime.parse(last_date))
results = new_results.map do |result|
callback.call(result)
end
if results.empty?
final_results = old_results
else
write_new(results)
|
|
Why do we go back to Redis here instead of just merging old and new? Because it’s faster and cleaner than selectively determining which types are changed by the to_json (like Dates) and which are preservered (like Hashes). |
final_results = @r.lrange(@key,0,-1).map{|q| JSON.parse(q)}
end
else
final_results = old_results
end
final_results = final_results.map do |result|
result.to_bare
end
end |
|
A helper method to keep the redis list at a reasonable size. length is the number of entries to reduce the redis to |
def cull(length)
@r.ltrim(@key,0,length-1)
end
private |
|
Writes the results to redis by pushing them in reverse order on the head of the redis list. This ensures that the first result will always be the newest. Also turns every result hash into JSON before writing because Redis is string based. Find will automatically parse these JSON strings upon retrieval. |
def write_new(results)
new_date = results[0][@time_field].to_s
@r.set("#{@key}:last",new_date)
results.reverse.each do |result|
@r.lpush(@key,result.to_json)
end
end
end
end |