fat_cache 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|