Skip to content

Instantly share code, notes, and snippets.

@rpkraemer
Created September 7, 2012 15:33
Show Gist options
  • Select an option

  • Save rpkraemer/3667203 to your computer and use it in GitHub Desktop.

Select an option

Save rpkraemer/3667203 to your computer and use it in GitHub Desktop.
Implementing a simple key/value cache with TDD
# DSL purposes :D
class Fixnum
def item
self
end
def items
self
end
# Time utils
def seconds
Time.now + self
end
def second
seconds
end
def minutes
Time.now + self * 60
end
def minute
minutes
end
end
module Cache
class Store
attr_reader :data, :default_expiration
attr_accessor :limit
def initialize(options = {})
@data = Hash.new
@limit = options[:limit] || 10.items
@default_expiration = options[:default_expiration] || 5.minutes
end
def set(*content)
set_multiple_data(content) if content.size == 1 # Hash
set_single_data(content) if content.size == 2 # key/value
set_single_data(content, content.last) if content.size == 3 and content.last.is_a? Time # key/value + valid expiration time
remove_less_used_cache if cache_full?
end
def get(key)
if @data.has_key? key
if data_expired? key
@data.delete key
nil
elsif
@data[key][:hits] += 1
@data[key][:value]
end
end
end
private
def set_single_data(content, expiration = nil)
@data[content.first] = {:value => content[1], :hits => 0, :expiration => expiration || @default_expiration}
end
def set_multiple_data(content)
content.each do |kv|
kv.each do |k,v|
if v.is_a? Hash
if v.has_key?(:data) and v.has_key?(:expiration) and v[:expiration].is_a? Time
@data[k] = {:value => v[:data], :hits => 0, :expiration => v[:expiration]}
end
elsif v.is_a? Array
if v.size == 2 and v.last.is_a? Time
@data[k] = {:value => v.first, :hits => 0, :expiration => v.last}
end
else
@data[k] = {:value => v, :hits => 0, :expiration => @default_expiration}
end
end
end
end
def cache_full?
@data.size > @limit
end
def data_expired?(key)
Time.now > @data[key][:expiration]
end
def remove_less_used_cache
less_used_hits = @data.values[0][:hits]
@data.each_value {|v| less_used_hits = v[:hits] if v[:hits] < less_used_hits}
@data.delete_if {|k,v| v[:hits] == less_used_hits && cache_full?}
end
end
end
=begin
Atualizando o Cache
Um cache de atualização é um cache de memória de baixo consumo de recursos.
É usado quando a memória é escassa e os valores do dicionário mudam a cada pouco tempo.
O cache contém dados chave-valor até que expire(definido por um tempo limite).
O cache tem um número máximo de itens, e se receber mais um item quando estiver cheio, o menos recentemente usado deve ser descartado
=end
require 'test/unit'
require 'cache'
class CacheTest < Test::Unit::TestCase
def setup
@cache = Cache::Store.new :limit => 20.items, :default_expiration => 30.seconds
end
def test_should_create_and_initialize_empty
assert_not_nil @cache
assert @cache.data.size == 0
end
def test_should_add_simple_data_to_cache
@cache.set :name, "My cached content"
assert @cache.data.size == 1
assert_equal "My cached content", @cache.get(:name)
end
def test_should_add_multiple_data_to_cache
@cache.set :thing1 => "My cached content", :thing2 => "Other cached content", :thing3 => 15_000
assert @cache.data.size == 3
assert_equal "My cached content", @cache.get(:thing1)
assert_equal 15_000, @cache.get(:thing3)
end
def test_should_remove_less_used_data
@cache.limit = 2.items
@cache.set :content1, "My cached content 1"
@cache.set :content2, "My cached content 2"
@cache.set "content3", "My Content 3 should remove content1"
assert_equal nil, @cache.get(:content1)
assert_equal "My cached content 2", @cache.get(:content2) # Should only remove one key/value
assert @cache.data.size == 2
end
def test_should_remove_less_used_data_2
@cache.limit = 2.items
@cache.set :content1, "My cached content 1"
@cache.set :content2, "My cached content 2"
# Realize gets to cached content
@cache.get(:content1); @cache.get(:content1) # 2 hits
@cache.set "content3", "My Content 3 should remove content2"
assert_equal nil, @cache.get(:content2)
assert_equal "My cached content 1", @cache.get(:content1) # Should only remove one key/value
assert_equal "My Content 3 should remove content2", @cache.get("content3") # Should only remove one key/value
assert @cache.data.size == 2
end
def test_should_add_simple_data_to_cache_with_expiration_date
@cache.set(:thing, "My thing cached!", 2.seconds)
assert_equal "My thing cached!", @cache.get(:thing)
sleep 2
assert_equal nil, @cache.get(:thing)
end
def test_should_add_multiple_data_to_cache_with_expiration_date
@cache.set :thing => {:data => "My thing cached!", :expiration => 2.seconds},
:thing2 => {:data => "My thing 2 cached!", :expiration => 2.seconds}
assert_equal "My thing cached!", @cache.get(:thing)
assert_equal "My thing 2 cached!", @cache.get(:thing2)
sleep 2
assert_equal nil, @cache.get(:thing)
assert_equal nil, @cache.get(:thing2)
end
def test_should_add_multiple_data_to_cache_with_expiration_date_2
@cache.set :thing => ["My thing cached!", 2.seconds],
:thing2 => ["My thing 2 cached!", 2.seconds]
assert_equal "My thing cached!", @cache.get(:thing)
assert_equal "My thing 2 cached!", @cache.get(:thing2)
sleep 2
assert_equal nil, @cache.get(:thing)
assert_equal nil, @cache.get(:thing2)
end
def test_should_not_add_invalid_simple_data_expiration_time
@cache.set :login_at, Time.now, "invalid expiration datetime"
assert_equal nil, @cache.get(:login_at)
end
def test_should_not_add_invalid_multiple_data_expiration_time
@cache.set :login_at => {:data => Time.now, :expiration => "invalid expiration datetime"},
:permissions => {:data => [1,2,3], :expiration => "invalid expiration datetime"}
assert_equal nil, @cache.get(:login_at)
assert_equal nil, @cache.get(:permissions)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment