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 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