caches 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -3
- data/CONTRIBUTING.md +11 -0
- data/README.md +2 -6
- data/Rakefile +7 -0
- data/caches.gemspec +1 -0
- data/lib/caches/lru.rb +2 -0
- data/lib/caches/ttl.rb +14 -7
- data/lib/caches/version.rb +1 -1
- data/spec/benchmark_spec.rb +92 -0
- data/spec/caches/lru_spec.rb +22 -3
- data/spec/caches/ttl_spec.rb +22 -10
- data/spec/spec_helper.rb +1 -0
- metadata +29 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 89ecbca50c8eeca4f3a3ba844fc8af31051e0aca
|
4
|
+
data.tar.gz: 7bb00ffe94dd6f21767037d14217223e7f4bad86
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: afa402ae0b268873414fbd05f13a9c1d15982272523ab3e70d6badd37b6d05d03bf5054e774e1f17cfee93317460839018766fd76b7d8eb286b118821acfaadd
|
7
|
+
data.tar.gz: 36e84e288dc638a1c0b184136965fba84b737d243d81f334a0feddd356c95d3fb16631336bed1b0770ed76e29635e5d34390418ffa57fa7846083b1b4c6c594c
|
data/CHANGELOG.md
CHANGED
@@ -1,11 +1,14 @@
|
|
1
1
|
# Change Log
|
2
2
|
All notable changes to this project will be documented in this file.
|
3
3
|
|
4
|
-
##
|
4
|
+
## 0.0.4 - 2015-03-03
|
5
5
|
|
6
6
|
### Added
|
7
7
|
|
8
|
-
|
8
|
+
- `Caches::TTL` supports `#delete(key)`
|
9
|
+
- Added a CONTRIBUTING guide
|
10
|
+
- Benchmarking with Ruby 2.1.3
|
11
|
+
- `Caches::LRU` can handle `max_keys: 0`
|
9
12
|
|
10
13
|
### Changed
|
11
14
|
Nothing
|
@@ -15,4 +18,4 @@ Nothing
|
|
15
18
|
|
16
19
|
## 0.0.3 - 2014-08-22
|
17
20
|
|
18
|
-
Currently have Caches::TTL and Caches::LRU
|
21
|
+
Currently have `Caches::TTL` and `Caches::LRU`
|
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# Contributing
|
2
|
+
|
3
|
+
The goal of this gem is to provide a small collection of useful, performant caches.
|
4
|
+
|
5
|
+
- To add a new kind of cache, you must convince us that it's useful
|
6
|
+
- To change a cache, you must
|
7
|
+
- convince us that it's a useful change
|
8
|
+
- ensure that it's well-tested
|
9
|
+
- ensure that it's benchmarked and performant
|
10
|
+
|
11
|
+
`rake` runs the specs and `rake benchmark` runs the benchmarks (still in-progress). They are expected to pass on Ruby 2.1.3
|
data/README.md
CHANGED
@@ -49,7 +49,7 @@ h.memoize(:a) { |k| calculation_for(k) }
|
|
49
49
|
|
50
50
|
### Caches::LRU
|
51
51
|
|
52
|
-
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.
|
52
|
+
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. (Inserts and reads count as a usage; updates do not.)
|
53
53
|
|
54
54
|
```ruby
|
55
55
|
require 'caches/lru'
|
@@ -77,11 +77,7 @@ Or install it yourself as:
|
|
77
77
|
|
78
78
|
## Contributing
|
79
79
|
|
80
|
-
|
81
|
-
2. Create your feature branch (`git checkout -b my-new-feature`)
|
82
|
-
3. Commit your changes (`git commit -am 'Add some feature'`)
|
83
|
-
4. Push to the branch (`git push origin my-new-feature`)
|
84
|
-
5. Create new Pull Request
|
80
|
+
See CONTRIBUTING.md
|
85
81
|
|
86
82
|
# Thanks
|
87
83
|
|
data/Rakefile
CHANGED
@@ -1,11 +1,18 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
2
|
require 'rspec/core/rake_task'
|
3
3
|
|
4
|
+
task :default => :spec
|
5
|
+
|
4
6
|
desc "Run the specs in documentation format"
|
5
7
|
RSpec::Core::RakeTask.new(:spec) do |t|
|
6
8
|
t.rspec_opts = '--format documentation'
|
7
9
|
end
|
8
10
|
|
11
|
+
desc "Run the benchmarks"
|
12
|
+
RSpec::Core::RakeTask.new(:benchmark) do |t|
|
13
|
+
t.rspec_opts = '--format documentation --tag benchmark'
|
14
|
+
end
|
15
|
+
|
9
16
|
task :console do
|
10
17
|
require 'irb'
|
11
18
|
require 'irb/completion'
|
data/caches.gemspec
CHANGED
data/lib/caches/lru.rb
CHANGED
@@ -13,6 +13,7 @@ module Caches
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def [](key)
|
16
|
+
return nil if max_keys.zero?
|
16
17
|
return nil unless data.has_key?(key)
|
17
18
|
value, node = data[key]
|
18
19
|
keys.move_to_head(node)
|
@@ -20,6 +21,7 @@ module Caches
|
|
20
21
|
end
|
21
22
|
|
22
23
|
def []=(key, val)
|
24
|
+
return nil if max_keys.zero?
|
23
25
|
if data.has_key?(key)
|
24
26
|
data[key][0] = val
|
25
27
|
else
|
data/lib/caches/ttl.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'time'
|
2
2
|
require_relative 'accessible'
|
3
|
+
require_relative 'linked_list'
|
3
4
|
|
4
5
|
module Caches
|
5
6
|
class TTL
|
@@ -16,13 +17,12 @@ module Caches
|
|
16
17
|
|
17
18
|
def [](key)
|
18
19
|
return nil unless data.has_key?(key)
|
19
|
-
if current?(key)
|
20
|
-
data[key][:
|
21
|
-
|
20
|
+
if current?(key)
|
21
|
+
data[key][:value].tap {
|
22
|
+
data[key][:time] = current_time if refresh
|
23
|
+
}
|
22
24
|
else
|
23
|
-
|
24
|
-
nodes.delete(node)
|
25
|
-
data.delete(key)
|
25
|
+
delete(key)
|
26
26
|
nil
|
27
27
|
end
|
28
28
|
end
|
@@ -41,8 +41,15 @@ module Caches
|
|
41
41
|
data[key] = {time: current_time, value: val, node: node}
|
42
42
|
end
|
43
43
|
|
44
|
+
def delete(key)
|
45
|
+
node = data[key][:node]
|
46
|
+
nodes.delete(node)
|
47
|
+
hash = data.delete(key)
|
48
|
+
hash.fetch(:value)
|
49
|
+
end
|
50
|
+
|
44
51
|
def size
|
45
|
-
|
52
|
+
nodes.length
|
46
53
|
end
|
47
54
|
|
48
55
|
def keys
|
data/lib/caches/version.rb
CHANGED
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'benchmark/ips'
|
3
|
+
require_relative '../lib/caches/all'
|
4
|
+
require 'stringio'
|
5
|
+
|
6
|
+
describe "benchmarks", benchmark: true do
|
7
|
+
|
8
|
+
# Comment to unmute the output from Benchmark
|
9
|
+
before(:all) { $stdout = StringIO.new }
|
10
|
+
after(:all) { $stdout = STDOUT }
|
11
|
+
|
12
|
+
let(:klass) { described_class }
|
13
|
+
|
14
|
+
# These are both somewhat arbitrary and may depend on Ruby version
|
15
|
+
# Tested with Ruby 2.1.3
|
16
|
+
let(:reasonable_ratio) { 1.1 }
|
17
|
+
let(:reasonable_percent_variation) { 30 }
|
18
|
+
|
19
|
+
describe Caches::TTL do
|
20
|
+
|
21
|
+
let!(:small_range) { (1..10) }
|
22
|
+
let!(:small_cache) {
|
23
|
+
klass.new.tap { |sc| small_range.each {|key| sc[key] = key } }
|
24
|
+
}
|
25
|
+
let(:big_range) { (1..1_000_000) }
|
26
|
+
let(:big_cache) {
|
27
|
+
klass.new.tap { |sc| big_range.each {|key| sc[key] = key } }
|
28
|
+
}
|
29
|
+
|
30
|
+
let(:very_big_range) { 1..10_000_000 }
|
31
|
+
let(:very_big_cache) {
|
32
|
+
klass.new.tap { |sc| very_big_range.each { |key| sc[key] = key } }
|
33
|
+
}
|
34
|
+
|
35
|
+
it "has constant time reads" do
|
36
|
+
big_cache[1] # force creation
|
37
|
+
results = {}
|
38
|
+
report = Benchmark.ips do |x|
|
39
|
+
small_cache_test_keys = small_range.to_a.shuffle.cycle
|
40
|
+
big_cache_test_keys = big_range.to_a.shuffle.cycle
|
41
|
+
results[:small] = x.report("reading from small cache") {
|
42
|
+
small_cache[small_cache_test_keys.next]
|
43
|
+
}
|
44
|
+
results[:big] = x.report("reading from big cache") {
|
45
|
+
big_cache[big_cache_test_keys.next]
|
46
|
+
}
|
47
|
+
end
|
48
|
+
small_ips = report.entries.first.ips
|
49
|
+
big_ips = report.entries.last.ips
|
50
|
+
expect(big_ips).to be < (small_ips * reasonable_ratio)
|
51
|
+
end
|
52
|
+
|
53
|
+
it "has constant time inserts" do
|
54
|
+
write_keys = (1..10_000_000).to_a.shuffle.cycle
|
55
|
+
report = Benchmark.ips do |x|
|
56
|
+
x.report("inserts") { small_cache[write_keys.next] = :a }
|
57
|
+
end
|
58
|
+
rand_report = report.entries.first
|
59
|
+
|
60
|
+
variance = rand_report.stddev_percentage
|
61
|
+
expect(variance).to be < reasonable_percent_variation
|
62
|
+
end
|
63
|
+
|
64
|
+
it "has constant time updates" do
|
65
|
+
write_keys = small_range.to_a.cycle
|
66
|
+
report = Benchmark.ips do |x|
|
67
|
+
x.report("updates") { small_cache[write_keys.next] = :a }
|
68
|
+
end
|
69
|
+
rand_report = report.entries.first
|
70
|
+
|
71
|
+
variance = rand_report.stddev_percentage
|
72
|
+
expect(variance).to be < reasonable_percent_variation
|
73
|
+
end
|
74
|
+
|
75
|
+
# Using a very big cache here in hopes that, even though IPS does as many
|
76
|
+
# deletes as it possibly can during its runtime, it won't finish deleting
|
77
|
+
# all the cache keys before it finishes
|
78
|
+
it "has constant time deletes" do
|
79
|
+
delete_keys = very_big_range.to_a.each
|
80
|
+
very_big_cache[1] # force creation
|
81
|
+
report = Benchmark.ips do |x|
|
82
|
+
x.report("deletes") { very_big_cache.delete(delete_keys.next) }
|
83
|
+
end
|
84
|
+
rand_report = report.entries.first
|
85
|
+
|
86
|
+
variance = rand_report.stddev_percentage
|
87
|
+
expect(variance).to be < reasonable_percent_variation
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
data/spec/caches/lru_spec.rb
CHANGED
@@ -62,7 +62,8 @@ describe Caches::LRU do
|
|
62
62
|
|
63
63
|
it "does not calculate the value" do
|
64
64
|
expect(greeting).not_to receive(:upcase)
|
65
|
-
cache.memoize(:c) { greeting.upcase }
|
65
|
+
val = cache.memoize(:c) { greeting.upcase }
|
66
|
+
expect(val).to eq("Caspian")
|
66
67
|
end
|
67
68
|
|
68
69
|
end
|
@@ -72,8 +73,9 @@ describe Caches::LRU do
|
|
72
73
|
let(:key) { :nonexistent }
|
73
74
|
|
74
75
|
it "calculates the value" do
|
75
|
-
expect(greeting).to receive(:upcase)
|
76
|
-
cache.memoize(:nonexistent) { greeting.upcase }
|
76
|
+
expect(greeting).to receive(:upcase).and_call_original
|
77
|
+
val = cache.memoize(:nonexistent) { greeting.upcase }
|
78
|
+
expect(val).to eq("HI")
|
77
79
|
end
|
78
80
|
|
79
81
|
it "sets the value" do
|
@@ -104,5 +106,22 @@ describe Caches::LRU do
|
|
104
106
|
|
105
107
|
end
|
106
108
|
|
109
|
+
describe "when max_keys is 0" do
|
110
|
+
|
111
|
+
let(:options) { {max_keys: 0} }
|
112
|
+
let(:greeting) { 'hi' }
|
113
|
+
|
114
|
+
it "doesn't store anything" do
|
115
|
+
expect(cache[:a]).to eq(nil)
|
116
|
+
end
|
117
|
+
|
118
|
+
it "always runs a memoize block" do
|
119
|
+
expect(greeting).to receive(:upcase).and_call_original
|
120
|
+
val = cache.memoize(:a) { greeting.upcase }
|
121
|
+
expect(val).to eq("HI")
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
|
107
126
|
end
|
108
127
|
|
data/spec/caches/ttl_spec.rb
CHANGED
@@ -7,7 +7,7 @@ describe Caches::TTL do
|
|
7
7
|
let(:ttl) { 0.01 }
|
8
8
|
let(:most_of_ttl) { ttl * 0.8 }
|
9
9
|
let(:options) { { ttl: ttl} }
|
10
|
-
let(:cache) {
|
10
|
+
let!(:cache) {
|
11
11
|
described_class.new(options).tap {|c|
|
12
12
|
c[:a] = 'Aravis'
|
13
13
|
c[:b] = 'Bern'
|
@@ -19,24 +19,36 @@ describe Caches::TTL do
|
|
19
19
|
expect(cache.size).to eq(3)
|
20
20
|
end
|
21
21
|
|
22
|
-
it "
|
23
|
-
expect(cache
|
22
|
+
it "can delete keys" do
|
23
|
+
expect(cache.keys).to eq(%i[a b c])
|
24
24
|
expect(cache.size).to eq(3)
|
25
25
|
# testing private method
|
26
26
|
expect(cache.send(:nodes).length).to eq(3)
|
27
|
-
end
|
28
27
|
|
29
|
-
|
30
|
-
expect(
|
31
|
-
|
32
|
-
expect(cache[
|
28
|
+
deleted = cache.delete(:b)
|
29
|
+
expect(deleted).to eq('Bern')
|
30
|
+
|
31
|
+
expect(cache.keys).to eq(%i[a c])
|
33
32
|
expect(cache.size).to eq(2)
|
34
33
|
# testing private method
|
35
34
|
expect(cache.send(:nodes).length).to eq(2)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "remembers cached values before the TTL expires" do
|
38
|
+
expect(cache).not_to receive(:delete)
|
39
|
+
expect(cache[:c]).to eq('Caspian')
|
40
|
+
expect(cache.size).to eq(3)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "forgets cached values when a read is attempted after the TTL expires" do
|
44
|
+
sleep(ttl)
|
45
|
+
expect(cache).to receive(:delete).with(:a).and_call_original
|
36
46
|
expect(cache[:a]).to be_nil
|
47
|
+
expect(cache.size).to eq(2)
|
48
|
+
|
49
|
+
expect(cache).to receive(:delete).with(:c).and_call_original
|
50
|
+
expect(cache[:c]).to be_nil
|
37
51
|
expect(cache.size).to eq(1)
|
38
|
-
# testing private method
|
39
|
-
expect(cache.send(:nodes).length).to eq(1)
|
40
52
|
end
|
41
53
|
|
42
54
|
describe "listing keys and values" do
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,57 +1,71 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: caches
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nathan Long
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-03-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - ~>
|
17
|
+
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '1.3'
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - ~>
|
24
|
+
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '1.3'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rake
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- -
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '0'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- -
|
38
|
+
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rspec
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- - ~>
|
45
|
+
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
47
|
version: '2.14'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- - ~>
|
52
|
+
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '2.14'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: benchmark-ips
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '2.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '2.0'
|
55
69
|
description: A small collection of caches with good performance and hash-like access
|
56
70
|
patterns
|
57
71
|
email:
|
@@ -60,8 +74,9 @@ executables: []
|
|
60
74
|
extensions: []
|
61
75
|
extra_rdoc_files: []
|
62
76
|
files:
|
63
|
-
- .gitignore
|
77
|
+
- ".gitignore"
|
64
78
|
- CHANGELOG.md
|
79
|
+
- CONTRIBUTING.md
|
65
80
|
- Gemfile
|
66
81
|
- LICENSE.txt
|
67
82
|
- README.md
|
@@ -75,6 +90,7 @@ files:
|
|
75
90
|
- lib/caches/lru.rb
|
76
91
|
- lib/caches/ttl.rb
|
77
92
|
- lib/caches/version.rb
|
93
|
+
- spec/benchmark_spec.rb
|
78
94
|
- spec/caches/linked_list_spec.rb
|
79
95
|
- spec/caches/lru_spec.rb
|
80
96
|
- spec/caches/ttl_spec.rb
|
@@ -90,21 +106,22 @@ require_paths:
|
|
90
106
|
- lib
|
91
107
|
required_ruby_version: !ruby/object:Gem::Requirement
|
92
108
|
requirements:
|
93
|
-
- -
|
109
|
+
- - ">="
|
94
110
|
- !ruby/object:Gem::Version
|
95
111
|
version: '0'
|
96
112
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
97
113
|
requirements:
|
98
|
-
- -
|
114
|
+
- - ">="
|
99
115
|
- !ruby/object:Gem::Version
|
100
116
|
version: '0'
|
101
117
|
requirements: []
|
102
118
|
rubyforge_project:
|
103
|
-
rubygems_version: 2.
|
119
|
+
rubygems_version: 2.2.2
|
104
120
|
signing_key:
|
105
121
|
specification_version: 4
|
106
122
|
summary: Caches with hash-like access
|
107
123
|
test_files:
|
124
|
+
- spec/benchmark_spec.rb
|
108
125
|
- spec/caches/linked_list_spec.rb
|
109
126
|
- spec/caches/lru_spec.rb
|
110
127
|
- spec/caches/ttl_spec.rb
|