caches 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +85 -0
- data/Rakefile +17 -0
- data/TODO.md +29 -0
- data/caches.gemspec +25 -0
- data/lib/caches/accessible.rb +19 -0
- data/lib/caches/all.rb +2 -0
- data/lib/caches/linked_list.rb +115 -0
- data/lib/caches/lru.rb +47 -0
- data/lib/caches/ttl.rb +42 -0
- data/lib/caches/version.rb +3 -0
- data/lib/caches.rb +5 -0
- data/spec/fetch_examples.rb +24 -0
- data/spec/hash_cache/linked_list_spec.rb +120 -0
- data/spec/hash_cache/lru_spec.rb +108 -0
- data/spec/hash_cache/ttl_spec.rb +141 -0
- data/spec/spec_helper.rb +6 -0
- metadata +110 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: dc1818f0238ced3bc043d36e2e5630c7428fbad3
|
4
|
+
data.tar.gz: 5e7f76557165b0c31e62fc52dbc8515943e983fd
|
5
|
+
!binary "U0hBNTEy":
|
6
|
+
metadata.gz: 8a7712863a9cdd2b4fbfc74f8b5898f279857d204677b5ba4ba26be846045fafd8c5932025e218ccd1cae34e5c0c927e95afd3c38de3e3bc5bd0581f075b8a0d
|
7
|
+
data.tar.gz: 874d0aa83657382395ac5ae9ce1df07447ebf45158456dac1051225106a186c819b150df963eeccb78027b454e7895c5a93a5c681e3e2c6681181703b26eb5ba
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Nathan Long
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
# `caches`
|
2
|
+
|
3
|
+
`caches` is a Ruby gem, providing a small collection of caches with good performance and hash-like access patterns. Each is named for its cache expiration strategy - when it will drop a key.
|
4
|
+
|
5
|
+
## Usage
|
6
|
+
|
7
|
+
`caches` provides the following classes.
|
8
|
+
|
9
|
+
### Caches::TTL
|
10
|
+
|
11
|
+
TTL (Time To Live) remembers values for as many seconds as you tell it. The default is 3600 seconds (1 hour).
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
require 'caches/ttl'
|
15
|
+
|
16
|
+
h = Caches::TTL.new(ttl: 5)
|
17
|
+
h[:a] = 'aardvark'
|
18
|
+
h[:a] #=> 'aardvark'
|
19
|
+
sleep(6)
|
20
|
+
h[:a] #=> nil
|
21
|
+
```
|
22
|
+
|
23
|
+
If you pass `refresh: true`, reading a value will reset its timer; otherwise, only writing will.
|
24
|
+
|
25
|
+
The `memoize` method fetches a key if it exists and isn't expired; otherwise, it calculates the value using the block and saves it.
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
h = Caches::TTL.new(ttl: 5)
|
29
|
+
|
30
|
+
# Runs the block
|
31
|
+
h.memoize(:a) { |k| calculation_for(k) }
|
32
|
+
|
33
|
+
# Returns the previously-calculated value
|
34
|
+
h.memoize(:a) { |k| calculation_for(k) }
|
35
|
+
|
36
|
+
sleep(6)
|
37
|
+
|
38
|
+
# Runs the block
|
39
|
+
h.memoize(:a) { |k| calculation_for(k) }
|
40
|
+
```
|
41
|
+
|
42
|
+
### Caches::LRU
|
43
|
+
|
44
|
+
LRU (Least Recently Used) remembers as many keys as you tell it to, dropping the least recently used key on each insert after its limit is reached.
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
require 'caches/lru'
|
48
|
+
h = Caches::LRU.new(max_keys: 3)
|
49
|
+
h[:a] = "aardvark"
|
50
|
+
h[:b] = "boron"
|
51
|
+
h[:c] = "cattail"
|
52
|
+
h[:d] = "dingo"
|
53
|
+
puts h[:a] # => nil
|
54
|
+
```
|
55
|
+
|
56
|
+
## Installation
|
57
|
+
|
58
|
+
Add this line to your application's Gemfile:
|
59
|
+
|
60
|
+
gem 'caches'
|
61
|
+
|
62
|
+
And then execute:
|
63
|
+
|
64
|
+
$ bundle
|
65
|
+
|
66
|
+
Or install it yourself as:
|
67
|
+
|
68
|
+
$ gem install caches
|
69
|
+
|
70
|
+
## Coming Soon
|
71
|
+
|
72
|
+
Other classes with different cache expiration strategies.
|
73
|
+
|
74
|
+
## Contributing
|
75
|
+
|
76
|
+
1. Fork it
|
77
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
78
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
79
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
80
|
+
5. Create new Pull Request
|
81
|
+
|
82
|
+
# Thanks
|
83
|
+
|
84
|
+
- Thanks to [FromAToB.com](http://www.fromatob.com) for giving me time to make this
|
85
|
+
- Thanks to [satyap](https://github.com/satyap); the original specs for `caches` were adapted from [volatile_hash](https://github.com/satyap/volatile_hash).
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
|
4
|
+
desc "Run the specs in documentation format"
|
5
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
6
|
+
t.rspec_opts = '--format documentation'
|
7
|
+
end
|
8
|
+
|
9
|
+
task :console do
|
10
|
+
require 'irb'
|
11
|
+
require 'irb/completion'
|
12
|
+
require 'caches/all'
|
13
|
+
ARGV.clear
|
14
|
+
IRB.start
|
15
|
+
end
|
16
|
+
|
17
|
+
task :default => :spec
|
data/TODO.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# TODO
|
2
|
+
|
3
|
+
## General
|
4
|
+
|
5
|
+
- Refactor LinkedList
|
6
|
+
- Add benchmarks: speed (and maybe memory usage), runnable with rake task for all cache classes. Be able to compare over the course of development (eg, version 0.0.1 vs 0.0.3). Maybe output ASCII graphs for fun.
|
7
|
+
- Add Travis CI testing
|
8
|
+
- Give all caches a way to manually invalidate a given key. Return nil if it wasn't here and the value if it was.
|
9
|
+
- Add "least used" cache class
|
10
|
+
- Define `blank?`, etc on list and caches
|
11
|
+
- Should TTL keys be renewed by updates? LRU ones aren't. If the cache exists to be read, probably not.
|
12
|
+
- Ride a skateboard and, simultaneously, shoot a potato gun
|
13
|
+
|
14
|
+
## New types
|
15
|
+
|
16
|
+
Build LU - Least Used. Expiration criteria is essentially "uses per second" - reads divided by time since write.
|
17
|
+
|
18
|
+
Implementation ideas:
|
19
|
+
- Passage of time doesn't change ranking of keys; only writing does.
|
20
|
+
- Keep a doubly-linked list of keys ordered by usefulness and a reference to the tail for easy dropping
|
21
|
+
- When updating a key, be able to quickly compare it with the one ahead of it and swap if it overtook it in rank
|
22
|
+
- Normal hash data structure has pointers into linked list segments
|
23
|
+
|
24
|
+
|
25
|
+
## Benchmarks
|
26
|
+
|
27
|
+
TTL: Prepopulate instances differing in size by orders of magnitude. Measure N operations of various types. These vary on three axes: read/write, expired/fresh, and update/insert for writes. Output results as graph. Visually confirm that it appears O(1) and is reasonably fast (?) in absolute terms.
|
28
|
+
|
29
|
+
LRU: ?
|
data/caches.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'caches/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "caches"
|
8
|
+
spec.version = Caches::VERSION
|
9
|
+
spec.authors = ["Nathan Long"]
|
10
|
+
spec.email = ["nathanmlong@gmail.com"]
|
11
|
+
spec.description = %q{A small collection of hashes that cache}
|
12
|
+
spec.summary = %q{A small collection of hashes that cache}
|
13
|
+
spec.homepage = "https://github.com/nathanl/caches"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "rspec", "~> 2.14"
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Caches
|
2
|
+
module Accessible
|
3
|
+
|
4
|
+
def fetch(key, default = (default_omitted = true; nil))
|
5
|
+
return self[key] if data.has_key?(key)
|
6
|
+
return yield(key) if block_given?
|
7
|
+
return default unless default_omitted
|
8
|
+
raise KeyError
|
9
|
+
end
|
10
|
+
|
11
|
+
def memoize(key)
|
12
|
+
raise ArgumentError, "Block is required" unless block_given?
|
13
|
+
self[key] # triggers flush or refresh if expired
|
14
|
+
return self[key] if data.has_key?(key)
|
15
|
+
self[key] = yield(key) if block_given?
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
data/lib/caches/all.rb
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
module Caches
|
2
|
+
# This LinkedList class is unusual in that it gives direct access to its nodes.
|
3
|
+
# It trusts the user not to break it! The advantage is that outsiders with
|
4
|
+
# a node reference can reorder the list (eg, using #move_to_head) in O(1) time.
|
5
|
+
class LinkedList
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
attr_reader :length
|
9
|
+
|
10
|
+
def initialize(item = nil)
|
11
|
+
if item
|
12
|
+
Node.new(item).tap { |node|
|
13
|
+
self.head = self.tail = node
|
14
|
+
self.length = 1
|
15
|
+
}
|
16
|
+
else
|
17
|
+
self.length = 0
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def each
|
22
|
+
current = head
|
23
|
+
while current do
|
24
|
+
yield(current.value)
|
25
|
+
current = current.right
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def append(item)
|
30
|
+
return initialize(item) if empty?
|
31
|
+
Node.new(item, left: tail).tap { |new_tail|
|
32
|
+
tail.right = new_tail
|
33
|
+
self.tail = new_tail
|
34
|
+
self.length = length + 1
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
def prepend(item)
|
39
|
+
return initialize(item) if empty?
|
40
|
+
Node.new(item, right: head).tap { |new_head|
|
41
|
+
head.left = new_head
|
42
|
+
self.head = new_head
|
43
|
+
self.length = length + 1
|
44
|
+
}
|
45
|
+
end
|
46
|
+
alias :unshift :prepend
|
47
|
+
|
48
|
+
def pop
|
49
|
+
tail.tap {
|
50
|
+
new_tail = tail.left
|
51
|
+
new_tail.right = nil
|
52
|
+
self.tail = new_tail
|
53
|
+
self.length = length - 1
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
def lpop
|
58
|
+
head.tap {
|
59
|
+
new_head = head.right
|
60
|
+
new_head.left = nil
|
61
|
+
self.head = new_head
|
62
|
+
self.length = length - 1
|
63
|
+
}
|
64
|
+
end
|
65
|
+
alias :shift :lpop
|
66
|
+
|
67
|
+
def move_to_head(node)
|
68
|
+
excise(node)
|
69
|
+
node.right = head
|
70
|
+
self.head = node
|
71
|
+
end
|
72
|
+
|
73
|
+
def empty?
|
74
|
+
length == 0
|
75
|
+
end
|
76
|
+
|
77
|
+
# TODO - move these to a module and use in caches, too
|
78
|
+
alias :blank? :empty?
|
79
|
+
def present?
|
80
|
+
!empty?
|
81
|
+
end
|
82
|
+
def presence
|
83
|
+
self if present?
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
attr_accessor :head, :tail
|
88
|
+
attr_writer :length
|
89
|
+
|
90
|
+
def excise(node)
|
91
|
+
raise InvalidNode unless node.is_a?(Node)
|
92
|
+
left = node.left
|
93
|
+
right = node.right
|
94
|
+
left.right = right unless left.nil?
|
95
|
+
right.left = left unless right.nil?
|
96
|
+
self.head = right if head == node
|
97
|
+
self.tail = left if tail == node
|
98
|
+
node
|
99
|
+
end
|
100
|
+
|
101
|
+
class Node
|
102
|
+
attr_accessor :value, :right, :left
|
103
|
+
def initialize(value, pointers = {})
|
104
|
+
self.value = value
|
105
|
+
self.right = pointers[:right]
|
106
|
+
self.left = pointers[:left]
|
107
|
+
end
|
108
|
+
|
109
|
+
def right?; !right.nil?; end
|
110
|
+
def left?; !left.nil? end
|
111
|
+
end
|
112
|
+
|
113
|
+
InvalidNode = Class.new(StandardError)
|
114
|
+
end
|
115
|
+
end
|
data/lib/caches/lru.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require_relative 'accessible'
|
2
|
+
require_relative 'linked_list'
|
3
|
+
|
4
|
+
module Caches
|
5
|
+
class LRU
|
6
|
+
include Accessible
|
7
|
+
attr_accessor :max_keys
|
8
|
+
|
9
|
+
def initialize(options = {})
|
10
|
+
self.max_keys = options.fetch(:max_keys, 20)
|
11
|
+
self.data = {}
|
12
|
+
self.keys = LinkedList.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def [](key)
|
16
|
+
return nil unless data.has_key?(key)
|
17
|
+
value, node = data[key]
|
18
|
+
keys.move_to_head(node)
|
19
|
+
value
|
20
|
+
end
|
21
|
+
|
22
|
+
def []=(key, val)
|
23
|
+
if data.has_key?(key)
|
24
|
+
data[key][0] = val
|
25
|
+
else
|
26
|
+
node = keys.prepend(key)
|
27
|
+
data[key] = [val, node]
|
28
|
+
end
|
29
|
+
prune
|
30
|
+
val
|
31
|
+
end
|
32
|
+
|
33
|
+
def size
|
34
|
+
keys.length
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
attr_accessor :keys, :data
|
39
|
+
|
40
|
+
def prune
|
41
|
+
return unless keys.length > max_keys
|
42
|
+
expiring_node = keys.pop
|
43
|
+
data.delete(expiring_node.value)
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
data/lib/caches/ttl.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require_relative 'accessible'
|
2
|
+
|
3
|
+
module Caches
|
4
|
+
class TTL
|
5
|
+
include Accessible
|
6
|
+
attr_accessor :ttl, :refresh
|
7
|
+
|
8
|
+
def initialize(options = {})
|
9
|
+
self.ttl = options.fetch(:ttl) { 3600 }
|
10
|
+
self.refresh = !!(options.fetch(:refresh, false))
|
11
|
+
self.data = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def [](key)
|
15
|
+
return nil unless data.has_key?(key)
|
16
|
+
if (current_time - data[key][:time]) < ttl
|
17
|
+
data[key][:time] = current_time if refresh
|
18
|
+
data[key][:value]
|
19
|
+
else
|
20
|
+
data.delete(key)
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def []=(key, val)
|
26
|
+
data[key] = {time: current_time, value: val}
|
27
|
+
end
|
28
|
+
|
29
|
+
def size
|
30
|
+
data.keys.length
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
attr_accessor :data
|
36
|
+
|
37
|
+
def current_time
|
38
|
+
Time.now
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
data/lib/caches.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
shared_examples "fetch" do
|
2
|
+
|
3
|
+
it "returns the value if an existing key is accessed" do
|
4
|
+
expect(cache.fetch(:c)).to eq('Caspian')
|
5
|
+
end
|
6
|
+
|
7
|
+
it "raises an error if a missing key is accessed" do
|
8
|
+
expect{cache.fetch(:nonexistent)}.to raise_error(KeyError)
|
9
|
+
end
|
10
|
+
|
11
|
+
it "uses a default value if one is supplied" do
|
12
|
+
expect(cache.fetch(:nonexistent, 'hi')).to eq('hi')
|
13
|
+
end
|
14
|
+
|
15
|
+
it "gets its default from a block if one is supplied" do
|
16
|
+
expect(cache.fetch(:nonexistent) { |key| key.to_s.upcase }).to eq('NONEXISTENT')
|
17
|
+
end
|
18
|
+
|
19
|
+
it "prefers a block value to an immediate one (like native hashes do)" do
|
20
|
+
expect(cache.fetch(:nonexistent, 'hi') { |key| key.to_s.upcase }).to eq('NONEXISTENT')
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require_relative '../../lib/caches/linked_list'
|
3
|
+
|
4
|
+
describe Caches::LinkedList do
|
5
|
+
|
6
|
+
let(:empty_list) { described_class.new }
|
7
|
+
let(:list) { described_class.new('lemur') }
|
8
|
+
let(:node_class) { Caches::LinkedList::Node }
|
9
|
+
|
10
|
+
it "can convert itself to an array of values" do
|
11
|
+
expect(list.to_a).to eq(['lemur'])
|
12
|
+
end
|
13
|
+
|
14
|
+
describe '#append' do
|
15
|
+
|
16
|
+
it "adds an item to the end" do
|
17
|
+
list.append('wombat')
|
18
|
+
expect(list.to_a).to eq(['lemur', 'wombat'])
|
19
|
+
end
|
20
|
+
|
21
|
+
it "returns a Node" do
|
22
|
+
node = list.append('wombat')
|
23
|
+
expect(node).to be_a(node_class)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "sets up the list correctly if it was empty to start with" do
|
27
|
+
empty_list.append('jimmy')
|
28
|
+
expect{empty_list.prepend('jammy')}.not_to raise_error
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "#prepend" do
|
34
|
+
|
35
|
+
it "adds an item to the beginning" do
|
36
|
+
list.prepend('vole')
|
37
|
+
expect(list.to_a).to eq(['vole', 'lemur'])
|
38
|
+
end
|
39
|
+
|
40
|
+
it "returns a Node" do
|
41
|
+
node = list.prepend('vole')
|
42
|
+
expect(node).to be_a(node_class)
|
43
|
+
end
|
44
|
+
|
45
|
+
it "sets up the list correctly if it was empty to start with" do
|
46
|
+
empty_list.prepend('jimmy')
|
47
|
+
expect{empty_list.append('jammy')}.not_to raise_error
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
it "knows its length" do
|
53
|
+
expect(list.length).to eq(1)
|
54
|
+
list.append('wombat')
|
55
|
+
list.prepend('vole')
|
56
|
+
expect(list.length).to eq(3)
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "#move_to_head" do
|
60
|
+
|
61
|
+
context "when the argument is a Node" do
|
62
|
+
|
63
|
+
it "makes it the new head" do
|
64
|
+
list.append('mittens')
|
65
|
+
node = list.append('cozy')
|
66
|
+
list.move_to_head(node)
|
67
|
+
expect(list.to_a).to eq(%w[cozy lemur mittens])
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
context "when the argument is not a Node" do
|
73
|
+
|
74
|
+
it "raises an error" do
|
75
|
+
list.append('cozy')
|
76
|
+
expect{list.move_to_head('super duper')}.to raise_error(Caches::LinkedList::InvalidNode)
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
context "when there's more than one item" do
|
84
|
+
|
85
|
+
let(:list) {
|
86
|
+
list = described_class.new('lemur')
|
87
|
+
list.append('muskrat')
|
88
|
+
list
|
89
|
+
}
|
90
|
+
|
91
|
+
describe "#pop" do
|
92
|
+
|
93
|
+
it "removes the last item" do
|
94
|
+
list.pop
|
95
|
+
expect(list.to_a).to eq(['lemur'])
|
96
|
+
end
|
97
|
+
|
98
|
+
it "returns a node" do
|
99
|
+
expect(list.pop).to be_a(node_class)
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
describe "#lpop" do
|
106
|
+
|
107
|
+
it "removes the first item" do
|
108
|
+
list.lpop
|
109
|
+
expect(list.to_a).to eq(['muskrat'])
|
110
|
+
end
|
111
|
+
|
112
|
+
it "returns a node" do
|
113
|
+
expect(list.lpop).to be_a(node_class)
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'fetch_examples'
|
3
|
+
require_relative '../../lib/caches/lru'
|
4
|
+
|
5
|
+
describe Caches::LRU do
|
6
|
+
|
7
|
+
let(:options) { {max_keys: 4} }
|
8
|
+
let(:cache) {
|
9
|
+
described_class.new(options).tap {|c|
|
10
|
+
c[:a] = 'Alambil'
|
11
|
+
c[:b] = 'Belisar'
|
12
|
+
c[:c] = 'Caspian'
|
13
|
+
}
|
14
|
+
}
|
15
|
+
|
16
|
+
it "remembers its values" do
|
17
|
+
expect(cache[:a]).to eq('Alambil')
|
18
|
+
expect(cache[:b]).to eq('Belisar')
|
19
|
+
expect(cache[:c]).to eq('Caspian')
|
20
|
+
end
|
21
|
+
|
22
|
+
it "can report its size" do
|
23
|
+
expect(cache.size).to eq(3)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "can grow up to the max size given" do
|
27
|
+
cache[:d] = 'Destrier'
|
28
|
+
expect(cache.size).to eq(4)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "does not grow beyond the max size given" do
|
32
|
+
cache[:d] = 'Destrier'
|
33
|
+
cache[:e] = 'Erimon'
|
34
|
+
expect(cache.size).to eq(4)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "drops the least-recently accessed key" do
|
38
|
+
cache[:d] = 'Destrier'
|
39
|
+
cache[:e] = 'Erimon'
|
40
|
+
expect(cache[:a]).to be_nil
|
41
|
+
end
|
42
|
+
|
43
|
+
it "counts a read as access" do
|
44
|
+
cache[:d] = 'Destrier'
|
45
|
+
cache[:a]
|
46
|
+
cache[:e] = 'Erimon'
|
47
|
+
expect(cache[:a]).to eq('Alambil')
|
48
|
+
expect(cache[:b]).to be_nil
|
49
|
+
end
|
50
|
+
|
51
|
+
include_examples "fetch"
|
52
|
+
|
53
|
+
describe "memoize" do
|
54
|
+
|
55
|
+
it "requires a block" do
|
56
|
+
expect{cache_memoize(:c)}.to raise_error
|
57
|
+
end
|
58
|
+
|
59
|
+
let(:greeting) { 'hi' }
|
60
|
+
|
61
|
+
context "when the key already exists" do
|
62
|
+
|
63
|
+
it "does not calculate the value" do
|
64
|
+
expect(greeting).not_to receive(:upcase)
|
65
|
+
cache.memoize(:c) { greeting.upcase }
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
context "when the key has not been set" do
|
71
|
+
|
72
|
+
let(:key) { :nonexistent }
|
73
|
+
|
74
|
+
it "calculates the value" do
|
75
|
+
expect(greeting).to receive(:upcase)
|
76
|
+
cache.memoize(:nonexistent) { greeting.upcase }
|
77
|
+
end
|
78
|
+
|
79
|
+
it "sets the value" do
|
80
|
+
cache.memoize(:nonexistent) { greeting.upcase }
|
81
|
+
expect(cache[:nonexistent]).to eq(greeting.upcase)
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
context "when the key has been dropped" do
|
87
|
+
|
88
|
+
before :each do
|
89
|
+
cache[:d] = 'Destrier'
|
90
|
+
cache[:e] = 'Erimon'
|
91
|
+
end
|
92
|
+
|
93
|
+
it "calculates the value" do
|
94
|
+
expect(greeting).to receive(:upcase)
|
95
|
+
cache.memoize(:a) { greeting.upcase }
|
96
|
+
end
|
97
|
+
|
98
|
+
it "sets the value" do
|
99
|
+
cache.memoize(:a) { greeting.upcase }
|
100
|
+
expect(cache[:a]).to eq(greeting.upcase)
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
@@ -0,0 +1,141 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'fetch_examples'
|
3
|
+
require_relative '../../lib/caches/ttl'
|
4
|
+
|
5
|
+
describe Caches::TTL do
|
6
|
+
|
7
|
+
let(:options) { {} }
|
8
|
+
let(:cache) {
|
9
|
+
described_class.new(options).tap {|c|
|
10
|
+
c[:c] = 'Caspian'
|
11
|
+
c.stub(:current_time).and_return(start_time)
|
12
|
+
}
|
13
|
+
}
|
14
|
+
|
15
|
+
let(:start_time) { Time.now }
|
16
|
+
let(:before_ttl) { start_time + 1800 }
|
17
|
+
let(:after_ttl) { start_time + 3601 }
|
18
|
+
|
19
|
+
it "can report its size" do
|
20
|
+
expect(cache.size).to eq(1)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "remembers cached values before the TTL expires" do
|
24
|
+
expect(cache[:c]).to eq('Caspian')
|
25
|
+
cache.stub(:current_time).and_return(before_ttl)
|
26
|
+
expect(cache[:c]).to eq('Caspian')
|
27
|
+
end
|
28
|
+
|
29
|
+
it "forgets cached values after the TTL expires" do
|
30
|
+
expect(cache[:c]).to eq('Caspian')
|
31
|
+
cache.stub(:current_time).and_return(after_ttl)
|
32
|
+
expect(cache[:c]).to be_nil
|
33
|
+
end
|
34
|
+
|
35
|
+
it "continues returning nil for cached values after the TTL expires" do
|
36
|
+
expect(cache[:c]).to eq('Caspian')
|
37
|
+
cache.stub(:current_time).and_return(after_ttl)
|
38
|
+
expect(cache[:c]).to be_nil
|
39
|
+
expect(cache[:c]).to be_nil
|
40
|
+
end
|
41
|
+
|
42
|
+
it "resets TTL when an item is updated" do
|
43
|
+
cache.stub(:current_time).and_return(before_ttl)
|
44
|
+
cache[:c] = 'Cornelius'
|
45
|
+
cache.stub(:current_time).and_return(after_ttl)
|
46
|
+
expect(cache[:c]).to eq('Cornelius')
|
47
|
+
end
|
48
|
+
|
49
|
+
it "doesn't reset TTL when an item is accessed" do
|
50
|
+
cache.stub(:current_time).and_return(before_ttl)
|
51
|
+
expect(cache[:c]).to eq('Caspian')
|
52
|
+
cache.stub(:current_time).and_return(after_ttl)
|
53
|
+
expect(cache[:c]).to be_nil
|
54
|
+
end
|
55
|
+
|
56
|
+
context "when asked to refresh TTL on access" do
|
57
|
+
|
58
|
+
let(:options) { {refresh: true} }
|
59
|
+
|
60
|
+
it "keeps values that were accessed before the TTL expired" do
|
61
|
+
cache.stub(:current_time).and_return(before_ttl)
|
62
|
+
expect(cache[:c]).to eq('Caspian')
|
63
|
+
cache.stub(:current_time).and_return(after_ttl)
|
64
|
+
expect(cache[:c]).to eq('Caspian')
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
include_examples "fetch"
|
70
|
+
|
71
|
+
describe "memoize" do
|
72
|
+
|
73
|
+
it "requires a block" do
|
74
|
+
expect{cache_memoize(:c)}.to raise_error
|
75
|
+
end
|
76
|
+
|
77
|
+
let(:greeting) { 'hi' }
|
78
|
+
|
79
|
+
context "when the key already exists" do
|
80
|
+
|
81
|
+
context "before the TTL is up" do
|
82
|
+
|
83
|
+
it "returns the existing value" do
|
84
|
+
expect(cache.memoize(:c) { 'Eustace' } ).to eq('Caspian')
|
85
|
+
end
|
86
|
+
|
87
|
+
it "does not calculate the value" do
|
88
|
+
cache.stub(:current_time).and_return(before_ttl)
|
89
|
+
expect(greeting).not_to receive(:upcase)
|
90
|
+
cache.memoize(:c) { greeting.upcase }
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
context "after the TTL is up" do
|
96
|
+
|
97
|
+
it "recalculates the value" do
|
98
|
+
cache.stub(:current_time).and_return(after_ttl)
|
99
|
+
expect(greeting).to receive(:upcase)
|
100
|
+
cache.memoize(:c) { greeting.upcase }
|
101
|
+
end
|
102
|
+
|
103
|
+
it "returns the calculated value" do
|
104
|
+
cache.stub(:current_time).and_return(after_ttl)
|
105
|
+
expect(cache.memoize(:c) { |key| key.to_s.upcase }).to eq('C')
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
context "when the key does not exist" do
|
113
|
+
|
114
|
+
it "calculates the value" do
|
115
|
+
expect(greeting).to receive(:upcase)
|
116
|
+
cache.memoize(:nonexistent) { greeting.upcase }
|
117
|
+
end
|
118
|
+
|
119
|
+
it "returns the value" do
|
120
|
+
expect(cache.memoize(:nonexistent) { |key| key.to_s.upcase }).to eq('NONEXISTENT')
|
121
|
+
end
|
122
|
+
|
123
|
+
it "does not calculate the value again within the TTL" do
|
124
|
+
cache.memoize(:nonexistent) { greeting.upcase }
|
125
|
+
cache.stub(:current_time).and_return(before_ttl)
|
126
|
+
expect(greeting).not_to receive(:upcase)
|
127
|
+
cache.memoize(:nonexistent) { greeting.upcase }
|
128
|
+
end
|
129
|
+
|
130
|
+
it "does calculate the value again after the TTL is up" do
|
131
|
+
cache.memoize(:nonexistent) { greeting.upcase }
|
132
|
+
cache.stub(:current_time).and_return(after_ttl)
|
133
|
+
expect(greeting).to receive(:upcase)
|
134
|
+
cache.memoize(:nonexistent) { greeting.upcase }
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: caches
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Nathan Long
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-07-28 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.14'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2.14'
|
55
|
+
description: A small collection of hashes that cache
|
56
|
+
email:
|
57
|
+
- nathanmlong@gmail.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- .gitignore
|
63
|
+
- Gemfile
|
64
|
+
- LICENSE.txt
|
65
|
+
- README.md
|
66
|
+
- Rakefile
|
67
|
+
- TODO.md
|
68
|
+
- caches.gemspec
|
69
|
+
- lib/caches.rb
|
70
|
+
- lib/caches/accessible.rb
|
71
|
+
- lib/caches/all.rb
|
72
|
+
- lib/caches/linked_list.rb
|
73
|
+
- lib/caches/lru.rb
|
74
|
+
- lib/caches/ttl.rb
|
75
|
+
- lib/caches/version.rb
|
76
|
+
- spec/fetch_examples.rb
|
77
|
+
- spec/hash_cache/linked_list_spec.rb
|
78
|
+
- spec/hash_cache/lru_spec.rb
|
79
|
+
- spec/hash_cache/ttl_spec.rb
|
80
|
+
- spec/spec_helper.rb
|
81
|
+
homepage: https://github.com/nathanl/caches
|
82
|
+
licenses:
|
83
|
+
- MIT
|
84
|
+
metadata: {}
|
85
|
+
post_install_message:
|
86
|
+
rdoc_options: []
|
87
|
+
require_paths:
|
88
|
+
- lib
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ! '>='
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
requirements: []
|
100
|
+
rubyforge_project:
|
101
|
+
rubygems_version: 2.0.14
|
102
|
+
signing_key:
|
103
|
+
specification_version: 4
|
104
|
+
summary: A small collection of hashes that cache
|
105
|
+
test_files:
|
106
|
+
- spec/fetch_examples.rb
|
107
|
+
- spec/hash_cache/linked_list_spec.rb
|
108
|
+
- spec/hash_cache/lru_spec.rb
|
109
|
+
- spec/hash_cache/ttl_spec.rb
|
110
|
+
- spec/spec_helper.rb
|