fat_cache 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
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
@@ -0,0 +1,9 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require 'fat_cache'
4
+ require 'spec'
5
+ require 'spec/autorun'
6
+
7
+ Spec::Runner.configure do |config|
8
+
9
+ end
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