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 +2 -1
- data/.rspec +0 -1
- data/README.md +86 -2
- data/activesupport-db-cache.gemspec +2 -1
- data/lib/active_support/cache/active_record_store.rb +67 -6
- data/spec/active_support/cache/active_record_store_spec.rb +81 -6
- data/spec/active_support/cache/debug_mode_spec.rb +90 -0
- data/spec/spec_helper.rb +10 -2
- metadata +21 -4
- data/log/test.log +0 -0
data/.gitignore
CHANGED
data/.rspec
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,17 @@
|
|
1
1
|
# Activesupport::Db::Cache
|
2
2
|
|
3
|
-
|
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
|
-
|
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.
|
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.
|
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
|
-
|
4
|
-
|
5
|
-
|
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
|
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
|
-
|
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
|
+
|
data/spec/spec_helper.rb
CHANGED
@@ -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
|
-
|
14
|
-
|
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.
|
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-
|
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
|
data/log/test.log
DELETED
File without changes
|