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