activesupport-db-cache 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -10,9 +10,10 @@ coverage
10
10
  db/*.sqlite3
11
11
  doc/
12
12
  lib/bundler/man
13
+ log/*
13
14
  pkg
14
15
  rdoc
15
16
  spec/reports
16
17
  test/tmp
17
18
  test/version_tmp
18
- tmp
19
+ tmp/*
data/.rspec CHANGED
@@ -1,4 +1,3 @@
1
1
  --color
2
2
  --format documentation
3
- --backtrace
4
3
  --default_path spec
data/README.md CHANGED
@@ -1,6 +1,17 @@
1
1
  # Activesupport::Db::Cache
2
2
 
3
- TODO: Write a gem description
3
+ ActiveSupport::Cache store that stores data in a database table. Useful for NOT highload applications, when you do not want to use separate cache server like Memcache, Redis etc.. I am going to use it with Heroku application because of limitation of FREE cache storages.
4
+
5
+ Ruby on Rails support out of box.
6
+
7
+ ## Features
8
+
9
+ * Very LOW speed: think twice before use it! :)
10
+ * persistence: store cache after rails restart
11
+ * :expire_in option: cache lifetime for each item separetely
12
+ * total rows limit: auto clean old items when large (5000 rows max)
13
+ * ability to use external databse for cache store
14
+ * debug mode: ability to gather information about cache usage (access_time, access_counter)
4
15
 
5
16
  ## Installation
6
17
 
@@ -16,9 +27,82 @@ Or install it yourself as:
16
27
 
17
28
  $ gem install activesupport-db-cache
18
29
 
30
+ Set cache store in a config/environments/production.rb:
31
+
32
+ ..
33
+ config.cache_store = ActiveSupport::Cache::ActiveRecordStore.new
34
+ ..
35
+
36
+ Create migration for cache_items table:
37
+
38
+ $ rails g migration create_cache_items
39
+
40
+ Copy migration paste
41
+
42
+ def up
43
+ connection = ActiveSupport::Cache::ActiveRecordStore::CacheItem.connection
44
+ connection.create_table :cache_items do |t|
45
+ t.string :key
46
+ t.text :value
47
+ t.text :meta_info
48
+ t.datetime :expires_at
49
+ t.datetime :created_at
50
+ t.datetime :updated_at
51
+ end
52
+
53
+ connection.add_index :cache_items, :key, :unique => true
54
+ connection.add_index :cache_items, :expires_at
55
+ connection.add_index :cache_items, :updated_at
56
+ end
57
+
58
+ def down
59
+ ActiveSupport::Cache::ActiveRecordStore::CacheItem.connection.drop_table :cache_items
60
+ end
61
+
62
+ Migrate database:
63
+
64
+ $ rake db:migrate
65
+
66
+ Done!
67
+
19
68
  ## Usage
20
69
 
21
- TODO: Write usage instructions here
70
+ For usage details see ActiveSupport::Cache
71
+
72
+ ### External database
73
+
74
+ By default ActiveRecordStore uses your ActiveRecord::Base.connection. If you whant to use some external database for cache pupose you should set ACTIVE_RECORD_CACHE_STORE_DATABASE_URL env variable:
75
+
76
+ $ ACTIVE_RECORD_CACHE_STORE_DATABASE_URL="sqlite3://./db/test2.sqlite3" rails s
77
+
78
+ or postgres:
79
+
80
+ $ ACTIVE_RECORD_CACHE_STORE_DATABASE_URL="postgresql://user:password@host/database" rails s
81
+
82
+ or any other.
83
+
84
+ For Heroku you should add config variable:
85
+
86
+ $ heroku config:add ACTIVE_RECORD_CACHE_STORE_DATABASE_URL="postgresql://user:password@host/database"
87
+
88
+ ### Debug mode
89
+
90
+ To profile your application you can use debug mode. In debug mode additional meta information is gathered: access_counter, access_time. Be careful: debug mode is EXTREMELY SLOW. Do not use in production for a long time.
91
+
92
+ To enable debug mode you should set ACTIVE_RECORD_CACHE_STORE_DEBUG_MODE:
93
+
94
+ $ ACTIVE_RECORD_CACHE_STORE_DEBUG_MODE=1 rails s
95
+
96
+ For Heroku you should add config variable:
97
+
98
+ $ heroku config:add ACTIVE_RECORD_CACHE_STORE_DEBUG_MODE=1
99
+
100
+ Display debug information for a cache item:
101
+
102
+ $ rails c
103
+ > ActiveSupport::Cache::ActiveRecordStore::CacheItem.find_by_key(:foo).meta_info
104
+
105
+ >
22
106
 
23
107
  ## Contributing
24
108
 
@@ -7,7 +7,7 @@ Gem::Specification.new do |gem|
7
7
  gem.email = ["sergei.udalov@gmail.com"]
8
8
  gem.description = %q{ActiveRecord DB Store engine for ActiveSupport::Cache}
9
9
  gem.summary = %q{ActiveRecord DB Store engine for ActiveSupport::Cache}
10
- gem.homepage = ""
10
+ gem.homepage = "https://github.com/sergio-fry/activesupport-db-cache"
11
11
 
12
12
  gem.files = `git ls-files`.split($\)
13
13
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
@@ -20,4 +20,5 @@ Gem::Specification.new do |gem|
20
20
  gem.add_dependency "activerecord", ">=3.0.0"
21
21
  gem.add_development_dependency "rspec"
22
22
  gem.add_development_dependency "sqlite3"
23
+ gem.add_development_dependency "timecop"
23
24
  end
@@ -1,9 +1,13 @@
1
1
  require "base64"
2
+ require "ostruct"
2
3
 
3
4
  module ActiveSupport
4
5
  module Cache
5
6
  class ActiveRecordStore < Store
6
- VERSION = "0.0.2"
7
+ VERSION = "0.0.3"
8
+
9
+ # do not allow to store more items than
10
+ ITEMS_LIMIT = 5000
7
11
 
8
12
  # set database url:
9
13
  # ENV['ACTIVE_RECORD_CACHE_STORE_DATABASE_URL'] = "sqlite3://./db/test2.sqlite3"
@@ -11,12 +15,38 @@ module ActiveSupport
11
15
  self.table_name = ENV['ACTIVE_RECORD_CACHE_STORE_TABLE'] || 'cache_items'
12
16
  establish_connection ENV['ACTIVE_RECORD_CACHE_STORE_DATABASE_URL'] if ENV['ACTIVE_RECORD_CACHE_STORE_DATABASE_URL'].present?
13
17
 
18
+ cattr_accessor :debug_mode
19
+
20
+ serialize :meta_info, Hash
21
+ before_save :init_meta_info, :bump_version
22
+
23
+ def debug_mode?
24
+ debug_mode
25
+ end
26
+
27
+ DEFAULT_META_INFO = { :version => 0, :access_counter => 0 }
28
+
14
29
  def value
15
- Marshal.load(Base64.decode64(self[:value]))
30
+ Marshal.load(::Base64.decode64(self[:value])) if self[:value].present?
31
+ end
32
+
33
+ def value=(new_value)
34
+ @raw_value = new_value
35
+ self[:value] = ::Base64.encode64(Marshal.dump(@raw_value))
16
36
  end
17
37
 
18
38
  def expired?
19
- false
39
+ expires_at.try(:past?) || false
40
+ end
41
+
42
+ private
43
+
44
+ def bump_version
45
+ meta_info[:version] = meta_info[:version] + 1 if value_changed?
46
+ end
47
+
48
+ def init_meta_info
49
+ self.meta_info = DEFAULT_META_INFO.merge(meta_info)
20
50
  end
21
51
  end
22
52
 
@@ -28,14 +58,45 @@ module ActiveSupport
28
58
  CacheItem.delete_all(:key => key)
29
59
  end
30
60
 
31
- def read_entry(key, options)
32
- CacheItem.find_by_key(key)
61
+ def read_entry(key, options={})
62
+ item = CacheItem.find_by_key(key)
63
+
64
+ if item.present? && debug_mode?
65
+ item.meta_info[:access_counter] += 1
66
+ item.meta_info[:access_time] = Time.now
67
+ item.save
68
+ end
69
+
70
+ item
33
71
  end
34
72
 
35
73
  def write_entry(key, entry, options)
74
+ options = options.clone.symbolize_keys
36
75
  item = CacheItem.find_or_initialize_by_key(key)
37
- item.value = Base64.encode64(Marshal.dump(entry.value))
76
+ item.debug_mode = debug_mode?
77
+ item.value = entry.value
78
+ item.expires_at = options[:expires_in].try(:since)
38
79
  item.save
80
+
81
+ free_some_space
82
+ end
83
+
84
+ def debug_mode?
85
+ ENV['ACTIVE_RECORD_CACHE_STORE_DEBUG_MODE'] == "1"
86
+ end
87
+
88
+ private
89
+
90
+ def free_some_space
91
+ # free some space
92
+ if CacheItem.count >= ITEMS_LIMIT
93
+ # remove expired
94
+ CacheItem.where("expires_at < ?", Time.now).delete_all
95
+
96
+ # remove old items
97
+ oldest_updated_at = CacheItem.select(:updated_at).order(:updated_at).offset((ITEMS_LIMIT.to_f * 0.2).round).first.try(:updated_at)
98
+ CacheItem.where("updated_at < ?", oldest_updated_at).delete_all
99
+ end
39
100
  end
40
101
  end
41
102
  end
@@ -1,10 +1,21 @@
1
1
  require 'spec_helper.rb'
2
+ require 'ostruct'
2
3
 
3
- module ActiveSupport
4
- module Cache
5
- describe ActiveRecordStore do
4
+ include ActiveSupport::Cache
5
+
6
+ describe ActiveRecordStore do
7
+ def meta_info_for(key)
8
+ OpenStruct.new(ActiveRecordStore::CacheItem.find_by_key(key).meta_info)
9
+ end
10
+
11
+ before do
12
+ @store = ActiveRecordStore.new
13
+ end
14
+
15
+ [true, false].each do |debug_mode|
16
+ describe "cache use when debug_mode='#{debug_mode}'" do
6
17
  before do
7
- @store = ActiveRecordStore.new
18
+ @store.stub(:debug_mode?).and_return(debug_mode)
8
19
  end
9
20
 
10
21
  it "should store numbers" do
@@ -23,8 +34,26 @@ module ActiveSupport
23
34
  @store.read("foo")[:a].should eq(123)
24
35
  end
25
36
 
26
- it "should expire entries"
27
- it "should use LIMIT"
37
+ it "should expire entries" do
38
+ @store.write :foo, 123, :expires_in => 5.minutes
39
+ @store.read(:foo).should eq(123)
40
+ Timecop.travel 6.minutes.since do
41
+ @store.read(:foo).should be_blank
42
+ end
43
+ end
44
+
45
+ it "should use ITEMS_LIMIT" do
46
+ silence_warnings { ActiveRecordStore.const_set(:ITEMS_LIMIT, 10) }
47
+ @store.clear
48
+
49
+ 15.times do |i|
50
+ @store.write(i, 123)
51
+
52
+ ActiveRecordStore::CacheItem.count
53
+ end
54
+
55
+ ActiveRecordStore::CacheItem.count.should <= 10
56
+ end
28
57
 
29
58
  describe "#read" do
30
59
  before do
@@ -40,6 +69,24 @@ module ActiveSupport
40
69
  end
41
70
  end
42
71
 
72
+ describe "#fetch" do
73
+ it "should return calculate if missed" do
74
+ @store.delete(:foo)
75
+ obj = mock(:obj)
76
+ obj.should_receive(:func).and_return(123)
77
+
78
+ @store.fetch(:foo) { obj.func }.should eq(123)
79
+ end
80
+
81
+ it "should read data from cache if hit" do
82
+ @store.write(:foo, 123)
83
+ obj = mock(:obj)
84
+ obj.should_not_receive(:func)
85
+
86
+ @store.fetch(:foo) { obj.func }.should eq(123)
87
+ end
88
+ end
89
+
43
90
  describe "#clear" do
44
91
  it "should clear cache" do
45
92
  @store.write("foo", 123)
@@ -57,6 +104,34 @@ module ActiveSupport
57
104
  @store.read("foo").should be_blank
58
105
  end
59
106
  end
107
+
108
+ describe "cache item meta info" do
109
+ before { @store.clear }
110
+
111
+ describe "item version" do
112
+ it "should be 1 for a new cache item", :filter => true do
113
+ @store.write(:foo, "foo")
114
+ meta_info_for(:foo).version.should eq(1)
115
+ end
116
+
117
+ it "should be incremented after cache update" do
118
+ @store.write(:foo, "bar")
119
+ meta_info_for(:foo).version.should eq(1)
120
+
121
+ @store.write(:foo, "123")
122
+ meta_info_for(:foo).version.should eq(2)
123
+
124
+ @store.write(:foo, "hoo")
125
+ meta_info_for(:foo).version.should eq(3)
126
+ end
127
+
128
+ it "should not be incremented if no data change" do
129
+ @store.write(:foo, "bar")
130
+ @store.write(:foo, "bar")
131
+ meta_info_for(:foo).version.should eq(1)
132
+ end
133
+ end
134
+ end
60
135
  end
61
136
  end
62
137
  end
@@ -0,0 +1,90 @@
1
+ require 'spec_helper.rb'
2
+ require 'ostruct'
3
+
4
+ include ActiveSupport::Cache
5
+
6
+ describe ActiveRecordStore do
7
+ def meta_info_for(key)
8
+ OpenStruct.new(ActiveRecordStore::CacheItem.find_by_key(key).meta_info)
9
+ end
10
+
11
+ before do
12
+ @store = ActiveRecordStore.new
13
+ end
14
+
15
+ describe "debug mode" do
16
+ before { @store.clear }
17
+ context "debug mode is ON" do
18
+ before do
19
+ @store.stub(:debug_mode?).and_return(true)
20
+ end
21
+
22
+ describe "access_time" do
23
+ it "should be nil for a ne item" do
24
+ @store.write(:foo, 123)
25
+ meta_info_for(:foo).access_time.should be_nil
26
+ end
27
+
28
+ it "should store last access time" do
29
+ @store.write(:foo, 123)
30
+
31
+ atime = 10.minutes.ago
32
+
33
+ Timecop.freeze atime do
34
+ @store.read(:foo)
35
+ end
36
+
37
+ meta_info_for(:foo).access_time.should eq(atime)
38
+ end
39
+ end
40
+
41
+ describe "access_counter" do
42
+ it "should be 0 for a new item" do
43
+ @store.write(:foo, 123)
44
+ meta_info_for(:foo).access_counter.should eq(0)
45
+ end
46
+
47
+ it "should be incremented after each cache read" do
48
+ @store.write(:foo, 123)
49
+
50
+ @store.read(:foo)
51
+ meta_info_for(:foo).access_counter.should eq(1)
52
+
53
+ @store.read(:foo)
54
+ meta_info_for(:foo).access_counter.should eq(2)
55
+
56
+ @store.read(:foo)
57
+ meta_info_for(:foo).access_counter.should eq(3)
58
+ end
59
+ end
60
+ end
61
+
62
+ context "debug mode is OFF" do
63
+ before do
64
+ @store.stub(:debug_mode?).and_return(false)
65
+ end
66
+
67
+ describe "access_counter" do
68
+ it "should not be incremented after cache read" do
69
+ @store.write(:foo, 123)
70
+
71
+ @store.read(:foo)
72
+ meta_info_for(:foo).access_counter.should eq(0)
73
+
74
+ @store.read(:foo)
75
+ meta_info_for(:foo).access_counter.should eq(0)
76
+ end
77
+ end
78
+
79
+ describe "access_time" do
80
+ it "should not be updated" do
81
+ @store.write(:foo, 123)
82
+ @store.read(:foo)
83
+ meta_info_for(:foo).access_time.should be_nil
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+
@@ -1,17 +1,25 @@
1
1
  require 'rubygems'
2
2
  require 'bundler/setup'
3
3
  require 'activesupport-db-cache'
4
+ require 'timecop'
4
5
 
5
6
  require "sqlite3"
6
7
  ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => 'db/test.sqlite3')
7
8
  ActiveRecord::Base.connection.create_table(:cache_items, :force => true) do |t|
8
9
  t.column :key, :string
9
10
  t.column :value, :text
11
+ t.column :meta_info, :text
12
+ t.column :expires_at, :datetime
13
+ t.column :created_at, :datetime
14
+ t.column :updated_at, :datetime
10
15
  end
11
16
  ActiveRecord::Base.connection.add_index(:cache_items, :key, :unique => true)
17
+ ActiveRecord::Base.connection.add_index(:cache_items, :created_at)
18
+ ActiveRecord::Base.connection.add_index(:cache_items, :updated_at)
12
19
 
13
- #logfile = File.open(File.expand_path("../../log/test.log", __FILE__), 'a')
14
- #ActiveSupport::Cache::ActiveRecordStore.logger = Logger.new(logfile)
20
+ require 'logger'
21
+ logfile = File.open(File.expand_path("../../log/test.log", __FILE__), 'a')
22
+ ActiveRecord::Base.logger = Logger.new(logfile)
15
23
 
16
24
  Dir['./spec/support/*.rb'].map {|f| require f }
17
25
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activesupport-db-cache
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-01-21 00:00:00.000000000 Z
12
+ date: 2013-01-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -75,6 +75,22 @@ dependencies:
75
75
  - - ! '>='
76
76
  - !ruby/object:Gem::Version
77
77
  version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: timecop
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
78
94
  description: ActiveRecord DB Store engine for ActiveSupport::Cache
79
95
  email:
80
96
  - sergei.udalov@gmail.com
@@ -91,11 +107,11 @@ files:
91
107
  - activesupport-db-cache.gemspec
92
108
  - lib/active_support/cache/active_record_store.rb
93
109
  - lib/activesupport-db-cache.rb
94
- - log/test.log
95
110
  - spec/active_support/cache/active_record_store_spec.rb
111
+ - spec/active_support/cache/debug_mode_spec.rb
96
112
  - spec/spec_helper.rb
97
113
  - tasks/rspec.task
98
- homepage: ''
114
+ homepage: https://github.com/sergio-fry/activesupport-db-cache
99
115
  licenses: []
100
116
  post_install_message:
101
117
  rdoc_options: []
@@ -121,4 +137,5 @@ specification_version: 3
121
137
  summary: ActiveRecord DB Store engine for ActiveSupport::Cache
122
138
  test_files:
123
139
  - spec/active_support/cache/active_record_store_spec.rb
140
+ - spec/active_support/cache/debug_mode_spec.rb
124
141
  - spec/spec_helper.rb
File without changes