activesupport-db-cache 0.0.2 → 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/.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