fat_cache 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.md +86 -0
- data/Rakefile +45 -0
- data/VERSION +1 -0
- data/lib/fat_cache.rb +123 -0
- data/spec/fat_cache_spec.rb +142 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +9 -0
- metadata +85 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 phinze
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
fat_cache
|
2
|
+
=========
|
3
|
+
|
4
|
+
Data migration got you down? RAM to spare? Let `fat_cache` do the work for you.
|
5
|
+
`fat_cache` wastes resources for the sake of speed, intentionally!
|
6
|
+
|
7
|
+
Use Case
|
8
|
+
========
|
9
|
+
|
10
|
+
Say you are importing bank accounts associated with your users from an old
|
11
|
+
system, maybe 10,000 of them.
|
12
|
+
|
13
|
+
Naive Implementation
|
14
|
+
--------------------
|
15
|
+
|
16
|
+
You might write code that looks something like this:
|
17
|
+
|
18
|
+
old_accounts = legacy_db.select_all("select * from old_accounts")
|
19
|
+
|
20
|
+
old_accounts.each do |account_data|
|
21
|
+
user = User.find_by_user_number(account_data['user_number'], :include => :accounts)
|
22
|
+
next if user.accounts.find { |account| account.number == account_data['account_number'] }
|
23
|
+
# Save imported account
|
24
|
+
acct = Account.new(account_data)
|
25
|
+
acct.save!
|
26
|
+
end
|
27
|
+
|
28
|
+
But this is slow, two queries for each of your 10,000 accounts.
|
29
|
+
|
30
|
+
Refactor One: Fat Query
|
31
|
+
-----------------------
|
32
|
+
|
33
|
+
You can attack the speed problem by loading all your users into memory first.
|
34
|
+
You pay for a fat query up front, but you get a speed boost afterwards.
|
35
|
+
|
36
|
+
old_accounts = legacy_db.select_all("select * from old_accounts")
|
37
|
+
|
38
|
+
all_users = Users.all(:include => :accounts)
|
39
|
+
|
40
|
+
old_accounts.each do |account_data|
|
41
|
+
user = all_users.find { |user| user.user_number == account_data['user_number'] }
|
42
|
+
# ... (same as above) ...
|
43
|
+
end
|
44
|
+
|
45
|
+
But now instead of spending all your time in the network stack doing queries,
|
46
|
+
you're spinning the CPU doing a linear search through the `all_users` array.
|
47
|
+
|
48
|
+
Refactor Two: Indexed Hash
|
49
|
+
--------------------------
|
50
|
+
|
51
|
+
A similar "pay up front, gain later" strategy can be used on the in-memory data
|
52
|
+
structure by indexing it on the key that we will be searching on.
|
53
|
+
|
54
|
+
|
55
|
+
old_accounts = legacy_db.select_all("select * from old_accounts")
|
56
|
+
all_users = Users.all(:include => :accounts)
|
57
|
+
|
58
|
+
all_users_indexed_by_user_number = all_users.inject({}) do |hash, user|
|
59
|
+
hash[user.user_number] = user
|
60
|
+
hash
|
61
|
+
end
|
62
|
+
|
63
|
+
old_accounts.each do |account_data|
|
64
|
+
user = all_user_by_user_number[account_data['user_number']]
|
65
|
+
# ... (same as above) ...
|
66
|
+
end
|
67
|
+
|
68
|
+
Now finding a user for an account is constant time lookup in the hash.
|
69
|
+
|
70
|
+
FatCache makes this strategy simpler
|
71
|
+
------------------------------------
|
72
|
+
|
73
|
+
FatCache is a simple abstraction and encapsulation of the strategies used in
|
74
|
+
each refactor. Here is how the code looks:
|
75
|
+
|
76
|
+
FatCache.store(:users) { Users.all(:include => :accounts) }
|
77
|
+
FatCache.index(:users, :user_number)
|
78
|
+
|
79
|
+
old_accounts.each do |account_data|
|
80
|
+
user = FatCache.lookup :users, :by => :user_number, :using => account_data['user_number']
|
81
|
+
# ... (same as above) ...
|
82
|
+
end
|
83
|
+
|
84
|
+
And in fact, the call to `index` is optional, since `lookup` will create the
|
85
|
+
index the first time you call it if one doesn't exist, and you're still only
|
86
|
+
paying O(N) once.
|
data/Rakefile
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "fat_cache"
|
8
|
+
gem.summary = %Q{A dead simple pure-ruby caching framework for large datasets.}
|
9
|
+
gem.description = %Q{A dead simple pure-ruby caching framework for large datasets.}
|
10
|
+
gem.email = "paul.t.hinze@gmail.com"
|
11
|
+
gem.homepage = "http://github.com/phinze/fat_cache"
|
12
|
+
gem.authors = ["phinze"]
|
13
|
+
gem.add_development_dependency "rspec", ">= 1.2.9"
|
14
|
+
gem.add_development_dependency "yard", ">= 0"
|
15
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
16
|
+
end
|
17
|
+
Jeweler::GemcutterTasks.new
|
18
|
+
rescue LoadError
|
19
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
20
|
+
end
|
21
|
+
|
22
|
+
require 'spec/rake/spectask'
|
23
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
24
|
+
spec.libs << 'lib' << 'spec'
|
25
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
26
|
+
end
|
27
|
+
|
28
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
29
|
+
spec.libs << 'lib' << 'spec'
|
30
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
31
|
+
spec.rcov = true
|
32
|
+
end
|
33
|
+
|
34
|
+
task :spec => :check_dependencies
|
35
|
+
|
36
|
+
task :default => :spec
|
37
|
+
|
38
|
+
begin
|
39
|
+
require 'yard'
|
40
|
+
YARD::Rake::YardocTask.new
|
41
|
+
rescue LoadError
|
42
|
+
task :yardoc do
|
43
|
+
abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
|
44
|
+
end
|
45
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.3
|
data/lib/fat_cache.rb
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'ruby-debug'
|
3
|
+
class FatCache
|
4
|
+
|
5
|
+
class << self
|
6
|
+
@initted = false
|
7
|
+
attr_accessor :fetchers, :fatcache, :indexed_fatcache
|
8
|
+
|
9
|
+
def store(*key_and_maybe_data, &fetcher)
|
10
|
+
init unless initted? # for first time store
|
11
|
+
|
12
|
+
if key_and_maybe_data.length == 2 && fetcher.nil?
|
13
|
+
key, data = key_and_maybe_data
|
14
|
+
fatcache[key] = data
|
15
|
+
elsif key_and_maybe_data.length == 1 && fetcher
|
16
|
+
key = key_and_maybe_data.first
|
17
|
+
fetchers[key] = fetcher
|
18
|
+
fatcache[key] = fetcher.call
|
19
|
+
else
|
20
|
+
argstr = "#{key_and_maybe_data.length} arguments"
|
21
|
+
blockstr = (block_given?) ? 'a block' : 'no block'
|
22
|
+
raise "Got #{argstr} and #{blockstr}, expected (key, data) or (key) { fetcher block }"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def get(key)
|
27
|
+
unless cached?(key)
|
28
|
+
if fetchable?(key)
|
29
|
+
fatcache[key] = fetchers[key].call
|
30
|
+
else
|
31
|
+
raise "no data for #{key}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
fatcache[key]
|
35
|
+
end
|
36
|
+
|
37
|
+
def lookup(key, options={})
|
38
|
+
options = options.dup
|
39
|
+
by = [*options.delete(:by)]
|
40
|
+
using = [*options.delete(:using)]
|
41
|
+
|
42
|
+
# create index if it doesn't exist
|
43
|
+
index(key, by) unless indexed?(key, by)
|
44
|
+
|
45
|
+
return indexed_fatcache[key][by][using]
|
46
|
+
end
|
47
|
+
|
48
|
+
def index(key, on)
|
49
|
+
# must have cache data to work with
|
50
|
+
ensure_cached(key)
|
51
|
+
|
52
|
+
# ensure we're dealing with an array, we're such a friendly API!
|
53
|
+
on = [*on]
|
54
|
+
|
55
|
+
# init hash if we've never indexed for this key before
|
56
|
+
indexed_fatcache[key] = {} unless indexed_fatcache.has_key?(key)
|
57
|
+
|
58
|
+
raw_data = get(key)
|
59
|
+
|
60
|
+
# calls each method specified in the `on` array once on each element in
|
61
|
+
# the raw dataset, and uses the results of those calls to key this index
|
62
|
+
indexed_fatcache[key][on] = raw_data.group_by { |x| on.map { |b| x.send(b) } }
|
63
|
+
end
|
64
|
+
|
65
|
+
def get_index(key, on)
|
66
|
+
on = [*on]
|
67
|
+
|
68
|
+
ensure_indexed(key, on)
|
69
|
+
|
70
|
+
indexed_fatcache[key][on]
|
71
|
+
end
|
72
|
+
|
73
|
+
def ensure_cached(key)
|
74
|
+
raise "no data for #{key}" unless cached?(key)
|
75
|
+
end
|
76
|
+
|
77
|
+
def ensure_indexed(key, on)
|
78
|
+
ensure_cached(key)
|
79
|
+
raise "no index for #{key} on #{on.inspect}" unless indexed?(key, on)
|
80
|
+
end
|
81
|
+
|
82
|
+
def cached?(key)
|
83
|
+
fatcache && fatcache.has_key?(key)
|
84
|
+
end
|
85
|
+
|
86
|
+
def indexed?(key, on)
|
87
|
+
indexed_fatcache &&
|
88
|
+
indexed_fatcache.has_key?(key) &&
|
89
|
+
indexed_fatcache[key].has_key?(on)
|
90
|
+
end
|
91
|
+
|
92
|
+
def fetchable?(key)
|
93
|
+
fetchers && fetchers.has_key?(key)
|
94
|
+
end
|
95
|
+
|
96
|
+
def invalidate(key)
|
97
|
+
init unless initted?
|
98
|
+
|
99
|
+
fatcache.delete(key)
|
100
|
+
end
|
101
|
+
|
102
|
+
def reset!
|
103
|
+
self.fetchers = nil
|
104
|
+
self.fatcache = nil
|
105
|
+
self.indexed_fatcache = nil
|
106
|
+
@initted = nil
|
107
|
+
end
|
108
|
+
|
109
|
+
protected
|
110
|
+
|
111
|
+
def init
|
112
|
+
self.fetchers = {}
|
113
|
+
self.fatcache = {}
|
114
|
+
self.indexed_fatcache = {}
|
115
|
+
@initted = true
|
116
|
+
end
|
117
|
+
|
118
|
+
def initted?
|
119
|
+
@initted == true
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe FatCache do
|
4
|
+
# so we don't have leaky state
|
5
|
+
after { FatCache.reset! }
|
6
|
+
|
7
|
+
describe 'get(key)' do
|
8
|
+
it 'raises an exception if no data is stored for this key' do
|
9
|
+
lambda {
|
10
|
+
FatCache.get(:not_there)
|
11
|
+
}.should raise_error(/not_there/)
|
12
|
+
end
|
13
|
+
|
14
|
+
describe 'when data has been invalidated for a key with a fetcher' do
|
15
|
+
it 'uses a fetcher to get new data' do
|
16
|
+
i = 0
|
17
|
+
FatCache.store(:increment) { i += 1 }
|
18
|
+
FatCache.invalidate(:increment)
|
19
|
+
FatCache.get(:increment).should == 2
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe 'store(key[, data])' do
|
25
|
+
describe 'when called with key and data arguments' do
|
26
|
+
it 'stores data for specified key' do
|
27
|
+
FatCache.store(:five_alive, 5)
|
28
|
+
FatCache.get(:five_alive).should == 5
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'properly stores nil for key if explicitly specified' do
|
32
|
+
FatCache.store(:empty_inside, nil)
|
33
|
+
FatCache.get(:empty_inside).should be_nil
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe 'when called with key and fetcher block' do
|
38
|
+
it 'uses block as fetcher to retrieve data to store' do
|
39
|
+
FatCache.store(:fetched_from_block) { 'cheese sandwich' }
|
40
|
+
FatCache.get(:fetched_from_block).should == 'cheese sandwich'
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'calls the block only once, caching the data returned' do
|
44
|
+
i = 0
|
45
|
+
FatCache.store(:increment) { i += 1 }
|
46
|
+
|
47
|
+
first_result = FatCache.get(:increment)
|
48
|
+
second_result = FatCache.get(:increment)
|
49
|
+
|
50
|
+
first_result.should == 1
|
51
|
+
second_result.should == 1
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe 'lookup(key, :by => [:method_names], :using => [:index_key])' do
|
57
|
+
it 'returns a records stored in the dataset specified by key, indexed by the specified methods, and with the following key to the index' do
|
58
|
+
FatCache.store(:a_set, [0,1,2,3,4,5])
|
59
|
+
result = FatCache.lookup(:a_set, :by => :odd?, :using => true)
|
60
|
+
result.should == [1,3,5]
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'works with multi-element index keys' do
|
64
|
+
FatCache.store(:a_set, [0,1,2,3,4,5])
|
65
|
+
result = FatCache.lookup(:a_set, :by => [:even?, :zero?], :using => [true, true])
|
66
|
+
result.should == [0]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe 'get_index(key, on)' do
|
71
|
+
it 'returns the given index for a key' do
|
72
|
+
FatCache.store(:numbers, [0,1,2,3,4])
|
73
|
+
FatCache.index(:numbers, :odd?)
|
74
|
+
FatCache.get_index(:numbers, :odd?).should be_a Hash
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'raises an error if no index exists for on specified key' do
|
78
|
+
FatCache.store(:indexed_one_way, [123])
|
79
|
+
FatCache.index(:indexed_one_way, :zero?)
|
80
|
+
lambda {
|
81
|
+
FatCache.get_index(:indexed_one_way, :odd?)
|
82
|
+
}.should raise_error(/indexed_one_way.*odd?/)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe 'index(key, on)' do
|
87
|
+
it 'raises an error if there is no raw data to index for specified key' do
|
88
|
+
lambda {
|
89
|
+
FatCache.index(:wishbone, :whats_the_story?)
|
90
|
+
}.should raise_error(/wishbone/)
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'raises an error if the elements of the dataset do not respond to the index key methods' do
|
94
|
+
FatCache.store(:numbers, [1,2,3,4,5,6])
|
95
|
+
lambda {
|
96
|
+
FatCache.index(:numbers, :millionaire?)
|
97
|
+
}.should raise_error(/millionaire?/)
|
98
|
+
end
|
99
|
+
|
100
|
+
describe 'given a dataset which responds to methods' do
|
101
|
+
before do
|
102
|
+
fruit = [
|
103
|
+
stub(:mango, :grams_of_awesome => 3),
|
104
|
+
stub(:banana, :grams_of_awesome => 10),
|
105
|
+
stub(:apple, :grams_of_awesome => 3)
|
106
|
+
]
|
107
|
+
FatCache.store(:fruit, fruit)
|
108
|
+
end
|
109
|
+
it 'calls each method specified in `on` array and uses the results as the index key' do
|
110
|
+
FatCache.index(:fruit, [:grams_of_awesome])
|
111
|
+
index = FatCache.get_index(:fruit, :grams_of_awesome)
|
112
|
+
index.keys.should =~ [[3],[10]]
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
describe 'invalidate(key)' do
|
118
|
+
describe 'when no fetcher has been specified for key' do
|
119
|
+
it 'returns the last value the cache had for the key' do
|
120
|
+
FatCache.store(:once_upon_a_time, 33)
|
121
|
+
retval = FatCache.invalidate(:once_upon_a_time)
|
122
|
+
retval.should == 33
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'removes data stored for a given key' do
|
126
|
+
FatCache.store(:there_and_gone, 100)
|
127
|
+
FatCache.invalidate(:there_and_gone)
|
128
|
+
lambda {
|
129
|
+
FatCache.get(:there_and_gone)
|
130
|
+
}.should raise_error(/there_and_gone/)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
describe 'when a fetcher has been specified for a key' do
|
135
|
+
it 'does not clear out the fetcher, which can be used in the next lookup' do
|
136
|
+
FatCache.store(:fetch_me) { "I've been fetched" }
|
137
|
+
FatCache.invalidate(:fetch_me)
|
138
|
+
FatCache.get(:fetch_me).should == "I've been fetched"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fat_cache
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- phinze
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-03-02 00:00:00 -06:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rspec
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 1.2.9
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: yard
|
27
|
+
type: :development
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "0"
|
34
|
+
version:
|
35
|
+
description: A dead simple pure-ruby caching framework for large datasets.
|
36
|
+
email: paul.t.hinze@gmail.com
|
37
|
+
executables: []
|
38
|
+
|
39
|
+
extensions: []
|
40
|
+
|
41
|
+
extra_rdoc_files:
|
42
|
+
- LICENSE
|
43
|
+
- README.md
|
44
|
+
files:
|
45
|
+
- .document
|
46
|
+
- .gitignore
|
47
|
+
- LICENSE
|
48
|
+
- README.md
|
49
|
+
- Rakefile
|
50
|
+
- VERSION
|
51
|
+
- lib/fat_cache.rb
|
52
|
+
- spec/fat_cache_spec.rb
|
53
|
+
- spec/spec.opts
|
54
|
+
- spec/spec_helper.rb
|
55
|
+
has_rdoc: true
|
56
|
+
homepage: http://github.com/phinze/fat_cache
|
57
|
+
licenses: []
|
58
|
+
|
59
|
+
post_install_message:
|
60
|
+
rdoc_options:
|
61
|
+
- --charset=UTF-8
|
62
|
+
require_paths:
|
63
|
+
- lib
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: "0"
|
69
|
+
version:
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: "0"
|
75
|
+
version:
|
76
|
+
requirements: []
|
77
|
+
|
78
|
+
rubyforge_project:
|
79
|
+
rubygems_version: 1.3.5
|
80
|
+
signing_key:
|
81
|
+
specification_version: 3
|
82
|
+
summary: A dead simple pure-ruby caching framework for large datasets.
|
83
|
+
test_files:
|
84
|
+
- spec/fat_cache_spec.rb
|
85
|
+
- spec/spec_helper.rb
|