Created
September 7, 2012 15:33
-
-
Save rpkraemer/3667203 to your computer and use it in GitHub Desktop.
Implementing a simple key/value cache with TDD
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| =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