Class: Heroic::LRUCache
- Inherits:
-
Object
- Object
- Heroic::LRUCache
- Defined in:
- lib/heroic/lru_cache.rb
Overview
This LRU Cache is a generic key-value store that is designed to be safe for concurrent access. It uses a doubly-linked-list to identify which item was least recently retrieved, and a Hash for fast retrieval by key.
To support concurrent access, it uses two levels of locks: a cache-level lock is used to locate or create the desired node and move it to the front of the LRU list. A node-level lock is used to synchronize access to the node’s value.
If a thread is busy generating a value to be stored in the cache, other threads will still be able to read and write to other keys with no conflict. However, if a second thread tries to read the value that the first thread is generating, it will block until the first thread has completed its work.
Defined Under Namespace
Classes: Node
Instance Method Summary collapse
-
#get(key) ⇒ Object
Retrieve a value from the cache for a given key.
-
#initialize(capacity, &block) ⇒ LRUCache
constructor
If you yield a block to the constructor, it will be called on every cache miss to generate the needed value.
-
#put(key, value) ⇒ Object
Inserts a value into the cache, assigning it to the given key.
-
#verify! ⇒ Object
Verify the data structures used to maintain the cache.
Constructor Details
#initialize(capacity, &block) ⇒ LRUCache
If you yield a block to the constructor, it will be called on every cache miss to generate the needed value. This is optional but recommended, as the block will run while holding a lock on the cache node associated with the key. Additional attempts to retrieve the same key will wait for your block to return a result, avoiding duplication of work. However, this also means you MUST NOT access the cache itself from the block, or you will risk creating deadlock. (If you need to create cacheable items from other cacheable items, consider using two separate caches.)
28 29 30 31 32 33 34 35 36 |
# File 'lib/heroic/lru_cache.rb', line 28 def initialize(capacity, &block) raise ArgumentError unless capacity > 0 @capacity = capacity @block = block || Proc.new { nil } @lock = Mutex.new @store = Hash.new @leftmost = nil @rightmost = nil end |
Instance Method Details
#get(key) ⇒ Object
Retrieve a value from the cache for a given key. If the value is in the cache, the method will return immediately. If the value is not in the cache and a block was provided to the constructor, it will be invoked to generate the value and insert it into the cache. If another thread is in the process of generating the same value, the current thread will wait for it to complete.
If the cache is full, this method may cause the least recently used item to be evicted from the cache.
48 49 50 51 |
# File 'lib/heroic/lru_cache.rb', line 48 def get(key) node = node_for_key(key) node.read(&@block) end |
#put(key, value) ⇒ Object
Inserts a value into the cache, assigning it to the given key. (Instead of directly inserting values into the cache, it is recommended that you supply a block to the constructor to generate values on demand.)
57 58 59 60 |
# File 'lib/heroic/lru_cache.rb', line 57 def put(key, value) node = node_for_key(key) node.write(value) end |
#verify! ⇒ Object
Verify the data structures used to maintain the cache. If a problem is detected, an exception is raised. This method is intended for testing and debugging only.
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/heroic/lru_cache.rb', line 66 def verify! @lock.synchronize do left_to_right = Array.new begin node = @leftmost while node left_to_right << node node = node.right end end right_to_left = Array.new begin node = @rightmost while node right_to_left << node node = node.left end end begin raise "leftmost has a left node" if @leftmost && @leftmost.left raise "rightmost has a right node" if @rightmost && @rightmost.right raise "leftmost pointer mismatch" unless @leftmost == left_to_right.first raise "rightmost pointer mismatch" unless @rightmost == right_to_left.first raise "list size mismatch" unless right_to_left.length == left_to_right.length raise "list order mismatch" unless left_to_right.reverse == right_to_left raise "node missing from list" if left_to_right.length < @store.size raise "node missing from store" if left_to_right.length > @store.size raise "store size exceeds capacity" if @store.size > @capacity rescue $stderr.puts "Store: #{@store}" $stderr.puts "L-to-R: #{left_to_right}" $stderr.puts "R-to-L: #{right_to_left}" raise end end end |