hash_cache 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 61ac041d1e693549eb5e98520b4e974b0f8197f7
4
- data.tar.gz: 42c22e0f28c1df9c7d91073e29455cab89c4075a
3
+ metadata.gz: dbcce2aa290c97f9ea5e548fec1cd869cf2b427a
4
+ data.tar.gz: dbca8cf97e3aefbda13e2d33576d193df35179b1
5
5
  SHA512:
6
- metadata.gz: 6ee6af4165117ee25f8f8c707aeba7c8a923ef9af0bea40af20ffe2c72f443391edbb81959157f97c97819468e001ec7d6d9ddf7c6a39409cbc5c35f482bf0bc
7
- data.tar.gz: f9be59948e4c22d7801f5e3055ba1ccdbb2c7a2d9b1a0bf12b4e8a933215db8abf5399b2575218bd9cd7fc6a10dd458e736d65a42928395587fd9ad03e3db96d
6
+ metadata.gz: b29355549ca2d293c4adb71f5281708af0ceb144f7d81538627d590bdc5d502d5e0610541fbc78855d7816b145ef8e11a11df7cc697c007cfee105e9d0986c10
7
+ data.tar.gz: be73b34d5de59cb592b0fff58b726520960c1bb80fce502b34e92df5d0bbd45bc3fde16020c07b979a77354c65fdbd9ab031fee104e67d794ddd06f20fd715e9
data/README.md CHANGED
@@ -1,70 +1,3 @@
1
- # HashCache
1
+ # Renamed
2
2
 
3
- HashCache is a small collection of hashes that cache data.
4
-
5
- ## Usage
6
-
7
- HashCache provides the following classes.
8
-
9
- ### HashCache::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 'hash_cache/ttl'
15
-
16
- h = HashCache::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 = HashCache::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
- ## Installation
43
-
44
- Add this line to your application's Gemfile:
45
-
46
- gem 'hash_cache'
47
-
48
- And then execute:
49
-
50
- $ bundle
51
-
52
- Or install it yourself as:
53
-
54
- $ gem install hash_cache
55
-
56
- ## Coming Soon
57
-
58
- Other classes with different cache expiration strategies.
59
-
60
- ## Contributing
61
-
62
- 1. Fork it
63
- 2. Create your feature branch (`git checkout -b my-new-feature`)
64
- 3. Commit your changes (`git commit -am 'Add some feature'`)
65
- 4. Push to the branch (`git push origin my-new-feature`)
66
- 5. Create new Pull Request
67
-
68
- # Thanks
69
-
70
- Original tests were adapted from [volatile_hash](https://github.com/satyap/volatile_hash).
3
+ See [https://github.com/nathanl/caches](https://github.com/nathanl/caches)
data/Rakefile CHANGED
@@ -6,4 +6,12 @@ RSpec::Core::RakeTask.new(:spec) do |t|
6
6
  t.rspec_opts = '--format documentation'
7
7
  end
8
8
 
9
+ task :console do
10
+ require 'irb'
11
+ require 'irb/completion'
12
+ require 'hash_cache/all'
13
+ ARGV.clear
14
+ IRB.start
15
+ end
16
+
9
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/hash_cache.gemspec CHANGED
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
8
8
  spec.version = HashCache::VERSION
9
9
  spec.authors = ["Nathan Long"]
10
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}
11
+ spec.description = %q{Renamed - see 'caches' gem}
12
+ spec.summary = %q{Renamed - see 'caches' gem}
13
13
  spec.homepage = "https://github.com/nathanl/hash_cache"
14
14
  spec.license = "MIT"
15
15
 
@@ -0,0 +1,19 @@
1
+ module HashCache
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
@@ -0,0 +1,2 @@
1
+ require_relative 'ttl'
2
+ require_relative 'lru'
@@ -0,0 +1,115 @@
1
+ module HashCache
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
@@ -0,0 +1,47 @@
1
+ require_relative 'accessible'
2
+ require_relative 'linked_list'
3
+
4
+ module HashCache
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
@@ -1,5 +1,8 @@
1
+ require_relative 'accessible'
2
+
1
3
  module HashCache
2
4
  class TTL
5
+ include Accessible
3
6
  attr_accessor :ttl, :refresh
4
7
 
5
8
  def initialize(options = {})
@@ -23,18 +26,8 @@ module HashCache
23
26
  data[key] = {time: current_time, value: val}
24
27
  end
25
28
 
26
- def fetch(key, default = (default_omitted = true; nil))
27
- return self[key] if data.has_key?(key)
28
- return default unless default_omitted
29
- return yield(key) if block_given?
30
- raise KeyError
31
- end
32
-
33
- def memoize(key)
34
- raise ArgumentError, "Block is required" unless block_given?
35
- self[key] # triggers flush or refresh if expired
36
- return self[key] if data.has_key?(key)
37
- self[key] = yield(key) if block_given?
29
+ def size
30
+ data.keys.length
38
31
  end
39
32
 
40
33
  private
@@ -1,3 +1,3 @@
1
1
  module HashCache
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -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/hash_cache/linked_list'
3
+
4
+ describe HashCache::LinkedList do
5
+
6
+ let(:empty_list) { described_class.new }
7
+ let(:list) { described_class.new('lemur') }
8
+ let(:node_class) { HashCache::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(HashCache::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/hash_cache/lru'
4
+
5
+ describe HashCache::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
+
@@ -1,4 +1,5 @@
1
1
  require 'spec_helper'
2
+ require 'fetch_examples'
2
3
  require_relative '../../lib/hash_cache/ttl'
3
4
 
4
5
  describe HashCache::TTL do
@@ -15,6 +16,10 @@ describe HashCache::TTL do
15
16
  let(:before_ttl) { start_time + 1800 }
16
17
  let(:after_ttl) { start_time + 3601 }
17
18
 
19
+ it "can report its size" do
20
+ expect(cache.size).to eq(1)
21
+ end
22
+
18
23
  it "remembers cached values before the TTL expires" do
19
24
  expect(cache[:c]).to eq('Caspian')
20
25
  cache.stub(:current_time).and_return(before_ttl)
@@ -61,29 +66,7 @@ describe HashCache::TTL do
61
66
 
62
67
  end
63
68
 
64
- describe "fetch" do
65
-
66
- it "returns the value if an existing key is accessed" do
67
- expect(cache.fetch(:c)).to eq('Caspian')
68
- end
69
-
70
- it "raises an error if a missing key is accessed" do
71
- expect{cache.fetch(:nonexistent)}.to raise_error(KeyError)
72
- end
73
-
74
- it "uses a default value if one is supplied" do
75
- expect(cache.fetch(:nonexistent, 'hi')).to eq('hi')
76
- end
77
-
78
- it "gets its default from a block if one is supplied" do
79
- expect(cache.fetch(:nonexistent) { |key| key.to_s.upcase }).to eq('NONEXISTENT')
80
- end
81
-
82
- it "uses the immediate value if both it and the block are supplied" do
83
- expect(cache.fetch(:nonexistent, 'hi') { |key| key.to_s.upcase }).to eq('hi')
84
- end
85
-
86
- end
69
+ include_examples "fetch"
87
70
 
88
71
  describe "memoize" do
89
72
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hash_cache
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan Long
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-01-17 00:00:00.000000000 Z
11
+ date: 2014-09-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -52,7 +52,7 @@ dependencies:
52
52
  - - ~>
53
53
  - !ruby/object:Gem::Version
54
54
  version: '2.14'
55
- description: A small collection of hashes that cache
55
+ description: Renamed - see 'caches' gem
56
56
  email:
57
57
  - nathanmlong@gmail.com
58
58
  executables: []
@@ -64,10 +64,18 @@ files:
64
64
  - LICENSE.txt
65
65
  - README.md
66
66
  - Rakefile
67
+ - TODO.md
67
68
  - hash_cache.gemspec
68
69
  - lib/hash_cache.rb
70
+ - lib/hash_cache/accessible.rb
71
+ - lib/hash_cache/all.rb
72
+ - lib/hash_cache/linked_list.rb
73
+ - lib/hash_cache/lru.rb
69
74
  - lib/hash_cache/ttl.rb
70
75
  - lib/hash_cache/version.rb
76
+ - spec/fetch_examples.rb
77
+ - spec/hash_cache/linked_list_spec.rb
78
+ - spec/hash_cache/lru_spec.rb
71
79
  - spec/hash_cache/ttl_spec.rb
72
80
  - spec/spec_helper.rb
73
81
  homepage: https://github.com/nathanl/hash_cache
@@ -93,7 +101,10 @@ rubyforge_project:
93
101
  rubygems_version: 2.0.14
94
102
  signing_key:
95
103
  specification_version: 4
96
- summary: A small collection of hashes that cache
104
+ summary: Renamed - see 'caches' gem
97
105
  test_files:
106
+ - spec/fetch_examples.rb
107
+ - spec/hash_cache/linked_list_spec.rb
108
+ - spec/hash_cache/lru_spec.rb
98
109
  - spec/hash_cache/ttl_spec.rb
99
110
  - spec/spec_helper.rb