mongo_cache_store 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +102 -0
- data/Rakefile +1 -0
- data/lib/active_support/cache/mongo_cache_store.rb +41 -0
- data/lib/active_support/cache/mongo_cache_store/backend/base.rb +111 -0
- data/lib/active_support/cache/mongo_cache_store/backend/capped.rb +44 -0
- data/lib/active_support/cache/mongo_cache_store/backend/multi_ttl.rb +136 -0
- data/lib/active_support/cache/mongo_cache_store/backend/standard.rb +65 -0
- data/lib/active_support/cache/mongo_cache_store/backend/ttl.rb +65 -0
- data/lib/mongo_cache_store/version.rb +4 -0
- data/mongo_cache_store.gemspec +23 -0
- data/spec/active_support/cache/mongo_cache_store_spec.rb +160 -0
- data/spec/spec_helper.rb +6 -0
- metadata +110 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Kevin McGrath
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
# MongoCacheStore
|
2
|
+
|
3
|
+
A MongoDB cache store for ActiveSupport 3
|
4
|
+
|
5
|
+
## Description
|
6
|
+
|
7
|
+
MongoCacheStore uses pluggable backends to expose MongoDB as a cache store to ActiveSupport applications. Each backend allows the application to customize how the cache operates. Support is available for standard, capped and TTL collections.
|
8
|
+
|
9
|
+
**WARNING** This gem is in the early stages of development and should be treated as such. Checking the version of the gem will help with what could be many changes to the backends, options, etc...
|
10
|
+
|
11
|
+
While in beta, the major version will always be 0. The minor version will be increased for anything that breaks the current API. The patch version will be increased for all changes within a minor revision that add to or fix the current release without changing how the gem is configured or used.
|
12
|
+
|
13
|
+
|
14
|
+
## Backends
|
15
|
+
|
16
|
+
A Backend controls how MongoDB collections are used and manipulated as a cache store.
|
17
|
+
|
18
|
+
MongoCacheStore ships with 4 backends, TTL, Capped, Standard and MultiTTL.
|
19
|
+
|
20
|
+
Planned: GridFS
|
21
|
+
* For very large entries. Ex: Generate a large report (PDF, DOC, etc...), keep it around for a limited amount of time and have it automatically flush upon expiration.
|
22
|
+
|
23
|
+
|
24
|
+
The major difference between each backend involves how entries are flushed. The core driver will always respect ActiveSupport's *:expires_in* parameter for hits and misses whether the entry has actually been flushed from the backend or not.
|
25
|
+
|
26
|
+
#### Core Options
|
27
|
+
From ActiveSupport
|
28
|
+
* :namespace
|
29
|
+
* :expires_in
|
30
|
+
|
31
|
+
For MongoCacheStore
|
32
|
+
* :db - A Mongo::DB instance
|
33
|
+
* :db_name - Name of database to be created. Only used if *db* is not set
|
34
|
+
* :connection - A Mongo::Connection. Only used if *db* is not set. Defaults to Mongo::Connection.new()
|
35
|
+
* serialize: (*:always*, :on\_fail, :never)
|
36
|
+
* :always (default) - Serialize all entries
|
37
|
+
* :on\_fail - Try to save the entry in a native MongoDB format. If that fails, then serialize the entry.
|
38
|
+
* :never - only save the entry if it can be saved natively by MongoDB.
|
39
|
+
* *NOTE:* Without serialization class structures and instances that cannot be converted to a native MongoDB type will not be stored. Also, without serialization MongoDB converts all symbols to strings. Therefore a hash with symbols as keys will have strings as keys when read.
|
40
|
+
|
41
|
+
|
42
|
+
### TTL
|
43
|
+
Entries are kept in a namespaced TTL collection that will automatically flush any entries as they pass their expiration time. This keeps the size of the cache in check over time.
|
44
|
+
|
45
|
+
* *Requires MongoDB 2.0 or higher*
|
46
|
+
|
47
|
+
#### Options
|
48
|
+
No Extra options
|
49
|
+
|
50
|
+
### Standard
|
51
|
+
Entreis are kept in a namespaced MongoDB collection. In a standard collection entries are only flushed from the collection with an explicit delete call or if auto_flush is enabled. If auto_flush is enabled the cache will flush all expired entries when auto\_flush\_threshold is reached. The threshold is based on a set number of cache instance writes.
|
52
|
+
|
53
|
+
#### Options
|
54
|
+
* auto_flush: (*true*|false)
|
55
|
+
* auto\_flush\_threshold: *10_000*
|
56
|
+
|
57
|
+
|
58
|
+
### Capped (TODO)
|
59
|
+
This should only be used if limiting the size of the cache is the greatest concern. Entries are flushed from the cache on a FIFO basis, regardless of the entries expiration time. Delete operations set an entry to expired, but it will not be flushed until it is automatically removed by MongoDB.
|
60
|
+
|
61
|
+
|
62
|
+
### MultiTTL
|
63
|
+
Entries are stored in multiple namespaced TTL collections. A namespaced TTL collection is created for each unique expiration time. For example all entries with an expiration time of 300 seconds will be kept in the same collection while entries with a 900 second expiration time will be kept in another. This requires the use of a *key index* collection that keeps track of which TTL collection a entry resides in.
|
64
|
+
|
65
|
+
#### Downsides
|
66
|
+
* Cache set operations require 2 MongoDB write calls. One for the key index, one for the TTL collection. (unless *use_index* is false, see below)
|
67
|
+
* Cache read operations will require 1 or 2 MongoDB calls depending on whether the 'expires_in' option is set for the read.
|
68
|
+
|
69
|
+
#### Benefits
|
70
|
+
* Ability to flush cache based on expire time (TODO)
|
71
|
+
|
72
|
+
#### Options
|
73
|
+
use\_index: (*true*, false)
|
74
|
+
* This should only be set to *false* if all fetch and/or read operations are passed the *:expires_in* option. If so, this will eliminate the need for the key index collection and only one write and one read operation is necessary.
|
75
|
+
|
76
|
+
|
77
|
+
|
78
|
+
## Installation
|
79
|
+
|
80
|
+
Add this line to your application's Gemfile:
|
81
|
+
|
82
|
+
gem 'mongo_cache_store'
|
83
|
+
|
84
|
+
And then execute:
|
85
|
+
|
86
|
+
$ bundle
|
87
|
+
|
88
|
+
Or install it yourself as:
|
89
|
+
|
90
|
+
$ gem install mongo_cache_store
|
91
|
+
|
92
|
+
## Usage
|
93
|
+
|
94
|
+
|
95
|
+
|
96
|
+
## Contributing
|
97
|
+
|
98
|
+
1. Fork it
|
99
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
100
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
101
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
102
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require "mongo_cache_store/version"
|
3
|
+
require "mongo"
|
4
|
+
require "active_support/cache"
|
5
|
+
|
6
|
+
module ActiveSupport
|
7
|
+
module Cache
|
8
|
+
|
9
|
+
class MongoCacheStore < Store
|
10
|
+
|
11
|
+
def initialize (backend=:Standard, options = {})
|
12
|
+
|
13
|
+
options = {
|
14
|
+
:db_name => 'cache_store',
|
15
|
+
:db => nil,
|
16
|
+
:namespace => nil,
|
17
|
+
:connection => nil,
|
18
|
+
:serialize => :always
|
19
|
+
}.merge(options)
|
20
|
+
|
21
|
+
@db = options.delete :db
|
22
|
+
|
23
|
+
if (@db.nil?)
|
24
|
+
@db = Mongo::DB.new(options[:db_name], options[:connection] || Mongo::Connection.new)
|
25
|
+
end
|
26
|
+
|
27
|
+
extend ActiveSupport::Cache::MongoCacheStore::Backend.const_get(backend)
|
28
|
+
|
29
|
+
build_backend(options)
|
30
|
+
|
31
|
+
super(options)
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
require "active_support/cache/mongo_cache_store/backend/capped"
|
39
|
+
require "active_support/cache/mongo_cache_store/backend/standard"
|
40
|
+
require "active_support/cache/mongo_cache_store/backend/ttl"
|
41
|
+
require "active_support/cache/mongo_cache_store/backend/multi_ttl"
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
|
3
|
+
module ActiveSupport
|
4
|
+
module Cache
|
5
|
+
class MongoCacheStore
|
6
|
+
module Backend
|
7
|
+
# Base methods used by all MongoCacheStore backends
|
8
|
+
module Base
|
9
|
+
|
10
|
+
# No public methods are defined for this module
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def expand_key(key)
|
15
|
+
return key
|
16
|
+
end
|
17
|
+
|
18
|
+
def namespaced_key(key, options)
|
19
|
+
return key
|
20
|
+
end
|
21
|
+
|
22
|
+
def read_entry(key,options)
|
23
|
+
col = get_collection(options)
|
24
|
+
|
25
|
+
safe_rescue do
|
26
|
+
query = {
|
27
|
+
:_id => key,
|
28
|
+
:expires_at => {
|
29
|
+
'$gt' => Time.now
|
30
|
+
}
|
31
|
+
}
|
32
|
+
|
33
|
+
response = col.find_one(query)
|
34
|
+
return nil if response.nil?
|
35
|
+
|
36
|
+
entry_options = {
|
37
|
+
:compressed => response[:compressed],
|
38
|
+
:expires_in => response[:expires_in]
|
39
|
+
}
|
40
|
+
if response['serialized']
|
41
|
+
r_value = response['value'].to_s
|
42
|
+
else
|
43
|
+
r_value = Marshal.dump(response['value'])
|
44
|
+
end
|
45
|
+
ActiveSupport::Cache::Entry.create(r_value,response[:created_at],entry_options)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
def write_entry(key,entry,options)
|
51
|
+
col = get_collection(options)
|
52
|
+
serialize = options[:serialize] == :always ? true : false
|
53
|
+
try_cnt = 0
|
54
|
+
|
55
|
+
save_doc = {
|
56
|
+
:_id => key,
|
57
|
+
:created_at => Time.at(entry.created_at),
|
58
|
+
:expires_in => entry.expires_in,
|
59
|
+
:expires_at => entry.expires_in.nil? ? Time.utc(9999) : Time.at(entry.expires_at),
|
60
|
+
:compressed => entry.compressed?,
|
61
|
+
:serialized => serialize,
|
62
|
+
:value => serialize ? BSON::Binary.new(entry.raw_value) : entry.value
|
63
|
+
}.merge(options[:xentry] || {})
|
64
|
+
#puts save_doc.inspect
|
65
|
+
|
66
|
+
safe_rescue do
|
67
|
+
begin
|
68
|
+
col.save(save_doc)
|
69
|
+
rescue BSON::InvalidDocument => ex
|
70
|
+
if (options[:serialize] == :on_fail and try_cnt < 2)
|
71
|
+
save_doc[:serialized] = true
|
72
|
+
save_doc[:value] = BSON::Binary.new(entry.raw_value)
|
73
|
+
try_cnt += 1
|
74
|
+
retry
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def delete_entry(key,options)
|
81
|
+
col = get_collection(options)
|
82
|
+
safe_rescue do
|
83
|
+
col.remove({'_id' => key})
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def get_collection_name(options = {})
|
88
|
+
name_parts = ['cache']
|
89
|
+
name_parts.push(backend_name)
|
90
|
+
name_parts.push options[:namespace] unless options[:namespace].nil?
|
91
|
+
return name_parts.join('.')
|
92
|
+
end
|
93
|
+
|
94
|
+
def get_collection(options)
|
95
|
+
return options[:collection] if options[:collection].is_a? Mongo::Collection
|
96
|
+
end
|
97
|
+
|
98
|
+
def safe_rescue
|
99
|
+
begin
|
100
|
+
yield
|
101
|
+
rescue => e
|
102
|
+
warn e
|
103
|
+
logger.error("FileStoreError (#{e}): #{e.message}") if logger
|
104
|
+
false
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
|
3
|
+
require 'active_support/cache/mongo_cache_store/backend/base'
|
4
|
+
|
5
|
+
module ActiveSupport
|
6
|
+
module Cache
|
7
|
+
class MongoCacheStore
|
8
|
+
module Backend
|
9
|
+
# MongoCacheStoreBackend for capped collections
|
10
|
+
#
|
11
|
+
# == Capped Collections
|
12
|
+
#
|
13
|
+
module Capped
|
14
|
+
include Base
|
15
|
+
|
16
|
+
def clear(options = {})
|
17
|
+
col = get_collection(options)
|
18
|
+
ret = safe_rescue do
|
19
|
+
col.update({},{:expires_at => Time.new})
|
20
|
+
end
|
21
|
+
ret ? true : false
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def backend_name
|
27
|
+
"capped"
|
28
|
+
end
|
29
|
+
|
30
|
+
def get_collection(options)
|
31
|
+
@db[get_collection_name(options)]
|
32
|
+
end
|
33
|
+
|
34
|
+
def delete_entry(key,options)
|
35
|
+
col = get_collection(options)
|
36
|
+
safe_rescue do
|
37
|
+
col.update({:_id => key}, {:expires_at => Time.new})
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
|
3
|
+
require 'active_support/cache/mongo_cache_store/backend/base'
|
4
|
+
|
5
|
+
|
6
|
+
module ActiveSupport
|
7
|
+
module Cache
|
8
|
+
class MongoCacheStore
|
9
|
+
module Backend
|
10
|
+
# MongoCacheStoreBackend for TTL collections
|
11
|
+
#
|
12
|
+
# == Time To Live (TTL) collections
|
13
|
+
#
|
14
|
+
module MultiTTL
|
15
|
+
include Base
|
16
|
+
|
17
|
+
alias :get_collection_prefix :get_collection_name
|
18
|
+
|
19
|
+
def clear(options = {})
|
20
|
+
@db.collection_names.each do |cname|
|
21
|
+
prefix = get_collection_prefix
|
22
|
+
if prefix.match(/^cache/) and cname.match(/^#{get_collection_prefix(options)}/)
|
23
|
+
safe_rescue do
|
24
|
+
@db[cname].drop
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
true
|
29
|
+
end
|
30
|
+
|
31
|
+
protected
|
32
|
+
|
33
|
+
def read_entry(key, options)
|
34
|
+
if options[:expires_in]
|
35
|
+
options[:collection] = get_collection(options)
|
36
|
+
else
|
37
|
+
options[:collection] = get_collection_from_index(key,options)
|
38
|
+
end
|
39
|
+
|
40
|
+
super(key, options)
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
def write_entry(key, entry, options)
|
45
|
+
options[:collection] = get_collection(options)
|
46
|
+
|
47
|
+
super(key, entry, options)
|
48
|
+
|
49
|
+
if (options[:use_index])
|
50
|
+
ki = get_key_index_collection(options)
|
51
|
+
indexed = safe_rescue do
|
52
|
+
ki.save({
|
53
|
+
:_id => key,
|
54
|
+
:collection => options[:collection].name,
|
55
|
+
:expires_at => entry.expires_at
|
56
|
+
})
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
def delete_entry(key, options)
|
63
|
+
options[:collection] = get_collection_from_index(key,options)
|
64
|
+
super(key, options)
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def backend_name
|
71
|
+
"multi_ttl"
|
72
|
+
end
|
73
|
+
|
74
|
+
def get_collection(options)
|
75
|
+
|
76
|
+
col = super
|
77
|
+
return col unless col.nil?
|
78
|
+
|
79
|
+
name_parts = [get_collection_prefix(options)]
|
80
|
+
expires_in = options[:expires_in]
|
81
|
+
name_parts.push expires_in.nil? ? 'forever' : expires_in.to_i
|
82
|
+
collection_name = name_parts.join('.')
|
83
|
+
|
84
|
+
collection = @collection_map[collection_name]
|
85
|
+
|
86
|
+
collection ||= create_collection(collection_name, expires_in)
|
87
|
+
return collection
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
def get_key_index_collection(options)
|
92
|
+
name_parts = [get_collection_prefix(options)]
|
93
|
+
name_parts.push options[:namespace] unless options[:namespace].nil?
|
94
|
+
name_parts.push 'key_index'
|
95
|
+
|
96
|
+
col = @db[name_parts.join('.')]
|
97
|
+
col.ensure_index('expires_at',{ expireAfterSeconds: 0})
|
98
|
+
return col
|
99
|
+
end
|
100
|
+
|
101
|
+
def get_collection_from_index(key,options)
|
102
|
+
if (options[:use_index])
|
103
|
+
ki = get_key_index_collection(options)
|
104
|
+
|
105
|
+
options[:collection] = safe_rescue do
|
106
|
+
response = ki.find_one({
|
107
|
+
:_id => key,
|
108
|
+
})
|
109
|
+
return nil if response.nil?
|
110
|
+
|
111
|
+
return @db[response['collection']]
|
112
|
+
end
|
113
|
+
end
|
114
|
+
nil
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
def create_collection(name, expires_in)
|
119
|
+
collection = @db[name]
|
120
|
+
collection.ensure_index('created_at',{ expireAfterSeconds: expires_in.to_i }) unless expires_in.nil?
|
121
|
+
return collection
|
122
|
+
end
|
123
|
+
|
124
|
+
def build_backend(options)
|
125
|
+
options.replace({
|
126
|
+
:use_index => true
|
127
|
+
}.merge(options))
|
128
|
+
|
129
|
+
@collection_map = {}
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
|
3
|
+
require 'active_support/cache/mongo_cache_store/backend/base'
|
4
|
+
|
5
|
+
|
6
|
+
module ActiveSupport
|
7
|
+
module Cache
|
8
|
+
class MongoCacheStore
|
9
|
+
module Backend
|
10
|
+
# MongoCacheStoreBackend for standard collections
|
11
|
+
#
|
12
|
+
# == Standard collections
|
13
|
+
#
|
14
|
+
module Standard
|
15
|
+
include Base
|
16
|
+
|
17
|
+
def clear(options = {})
|
18
|
+
col = get_collection(options)
|
19
|
+
ret = safe_rescue do
|
20
|
+
col.remove
|
21
|
+
end
|
22
|
+
ret ? true : false
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def backend_name
|
28
|
+
"standard"
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_collection(options)
|
32
|
+
@db[get_collection_name(options)]
|
33
|
+
end
|
34
|
+
|
35
|
+
def write_entry(key, entry, options)
|
36
|
+
ret = super
|
37
|
+
@write_cnt += 1
|
38
|
+
if options[:auto_flush] and @write_cnt > options[:auto_flush_threshold]
|
39
|
+
safe_rescue do
|
40
|
+
col = get_collection(options)
|
41
|
+
col.delete({
|
42
|
+
:expires_at => {
|
43
|
+
'$gt' => Time.now
|
44
|
+
}
|
45
|
+
})
|
46
|
+
end
|
47
|
+
@write_cnt = 0
|
48
|
+
end
|
49
|
+
return ret
|
50
|
+
end
|
51
|
+
|
52
|
+
def build_backend(options)
|
53
|
+
options.replace({
|
54
|
+
:auto_flush => true,
|
55
|
+
:auto_flush_threshold => 10_000,
|
56
|
+
:collection => nil
|
57
|
+
}.merge(options))
|
58
|
+
|
59
|
+
@write_cnt = 0
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
|
3
|
+
require 'active_support/cache/mongo_cache_store/backend/base'
|
4
|
+
|
5
|
+
module ActiveSupport
|
6
|
+
module Cache
|
7
|
+
class MongoCacheStore
|
8
|
+
module Backend
|
9
|
+
# MongoCacheStoreBackend for OneTTL collections
|
10
|
+
#
|
11
|
+
# == OneTTL collections
|
12
|
+
#
|
13
|
+
module TTL
|
14
|
+
include Base
|
15
|
+
|
16
|
+
def clear(options = {})
|
17
|
+
col = get_collection(options)
|
18
|
+
ret = safe_rescue do
|
19
|
+
col.remove
|
20
|
+
end
|
21
|
+
ret ? true : false
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
def write_entry(key,entry,options)
|
27
|
+
|
28
|
+
# Set all time based entries here.
|
29
|
+
# This ensures all fields are based on the same Time
|
30
|
+
now = Time.now
|
31
|
+
options[:xentry] = {
|
32
|
+
:created_at => now,
|
33
|
+
:expires_at => entry.expires_in.nil? ? Time.utc(9999) : now + entry.expires_in.to_i
|
34
|
+
}
|
35
|
+
|
36
|
+
super(key,entry,options)
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def backend_name
|
43
|
+
"ttl"
|
44
|
+
end
|
45
|
+
|
46
|
+
def get_collection(options)
|
47
|
+
return @collection if @collection.is_a? Mongo::Collection
|
48
|
+
collection = super
|
49
|
+
return collection unless collection.nil?
|
50
|
+
|
51
|
+
collection = @db[get_collection_name(options)]
|
52
|
+
collection.ensure_index('expires_at',{ expireAfterSeconds: 0 })
|
53
|
+
@collection = collection
|
54
|
+
end
|
55
|
+
|
56
|
+
def build_backend(options)
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'mongo_cache_store/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "mongo_cache_store"
|
8
|
+
gem.version = MongoCacheStore::VERSION
|
9
|
+
gem.authors = ["Kevin McGrath"]
|
10
|
+
gem.email = ["kmcgrath@baknet.com"]
|
11
|
+
gem.description = %q{A MongoDB ActiveSupport Cache}
|
12
|
+
gem.summary = %q{A MongoDB ActiveSupport Cache}
|
13
|
+
gem.homepage = "https://github.com/kmcgrath/mongo_cache_store"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_dependency 'mongo'
|
21
|
+
gem.add_dependency 'activesupport'
|
22
|
+
gem.add_development_dependency 'rspec'
|
23
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
3
|
+
|
4
|
+
|
5
|
+
class MongoCacheStoreTestSaveClass
|
6
|
+
|
7
|
+
attr_reader :set_me
|
8
|
+
def initialize(set_me)
|
9
|
+
@set_me = set_me
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
module ActiveSupport
|
16
|
+
module Cache
|
17
|
+
class MongoCacheStoreTest
|
18
|
+
shared_examples "a cache store" do
|
19
|
+
describe "Caching" do
|
20
|
+
|
21
|
+
it "can write values" do
|
22
|
+
@store.write('forever!', 'I live forever!')
|
23
|
+
@store.fetch('forever!').should == "I live forever!"
|
24
|
+
end
|
25
|
+
|
26
|
+
it "can expire" do
|
27
|
+
@store.fetch 'testkey', expires_in: 2.minutes do
|
28
|
+
"I will expire."
|
29
|
+
end
|
30
|
+
@store.exist?("testkey").should == true
|
31
|
+
|
32
|
+
sleep 60
|
33
|
+
|
34
|
+
response = @store.fetch 'testkey'
|
35
|
+
response.should == "I will expire."
|
36
|
+
|
37
|
+
sleep 120
|
38
|
+
|
39
|
+
response = @store.fetch 'testkey'
|
40
|
+
response.should == nil
|
41
|
+
end
|
42
|
+
|
43
|
+
it "can cache a hash" do
|
44
|
+
@store.write('hashme',
|
45
|
+
{"1"=>{:name=>"Uptime", :zenoss_instance=>"cust", :path=>"/zport/RenderServer/render?gopts=k4VJvRM2K1Dahl3RzJWcqoicTSIz0QJ1Ja10idqC412ptG5_lrmcroRy3BDIqioB561tB4UjtjSg1ib_YaGOXBWOEHBhPbEmA6ZXXjd8LiIOmItqTDZjuFkZvOV38_CF_YEi7t9xUtFKOIxibyVo67jzbyK6P-ORc7i07rK5GUYeA2CnJxm4UDCNKcKRpDCzmdzR-NwcOp0w55KH8YeU6NpgHyM_tpu8G42d7nBQebs1DKvn20sktezqy_LiIizKk3eMzDW-c8w1EZTESWa4rIihXfHrrv9PNxvCw41wU9f3dxQt2c3OFfhSx6riSUbeVsUNEo6s_35KdKJkbr6rEZaaxL2RGR47eLBzifIcZAg="}, "2"=>{:name=>"CPU Utilization", :zenoss_instance=>"cust", :path=>"/zport/RenderServer/render?gopts=WN-Oc9wB_CFbVKzzN75ij-qQcZ6lSOC3jj_ALTzkdMEZ_EzgcvfWOdU7vmlrm9MFY6zrOUeW2Z_uzyJkhRrl_81bVs96xoa7y2S29NVU_oSIIxQj1rUElV04u3wGRXnEbfyYPjLp0m6QEDOthqCs3SLcc7jKkCI15V0Js1b6FpFleWdmO9dCMLvKqdBiE-yhSjfwPS7Rh0wPLAoxXTyIkFFHHBXIu_g69_xnESZhYz0rAWn0W0Ag5N46LuNqwbzW1PwIfczY1Hqw2iKQAq1C4fMry3DNiQvnRmUZdF_w11quhIo-XccH1em3iopVdyy6ik83vMvJN3DfhLUbXcZjnA=="}, "3"=>{:name=>"Memory Utilization", :instance=>"cust", :path=>"/zport/RenderServer/render?gopts=_E6b8KAZjy9xzEYtckX_RDQOsGfIRZnG5WRu7ekbUIlsgWmNZNJ8AgEfr7AM0rV6lZMC4PzCPacRN1zeVQ-nHgTBBDbq4sIQuUMl3ETW9DRM2wgjrmnCRwcl7Dz_qGL6PWkhsJzhQ231SuNi7pM6mDw8dbE_pmZ3F6xKa7_8frJLWoMGLji8sIklFf45YckRkIohkMsffZKMsIspx8JEOvR9mP24vGs0Wv4gRXxqFoE1FlgchuKf2KDvWbwp92RrdOmhOAqMxHWmNA2faAC75072ao9bf6UnxZUu5WFUcNsvJJFcEl2U30TEX5vyjf_9C-1Rvnyf8Q6ajvRKA1VNIl6fpItv8dsKTNViX27RWsI="}, "4"=>{:name=>"Swap", :instance=>"cust", :path=>"/zport/RenderServer/render?gopts=nBpbINZ6hhzV8IUJq49zWOTsQ4hX5F5ZGPyoHZ6KWPUx_4Xk0_URWbZPQO4yicu14u81lDFqfRCoFTwofkA0NJIqoOQfAx2gWrwbE7RtulqYeMol47KWQpSITLSV4Mkh1Sbp9VZqpc4YVjiu1EvWSOIHCeUPXZPSBWFh0R2VoPB_VutBricdimwkvRsHbU9-BlKyJcT-_vEnTMi2MHlB3iKWY329OkN-J2chI3MtoLvSw9sN2LJbrO4ynFQPX4WFY9D18Zhv1bzOapn0XcCsxj88XHZ2eZeEZ0uv33_9EKSVxKtOOYo3Y08mjEKduOme"}, "5"=>{:name=>"Load Average", :instance=>"cust", :path=>"/zport/RenderServer/render?gopts=0y-dExjiyMFT0yWeOf-FR3hBIgIVZZortlKSFUETQYjwMuSDfhB-JhsEYI-feAC0Bmh5T8fZQRuYi5MkD3C2S6Qwd_kzvPrjwxCfxjTaQzidN-DxlWjqTkTfZ-AiPQ3F8JVXr-8kP8ZA10XDcV2VpIHJwZatzxDBVk7isEdKAXEJm-1X_TY7BCdU9uFlcBT1XGEYESM5anyR3282Z77yiYX6PsUcoPwFtLzg1l6Ql8cda8n4d2zLK4ZBX5G25ZQoC0MAmHZPyXRIsI-GGZjKcvXHjZ350tEpyvPiSX8a7PG-dRtWIGEEecM9czU3MF5Xzl-qOVfoRWc8pmlc-5E37Z5TcAoZpUHSlssJdjwoLfOlhFkLGyAVZpVXRCVRWdXNxilLS9N4GbuIEqhsEILyM0MzPvEHwPO3rcPSlzcS4s_SXnjp6XLg2bB6NZjAvbz5Vyb6OPg-8b9C-MmzcpWzVHzP5nYo9RVPR_lpFcmOBUSO_cJO22pIqqMyn1vXbtVz"}, "6"=>{:name=>"Paging", :instance=>"cust", :path=>"/zport/RenderServer/render?gopts=N7sjMwpMD3JE0ZNya07IgWj6t7yn7uX4GJ1XHlmyhRcqcea0HpfXTen6ptASVAZIvWT7fW4kufkvtNBsxKfDMX9JMzqoR-kpfWqoRRfI3eGmqYecg6gcQgXSEnu5Mzf4r-lbo1_Cqp8dYuSmC37uWrKi0gTWVr54ZmQ6w4dSymQ4xSkCH2-MO1X8wxZdtm804NJy6yQ4l7JZYBkWD9rABoPhVfZq4mZpHmZN27UlbK14Z9R3EEF6MG0jKCbJ7gJVEjpOoX_y5ka4S3aRULFSByrZIA290_ZMO21DAnsJbJyMXAEP9r8UanaotdVrPLSOXJkYD4WvdcdDeZ3wMtvdL42oHdhwLDyG9J8i9GkcvwsU04MlTTl270LkX31HJjxjB8IChpDke-9LEmxG_1icmGcAcWr26gTk9HOULwTyso4sX_ZqqCnrrnRRQLCpCZtlqxIB9iAMUtSdkEUdZlsAuw=="}},
|
46
|
+
expires_in: 1.hour
|
47
|
+
)
|
48
|
+
|
49
|
+
hash = @store.read('hashme')
|
50
|
+
hash["1"].should be_a_kind_of(Hash)
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
it "can cache class instances" do
|
55
|
+
@store.fetch 'my_class', expires_in: 30.seconds do
|
56
|
+
MongoCacheStoreTestSaveClass.new('what did i say?')
|
57
|
+
end
|
58
|
+
|
59
|
+
my_class = @store.fetch 'my_class'
|
60
|
+
my_class.set_me.should == 'what did i say?'
|
61
|
+
end
|
62
|
+
|
63
|
+
it "can use a hash as a key" do
|
64
|
+
hash_key = {
|
65
|
+
:class_name => 'my_class',
|
66
|
+
:option2 => 2
|
67
|
+
}
|
68
|
+
|
69
|
+
miss_key = {
|
70
|
+
:class_name => 'my_class',
|
71
|
+
:option2 => 1
|
72
|
+
}
|
73
|
+
|
74
|
+
hit_key = {
|
75
|
+
:class_name => 'my_class',
|
76
|
+
:option2 => 2
|
77
|
+
}
|
78
|
+
|
79
|
+
|
80
|
+
@store.fetch(hash_key, expires_in: 30.seconds) do
|
81
|
+
MongoCacheStoreTestSaveClass.new('what did i say?')
|
82
|
+
end
|
83
|
+
|
84
|
+
my_class = @store.fetch(hash_key)
|
85
|
+
my_class.set_me.should == 'what did i say?'
|
86
|
+
|
87
|
+
my_class = @store.fetch(hit_key)
|
88
|
+
my_class.set_me.should == 'what did i say?'
|
89
|
+
|
90
|
+
my_class = @store.fetch(miss_key)
|
91
|
+
my_class.should == nil
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
after(:all) do
|
96
|
+
@store.clear
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
describe MongoCacheStore do
|
103
|
+
describe "initializing" do
|
104
|
+
it "can take a Mongo::DB object" do
|
105
|
+
db = Mongo::DB.new('mongo_cache_store_test', Mongo::Connection.new)
|
106
|
+
store = ActiveSupport::Cache::MongoCacheStore.new(:TTL, :db => db)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
describe "TTL Caching" do
|
111
|
+
it_behaves_like "a cache store"
|
112
|
+
before(:all) do
|
113
|
+
db = Mongo::DB.new('mongo_cache_store_test', Mongo::Connection.new)
|
114
|
+
@store = ActiveSupport::Cache::MongoCacheStore.new(:TTL, :db => db)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe "TTL Caching On Fail Serialization" do
|
119
|
+
it_behaves_like "a cache store"
|
120
|
+
before(:all) do
|
121
|
+
db = Mongo::DB.new('mongo_cache_store_test', Mongo::Connection.new)
|
122
|
+
@store = ActiveSupport::Cache::MongoCacheStore.new(:TTL, :db => db, :serialize => :on_fail)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
describe "MultiTTL Caching" do
|
127
|
+
it_behaves_like "a cache store"
|
128
|
+
before(:all) do
|
129
|
+
db = Mongo::DB.new('mongo_cache_store_test', Mongo::Connection.new)
|
130
|
+
@store = ActiveSupport::Cache::MongoCacheStore.new(:MultiTTL, :db => db)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
describe "MultiTTL Caching On Fail Serialization" do
|
135
|
+
it_behaves_like "a cache store"
|
136
|
+
before(:all) do
|
137
|
+
db = Mongo::DB.new('mongo_cache_store_test', Mongo::Connection.new)
|
138
|
+
@store = ActiveSupport::Cache::MongoCacheStore.new(:MultiTTL, :db => db, :serialize => :on_fail)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
describe "Standard Caching" do
|
143
|
+
it_behaves_like "a cache store"
|
144
|
+
before(:all) do
|
145
|
+
db = Mongo::DB.new('mongo_cache_store_test', Mongo::Connection.new)
|
146
|
+
@store = ActiveSupport::Cache::MongoCacheStore.new(:Standard, :db => db)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
describe "Standard Caching On Fail Serialization" do
|
151
|
+
it_behaves_like "a cache store"
|
152
|
+
before(:all) do
|
153
|
+
db = Mongo::DB.new('mongo_cache_store_test', Mongo::Connection.new)
|
154
|
+
@store = ActiveSupport::Cache::MongoCacheStore.new(:Standard, :db => db, :serialize => :on_fail)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mongo_cache_store
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Kevin McGrath
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-01-13 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: mongo
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: activesupport
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
description: A MongoDB ActiveSupport Cache
|
63
|
+
email:
|
64
|
+
- kmcgrath@baknet.com
|
65
|
+
executables: []
|
66
|
+
extensions: []
|
67
|
+
extra_rdoc_files: []
|
68
|
+
files:
|
69
|
+
- .gitignore
|
70
|
+
- Gemfile
|
71
|
+
- LICENSE.txt
|
72
|
+
- README.md
|
73
|
+
- Rakefile
|
74
|
+
- lib/active_support/cache/mongo_cache_store.rb
|
75
|
+
- lib/active_support/cache/mongo_cache_store/backend/base.rb
|
76
|
+
- lib/active_support/cache/mongo_cache_store/backend/capped.rb
|
77
|
+
- lib/active_support/cache/mongo_cache_store/backend/multi_ttl.rb
|
78
|
+
- lib/active_support/cache/mongo_cache_store/backend/standard.rb
|
79
|
+
- lib/active_support/cache/mongo_cache_store/backend/ttl.rb
|
80
|
+
- lib/mongo_cache_store/version.rb
|
81
|
+
- mongo_cache_store.gemspec
|
82
|
+
- spec/active_support/cache/mongo_cache_store_spec.rb
|
83
|
+
- spec/spec_helper.rb
|
84
|
+
homepage: https://github.com/kmcgrath/mongo_cache_store
|
85
|
+
licenses: []
|
86
|
+
post_install_message:
|
87
|
+
rdoc_options: []
|
88
|
+
require_paths:
|
89
|
+
- lib
|
90
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
91
|
+
none: false
|
92
|
+
requirements:
|
93
|
+
- - ! '>='
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
requirements: []
|
103
|
+
rubyforge_project:
|
104
|
+
rubygems_version: 1.8.24
|
105
|
+
signing_key:
|
106
|
+
specification_version: 3
|
107
|
+
summary: A MongoDB ActiveSupport Cache
|
108
|
+
test_files:
|
109
|
+
- spec/active_support/cache/mongo_cache_store_spec.rb
|
110
|
+
- spec/spec_helper.rb
|