lion-attr 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/Rakefile +32 -0
- data/lib/lion_attr.rb +262 -0
- data/lib/lion_attr/bson.rb +11 -0
- data/lib/lion_attr/config.rb +38 -0
- data/lib/lion_attr/internal_redis.rb +46 -0
- data/lib/lion_attr/redis_pool.rb +25 -0
- data/lib/lion_attr/version.rb +3 -0
- data/lib/tasks/lion_attr_tasks.rake +4 -0
- data/spec/config_spec.rb +17 -0
- data/spec/lion_attr_spec.rb +290 -0
- data/spec/lion_attr_with_custom_configuration_spec.rb +26 -0
- data/spec/mongoid.yml +6 -0
- data/spec/spec_helper.rb +27 -0
- metadata +124 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 34d61bee3070595310ca92f1f1ef9256aaebe603
|
4
|
+
data.tar.gz: ac04beac1d6b6bcacfabfe81583e5259f05fea54
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 31280e75824539ab18df77a855c69b20d4f54ec23c945beca643f1e264bb444b5e8b450d8c47852f94fb46db50b145829599fe24c98e30222e140b66ac962f6f
|
7
|
+
data.tar.gz: 40b783f03ea499fef042d7596ac9d9f1dad11fae3bc997907dd5ad83df47229b3317207d59d47f2f97883a155dea584d7c44a90a2d7092e4633a11ba4cb2b304
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2014 Victor Tran
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'LionAttr'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.rdoc')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
Bundler::GemHelper.install_tasks
|
21
|
+
|
22
|
+
require 'rake/testtask'
|
23
|
+
|
24
|
+
Rake::TestTask.new(:test) do |t|
|
25
|
+
t.libs << 'lib'
|
26
|
+
t.libs << 'test'
|
27
|
+
t.pattern = 'test/**/*_test.rb'
|
28
|
+
t.verbose = false
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
task default: :test
|
data/lib/lion_attr.rb
ADDED
@@ -0,0 +1,262 @@
|
|
1
|
+
require_relative 'lion_attr/internal_redis'
|
2
|
+
require_relative 'lion_attr/config'
|
3
|
+
require_relative 'lion_attr/bson'
|
4
|
+
|
5
|
+
# LionAttr, a gem to store Mongoid object in Redis for fast access. It also helps
|
6
|
+
# you to store object attributes to real time update via Redis increamental
|
7
|
+
# operations.
|
8
|
+
#
|
9
|
+
# In many web applications, updating objects to database (mongodb) too often
|
10
|
+
# would hurt overal application performance because databases usually need to
|
11
|
+
# do some extra tasks such as indexing, updating its cache ...
|
12
|
+
#
|
13
|
+
# One of the most popular example for this problem is tracking pageview of a web
|
14
|
+
# page. Imagine if you have a blog and you want to know how many time people visit
|
15
|
+
# a specific article, what would you do? An obvious way to do so is whenever a
|
16
|
+
# person make a request, you increase the record holding pageview counter by using
|
17
|
+
# `mongoid#inc` method. Doing this is not a good idea because it increases database
|
18
|
+
# calls making your application slower.
|
19
|
+
#
|
20
|
+
# A better solution for this problem is to keep the pageview counter in memory
|
21
|
+
# (which is fast) key-value storage such as Redis. Instead of increasing the
|
22
|
+
# counter by the database, we increase the counter inside Redis and save back to
|
23
|
+
# the database later (like after 10 mins, 30 mins or even a day).
|
24
|
+
#
|
25
|
+
# LionAttr provides APIs for you to do so with ease.
|
26
|
+
#
|
27
|
+
# That counter is usually an attribute of a Model object, the difference is that
|
28
|
+
# attribute will get the value from Redis instead of the database. We call it live
|
29
|
+
# attribute because it tends update its value very frequently.
|
30
|
+
#
|
31
|
+
# @example
|
32
|
+
#
|
33
|
+
# class Article
|
34
|
+
# include Mongoid::Document
|
35
|
+
# include LionAttr
|
36
|
+
# field :url, type: String
|
37
|
+
# field :view, type: Integer
|
38
|
+
#
|
39
|
+
# # field :view will be stored in Redis and saved back to Mongodb later
|
40
|
+
# live :view
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# @todo: Cache object
|
44
|
+
# @todo: Custom key
|
45
|
+
# @todo: Inc
|
46
|
+
# @todo: Callbacks
|
47
|
+
# @todo: Configure
|
48
|
+
#
|
49
|
+
# @since 0.1.0
|
50
|
+
#
|
51
|
+
# @see http://tranvictor.github.io/lion_attr
|
52
|
+
|
53
|
+
module LionAttr
|
54
|
+
|
55
|
+
# Including LionAttr will set an after save callback to update the object cache
|
56
|
+
# in Redis.
|
57
|
+
#
|
58
|
+
# @param base Mongoid::Document
|
59
|
+
def self.included(base)
|
60
|
+
base.extend(ClassMethods)
|
61
|
+
base.set_callback(:save, :after, :update_to_redis, prepend: true)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Key to store in Redis, it is combined by object identity and field name.
|
65
|
+
#
|
66
|
+
# @param [String, Symbol] field
|
67
|
+
# @return [String]
|
68
|
+
# @example
|
69
|
+
# article.key(:view)
|
70
|
+
# #=> 54d5f10d5675730bd1050000_view
|
71
|
+
#
|
72
|
+
# article.url = "http://clicklion.com/article/1"
|
73
|
+
# article.class.live_key = :url
|
74
|
+
# article.key(:view)
|
75
|
+
# #=> http://clicklion.com/article/1_view
|
76
|
+
#
|
77
|
+
# @see ClassMethods#live_key Object Identity definition
|
78
|
+
def key(field)
|
79
|
+
self.class._key(self.send(self.class.live_key), field)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Call this method to manually create a cache of the object in Redis.
|
83
|
+
# It will store the object as a json string in a Redis Hash with key is the
|
84
|
+
# object's id. Object from different classes will be stored in different hashes
|
85
|
+
# distinguished by class full name.
|
86
|
+
#
|
87
|
+
# @see InternalRedis
|
88
|
+
def update_to_redis
|
89
|
+
InternalRedis.new(self.class.name).set(id, as_document.to_json)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Call this method will clear all of Redis keys related to the object.
|
93
|
+
def clean_cache_after_destroy
|
94
|
+
@live_keys ||= self.class.live_fields.map { |f| key(f) }
|
95
|
+
@internal_redis ||= InternalRedis.new(self.class.name)
|
96
|
+
@internal_redis.del @live_keys
|
97
|
+
@internal_redis.del id
|
98
|
+
end
|
99
|
+
|
100
|
+
module ClassMethods
|
101
|
+
|
102
|
+
# Fetch the object specified with an id from Redis. It will not touch the
|
103
|
+
# database (Mongdb). If that object is not available on Redis or invalid (due to
|
104
|
+
# model changes, it will make a query to the database (Mongodb).
|
105
|
+
#
|
106
|
+
# @param [String] id
|
107
|
+
# @return [Mongoid::Document]
|
108
|
+
def fetch(id)
|
109
|
+
@internal_redis ||= InternalRedis.new(name)
|
110
|
+
string_object = @internal_redis.get(id)
|
111
|
+
if string_object.nil?
|
112
|
+
object = _fetch_from_db(id)
|
113
|
+
else
|
114
|
+
object = new(JSON.load(string_object))
|
115
|
+
end
|
116
|
+
rescue Mongoid::Errors::UnknownAttribute
|
117
|
+
object = _fetch_from_db(id)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Query the object by id, and create a cache version in Redis.
|
121
|
+
#
|
122
|
+
# @param [String] id
|
123
|
+
# @return [Mongoid::Document]
|
124
|
+
def _fetch_from_db(id)
|
125
|
+
object = find(id)
|
126
|
+
@internal_redis.set(id, object.as_document.to_json)
|
127
|
+
object
|
128
|
+
end
|
129
|
+
|
130
|
+
# Get all live fields of the class
|
131
|
+
#
|
132
|
+
# @example
|
133
|
+
# article.class.live_fields
|
134
|
+
# #=> [:view]
|
135
|
+
def live_fields
|
136
|
+
@live_fields
|
137
|
+
end
|
138
|
+
|
139
|
+
def live(*fields)
|
140
|
+
fields.each do |field|
|
141
|
+
generate_fetch_cache_method(field)
|
142
|
+
# generate_set_cache_method(field)
|
143
|
+
end
|
144
|
+
generate_update_db_method(fields)
|
145
|
+
generate_incr_method
|
146
|
+
(@live_fields ||= []).push(*fields)
|
147
|
+
set_callback(:destroy, :after, :clean_cache_after_destroy)
|
148
|
+
end
|
149
|
+
|
150
|
+
# Specify the field which is used to get storage key for the object.
|
151
|
+
#
|
152
|
+
# @param [String, Symbole] field
|
153
|
+
# @example
|
154
|
+
# article.class.live_key = :url
|
155
|
+
# # Whenever LionAttr need to use object key, it gets from :url
|
156
|
+
def live_key=(field)
|
157
|
+
@key = field
|
158
|
+
end
|
159
|
+
|
160
|
+
# Get object key to interact with Redis. If you don't specify the live_key
|
161
|
+
# :id will be used by default.
|
162
|
+
def live_key
|
163
|
+
@key || :id
|
164
|
+
end
|
165
|
+
|
166
|
+
def generate_update_db_method(_fields)
|
167
|
+
define_method('update_db') do
|
168
|
+
@live_keys ||= self.class.live_fields.map { |f| key(f) }
|
169
|
+
@internal_redis ||= InternalRedis.new(self.class.name)
|
170
|
+
redis_values = @internal_redis.mget(@live_keys)
|
171
|
+
self.class.live_fields.each_with_index do |f, i|
|
172
|
+
if read_attribute(f) != redis_values[i]
|
173
|
+
write_attribute(f, redis_values[i])
|
174
|
+
end
|
175
|
+
end
|
176
|
+
# TODO: This could be improved by using batch save instead of saving
|
177
|
+
# individual document
|
178
|
+
save
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def _key(id, field)
|
183
|
+
"#{id}_#{field}"
|
184
|
+
end
|
185
|
+
|
186
|
+
# Increase live attributes value in Redis. If the live attributes are not
|
187
|
+
# Integer nor Float, a String message will be returned. Otherwise, increased
|
188
|
+
# value will be returned.
|
189
|
+
#
|
190
|
+
# @param [String] id
|
191
|
+
# @param [String, Symbol] field
|
192
|
+
# @param [Integer] increment
|
193
|
+
# @param [InternalRedis] internal_redis
|
194
|
+
#
|
195
|
+
# @return [Integer, Float, String]
|
196
|
+
def incr(id, field, increment = 1, internal_redis = nil)
|
197
|
+
unless self.live_fields.include?(field)
|
198
|
+
fail "#{field} is not a live attributes"
|
199
|
+
end
|
200
|
+
internal_redis ||= InternalRedis.new(name)
|
201
|
+
_incr(_key(id, field), fields[field.to_s].type,
|
202
|
+
@internal_redis, increment) do
|
203
|
+
find(id).read_attribute(field)
|
204
|
+
end
|
205
|
+
rescue => e
|
206
|
+
e.message
|
207
|
+
end
|
208
|
+
|
209
|
+
def _incr(key, type, internal_redis = nil, increment = 1, &block)
|
210
|
+
internal_redis ||= InternalRedis.new(name)
|
211
|
+
if block && !internal_redis.exists(key)
|
212
|
+
internal_redis.setnx key, block.call
|
213
|
+
end
|
214
|
+
if type == Integer
|
215
|
+
internal_redis.incrby(key, increment)
|
216
|
+
elsif type == Float
|
217
|
+
internal_redis.incrbyfloat(key, increment)
|
218
|
+
else
|
219
|
+
'ERR hash value is not a number'
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def generate_incr_method
|
224
|
+
define_method('incr') do |field, increment = 1|
|
225
|
+
begin
|
226
|
+
unless self.class.live_fields.include?(field)
|
227
|
+
fail "#{field} is not a live attributes"
|
228
|
+
end
|
229
|
+
@internal_redis ||= InternalRedis.new(self.class.name)
|
230
|
+
self.class._incr(key(field),
|
231
|
+
fields[field.to_s].type,
|
232
|
+
@internal_redis,
|
233
|
+
increment) { read_attribute(field) }
|
234
|
+
rescue => e
|
235
|
+
e.message
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def generate_fetch_cache_method(name)
|
241
|
+
re_define_method("#{name}") do
|
242
|
+
@internal_redis ||= InternalRedis.new(self.class.name)
|
243
|
+
raw = @internal_redis.get(key(name))
|
244
|
+
field = fields[name.to_s]
|
245
|
+
if raw.nil?
|
246
|
+
raw = read_attribute(name)
|
247
|
+
if lazy_settable?(field, raw)
|
248
|
+
value = write_attribute(name, field.eval_default(self))
|
249
|
+
else
|
250
|
+
value = field.demongoize(raw)
|
251
|
+
attribute_will_change!(name) if value.resizable?
|
252
|
+
end
|
253
|
+
|
254
|
+
@internal_redis.set(key(name), raw)
|
255
|
+
else
|
256
|
+
value = field.demongoize(raw)
|
257
|
+
end
|
258
|
+
value
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module LionAttr
|
4
|
+
|
5
|
+
# This module defines all the configuration options for LionAttr
|
6
|
+
module Config
|
7
|
+
def redis_config=(options)
|
8
|
+
@redis_config = options
|
9
|
+
RedisPool.instance.update_pool
|
10
|
+
end
|
11
|
+
|
12
|
+
def redis_config
|
13
|
+
@redis_config ||= {}
|
14
|
+
end
|
15
|
+
|
16
|
+
extend self
|
17
|
+
end
|
18
|
+
|
19
|
+
module ModuleInterface
|
20
|
+
# Sets the LionAttr configuration options. Best used by passing a block.
|
21
|
+
# You should configure before actually using LionAttr.
|
22
|
+
#
|
23
|
+
# @example Set up configuration options and tell LionAttr to store everything in
|
24
|
+
# redis datababse 13
|
25
|
+
#
|
26
|
+
# LionAttr.configure do |config|
|
27
|
+
# config.redis_config = { db: 13 }
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# @return [ Config ] The configuration object.
|
31
|
+
#
|
32
|
+
# @since 0.1.0
|
33
|
+
def configure
|
34
|
+
block_given? ? yield(::LionAttr::Config) : ::LionAttr::Config
|
35
|
+
end
|
36
|
+
end
|
37
|
+
extend ModuleInterface
|
38
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require_relative 'redis_pool'
|
2
|
+
|
3
|
+
module LionAttr
|
4
|
+
class InternalRedis
|
5
|
+
def initialize(hash_name)
|
6
|
+
@hash_name = hash_name
|
7
|
+
end
|
8
|
+
|
9
|
+
def get(key)
|
10
|
+
redis.with { |c| c.hget @hash_name, key }
|
11
|
+
end
|
12
|
+
|
13
|
+
def set(key, value)
|
14
|
+
redis.with { |c| c.hset @hash_name, key, value }
|
15
|
+
end
|
16
|
+
|
17
|
+
def exists(key)
|
18
|
+
redis.with { |c| c.hexists @hash_name, key }
|
19
|
+
end
|
20
|
+
|
21
|
+
def del(key)
|
22
|
+
redis.with { |c| c.hdel @hash_name, key }
|
23
|
+
end
|
24
|
+
|
25
|
+
def setnx(key, value)
|
26
|
+
redis.with { |c| c.hsetnx @hash_name, key, value }
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
def incrby(key, increment)
|
31
|
+
redis.with { |c| c.hincrby @hash_name, key, increment }
|
32
|
+
end
|
33
|
+
|
34
|
+
def incrbyfloat(key, increment)
|
35
|
+
redis.with { |c| c.hincrbyfloat @hash_name, key, increment }
|
36
|
+
end
|
37
|
+
|
38
|
+
def mget(keys)
|
39
|
+
redis.with { |c| c.hmget @hash_name, keys }
|
40
|
+
end
|
41
|
+
|
42
|
+
def redis
|
43
|
+
RedisPool.instance
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'redis'
|
3
|
+
require 'connection_pool'
|
4
|
+
|
5
|
+
module LionAttr
|
6
|
+
class RedisPool
|
7
|
+
include Singleton
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
update_pool
|
11
|
+
end
|
12
|
+
|
13
|
+
def update_pool
|
14
|
+
@pool = ConnectionPool.new(size: 100, timeout: 2) do
|
15
|
+
puts ::LionAttr.configure.redis_config
|
16
|
+
Redis.new ::LionAttr.configure.redis_config
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def with(&block)
|
21
|
+
@pool.with(&block)
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
data/spec/config_spec.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe LionAttr do
|
4
|
+
it 'can be configured via #configuree by passing a block' do
|
5
|
+
LionAttr.configure do |config|
|
6
|
+
config.redis_config = { :a => 1 }
|
7
|
+
end
|
8
|
+
|
9
|
+
expect(LionAttr::Config.redis_config[:a]).to eq 1
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "#configure" do
|
13
|
+
it 'returns config object' do
|
14
|
+
expect(LionAttr.configure).to be LionAttr::Config
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,290 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class TestRedis
|
4
|
+
include Mongoid::Document
|
5
|
+
field :foo, type: Integer
|
6
|
+
field :bar, type: Integer
|
7
|
+
field :mew, type: Float
|
8
|
+
field :baz, type: Float
|
9
|
+
field :qux, type: String
|
10
|
+
include LionAttr
|
11
|
+
live :foo, :bar, :mew, :qux
|
12
|
+
end
|
13
|
+
|
14
|
+
describe LionAttr do
|
15
|
+
before(:each) do
|
16
|
+
@test = TestRedis.new
|
17
|
+
@redis = LionAttr::InternalRedis.new(@test.class.name)
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "class#incr" do
|
21
|
+
before(:each) do
|
22
|
+
@test.save
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'defines an incr method on the class' do
|
26
|
+
expect(@test.class).to respond_to(:incr)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should change attribute value in redis' do
|
30
|
+
@test.foo = 0
|
31
|
+
@redis.set(@test.key(:foo), 0)
|
32
|
+
expect(@redis.get(@test.key(:foo))).to eq(@test.foo.to_s)
|
33
|
+
TestRedis.incr(@test.id, :foo, 10)
|
34
|
+
expect(@redis.get(@test.key(:foo))).not_to eq((@test.foo + 10).to_s)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should work fine if the attribute is not initialized to redis yet' do
|
38
|
+
@test.foo = 50
|
39
|
+
@test.save
|
40
|
+
TestRedis.incr @test.id, :foo, 1
|
41
|
+
expect(@test.foo).to eq 51
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'should not be affected by fetch' do
|
45
|
+
@test.foo = 0
|
46
|
+
@test.save
|
47
|
+
@test.incr :foo, 10
|
48
|
+
@new_test = TestRedis.fetch(@test.id)
|
49
|
+
TestRedis.incr @test.id, :foo, 10
|
50
|
+
expect(@new_test.foo).to eq 20
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'should return attribute value in redis' do
|
54
|
+
expect(TestRedis.incr(@test.id, :foo,
|
55
|
+
10).to_s).to eq(@redis.get(@test.key(:foo)))
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'should rescue error by return error message' do
|
59
|
+
#increase integer field by float increment
|
60
|
+
err_msg = "ERR value is not an integer or out of range"
|
61
|
+
expect(TestRedis.incr(@test.id, :foo, 10.0)).to eq(err_msg)
|
62
|
+
|
63
|
+
#increase float field by string
|
64
|
+
err_msg = 'ERR value is not a valid float'
|
65
|
+
expect(TestRedis.incr(@test.id, :mew, 'a')).to eq(err_msg)
|
66
|
+
|
67
|
+
#increase string field
|
68
|
+
err_msg = 'ERR hash value is not a number'
|
69
|
+
expect(TestRedis.incr(@test.id, :qux, 10)).to eq(err_msg)
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'should set default value for increment is 1' do
|
73
|
+
@redis.set(@test.key(:foo), 0)
|
74
|
+
TestRedis.incr @test.id, :foo
|
75
|
+
expect(@redis.get(@test.key(:foo))).to eq(1.to_s)
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'should raise exception when incr not live attribute' do
|
79
|
+
err_msg = 'baz is not a live attributes'
|
80
|
+
expect(@test.incr(:baz)).to eq(err_msg)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe 'more methods' do
|
85
|
+
it 'should generate method to fetch attributes value from cache' do
|
86
|
+
expect(@test).to respond_to(:foo)
|
87
|
+
expect(@test).to respond_to(:foo=)
|
88
|
+
expect(@test).to respond_to(:bar)
|
89
|
+
expect(@test).to respond_to(:bar=)
|
90
|
+
expect(@test).to respond_to(:update_db)
|
91
|
+
expect(@test).to respond_to(:incr)
|
92
|
+
expect(@test.class).to respond_to(:fetch)
|
93
|
+
expect(@test.class).to respond_to(:live_fields)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe 'callback' do
|
98
|
+
it 'should set callback function' do
|
99
|
+
expect(@test).to receive(:clean_cache_after_destroy)
|
100
|
+
@test.destroy
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'should remove live attribute in redis if object was destroy' do
|
104
|
+
@redis.set(@test.key(:foo), 10)
|
105
|
+
@redis.set(@test.key(:bar), 10)
|
106
|
+
@test.destroy
|
107
|
+
expect(@redis.get(@test.key(:foo))).to eq(nil)
|
108
|
+
expect(@redis.get(@test.key(:bar))).to eq(nil)
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'should remove object in redis if object was destroyed' do
|
112
|
+
id = @test.id
|
113
|
+
@test.save
|
114
|
+
@test.class.fetch(id)
|
115
|
+
expect(@redis.get(id)).to eq(@test.as_document.to_json)
|
116
|
+
@test.destroy
|
117
|
+
expect(@redis.get(id)).to eq(nil)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe '#fetch' do
|
122
|
+
it 'should return object if it exist in redis' do
|
123
|
+
@redis.set(@test.id, @test.as_document.to_json)
|
124
|
+
expect(@test.class.fetch(@test.id)).to eq(@test)
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'should set object in to redis in case not exist in redis yet' do
|
128
|
+
id = @test.id
|
129
|
+
expect(@redis.get(id)).to eq(nil)
|
130
|
+
@test.save
|
131
|
+
@test.class.fetch(id)
|
132
|
+
expect(@redis.get(id)).to eq(@test.as_document.to_json)
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'should return updated object if original was updated' do
|
136
|
+
@test.save
|
137
|
+
@old_test = TestRedis.fetch(@test.id)
|
138
|
+
@test.update(:baz => 100)
|
139
|
+
@updated_test = TestRedis.fetch(@test.id)
|
140
|
+
expect(@updated_test.baz).to eq 100
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'should be ok if the model is changed while cached object
|
144
|
+
is still available, it will refetch from db if the cached version is invalid' do
|
145
|
+
@test.save
|
146
|
+
document = @test.as_document
|
147
|
+
document['will_be_removed'] = 10
|
148
|
+
json_data = document.to_json
|
149
|
+
@redis.set(@test.id, json_data)
|
150
|
+
expect { @new_test = TestRedis.fetch(@test.id) }.not_to raise_error
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
describe '#key' do
|
155
|
+
it 'should contain object id and field name' do
|
156
|
+
expect(@test.key(:foo)).to eq("#{@test.id}_foo")
|
157
|
+
puts @test.key(:foo)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
describe '#update_db' do
|
162
|
+
it 'should update mongodb record if attribute value
|
163
|
+
is difference from redis' do
|
164
|
+
@test.incr(:foo, 10)
|
165
|
+
expect(@redis.get(@test.key(:foo))).not_to eq(@test.read_attribute(:foo).to_s)
|
166
|
+
@test.update_db
|
167
|
+
expect(@redis.get(@test.key(:foo))).to eq(@test.read_attribute(:foo).to_s)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
describe 'live attributes' do
|
172
|
+
it 'can be called multiple times' do
|
173
|
+
class TestMultipleLive
|
174
|
+
include Mongoid::Document
|
175
|
+
include LionAttr
|
176
|
+
field :foo, type: Integer
|
177
|
+
live :foo
|
178
|
+
field :bar, type: Integer
|
179
|
+
live :bar
|
180
|
+
include LionAttr
|
181
|
+
end
|
182
|
+
expect(TestMultipleLive.live_fields).to include(:foo, :bar)
|
183
|
+
@test = TestMultipleLive.new
|
184
|
+
expect(@test).to receive(:update_to_redis).exactly(1).times
|
185
|
+
@test.save
|
186
|
+
end
|
187
|
+
|
188
|
+
context 'custom key' do
|
189
|
+
it 'has default key of :id but can be assigned to other field' do
|
190
|
+
class TestCustomKey
|
191
|
+
include Mongoid::Document
|
192
|
+
include LionAttr
|
193
|
+
field :foo, type: Integer
|
194
|
+
live :bar
|
195
|
+
end
|
196
|
+
expect(TestCustomKey.live_key).to eq :id
|
197
|
+
TestCustomKey.class_eval do
|
198
|
+
self.live_key = :bar
|
199
|
+
end
|
200
|
+
expect(TestCustomKey.live_key).to eq :bar
|
201
|
+
end
|
202
|
+
|
203
|
+
it 'should change the key to save to redis' do
|
204
|
+
class TestCustomKey1
|
205
|
+
include Mongoid::Document
|
206
|
+
include LionAttr
|
207
|
+
field :foo, type: Integer
|
208
|
+
field :bar
|
209
|
+
live :bar
|
210
|
+
self.live_key = :foo
|
211
|
+
end
|
212
|
+
@instance = TestCustomKey1.new :foo => 100
|
213
|
+
expect(@instance.key(:bar)).to eq "100_bar"
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
describe '#incr' do
|
218
|
+
it 'should change attribute value in redis' do
|
219
|
+
@test.foo = 0
|
220
|
+
@redis.set(@test.key(:foo), 0)
|
221
|
+
expect(@redis.get(@test.key(:foo))).to eq(@test.foo.to_s)
|
222
|
+
@test.incr(:foo, 10)
|
223
|
+
expect(@redis.get(@test.key(:foo))).not_to eq((@test.foo + 10).to_s)
|
224
|
+
end
|
225
|
+
|
226
|
+
it 'should work fine if the attribute is not initialized to redis yet' do
|
227
|
+
@test.foo = 50
|
228
|
+
@test.save
|
229
|
+
@test.incr :foo, 1
|
230
|
+
expect(@test.foo).to eq 51
|
231
|
+
end
|
232
|
+
|
233
|
+
it 'should not be affected by fetch' do
|
234
|
+
@test.foo = 0
|
235
|
+
@test.save
|
236
|
+
@test.incr :foo, 10
|
237
|
+
@new_test = TestRedis.fetch(@test.id)
|
238
|
+
@new_test.incr :foo, 10
|
239
|
+
expect(@new_test.foo).to eq 20
|
240
|
+
end
|
241
|
+
|
242
|
+
it 'should return attribute value in redis' do
|
243
|
+
expect(@test.incr(:foo, 10).to_s).to eq(@redis.get(@test.key(:foo)))
|
244
|
+
end
|
245
|
+
|
246
|
+
it 'should rescue error by return error message' do
|
247
|
+
#increase integer field by float increment
|
248
|
+
err_msg = "ERR value is not an integer or out of range"
|
249
|
+
expect(@test.incr(:foo, 10.0)).to eq(err_msg)
|
250
|
+
|
251
|
+
#increase float field by string
|
252
|
+
err_msg = 'ERR value is not a valid float'
|
253
|
+
expect(@test.incr(:mew, 'a')).to eq(err_msg)
|
254
|
+
|
255
|
+
#increase string field
|
256
|
+
err_msg = 'ERR hash value is not a number'
|
257
|
+
expect(@test.incr(:qux, 10)).to eq(err_msg)
|
258
|
+
end
|
259
|
+
|
260
|
+
it 'should set default value for increment is 1' do
|
261
|
+
@redis.set(@test.key(:foo), 0)
|
262
|
+
@test.incr(:foo)
|
263
|
+
expect(@redis.get(@test.key(:foo))).to eq(1.to_s)
|
264
|
+
end
|
265
|
+
|
266
|
+
it 'should raise exception when incr not live attribute' do
|
267
|
+
err_msg = 'baz is not a live attributes'
|
268
|
+
expect(@test.incr(:baz)).to eq(err_msg)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
describe '#getter' do
|
273
|
+
it 'should read attribute value from db
|
274
|
+
in case the value is not exist in redis' do
|
275
|
+
# redis not contain attribute value
|
276
|
+
expect(@redis.get(@test.key(:foo))).to eq(nil)
|
277
|
+
# fetch cache still return attribute value
|
278
|
+
expect(@test.foo).to eq(@test.read_attribute(:foo))
|
279
|
+
# redis should contain attribute value now
|
280
|
+
expect(@redis.get(@test.key(:foo))).to eq(@test.foo.to_s)
|
281
|
+
end
|
282
|
+
|
283
|
+
it 'should return attribute value from redis if it exist in redis' do
|
284
|
+
@test.foo = 10
|
285
|
+
@redis.set(@test.key(:foo), 20)
|
286
|
+
expect(@test.foo).to eq(20)
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe LionAttr do
|
4
|
+
|
5
|
+
describe "configuration" do
|
6
|
+
it "can be configured to specific redis config" do
|
7
|
+
LionAttr.configure do |config|
|
8
|
+
puts "setting configuration"
|
9
|
+
config.redis_config = { :db => 13 }
|
10
|
+
end
|
11
|
+
|
12
|
+
class TestClass
|
13
|
+
include Mongoid::Document
|
14
|
+
include LionAttr
|
15
|
+
field :foo, type: Integer
|
16
|
+
|
17
|
+
live :foo
|
18
|
+
end
|
19
|
+
|
20
|
+
@test = TestClass.new
|
21
|
+
@test.save
|
22
|
+
redis = Redis.new :db => 13
|
23
|
+
expect(redis.exists "TestClass").to be true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/spec/mongoid.yml
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'bundler/setup'
|
3
|
+
Bundler.setup
|
4
|
+
|
5
|
+
require 'mongoid'
|
6
|
+
|
7
|
+
Mongoid.load!("#{File.dirname(__FILE__)}/mongoid.yml", :test)
|
8
|
+
|
9
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
|
10
|
+
|
11
|
+
require 'lion_attr'
|
12
|
+
|
13
|
+
RSpec.configure do |config|
|
14
|
+
config.order = :random
|
15
|
+
|
16
|
+
config.color = true
|
17
|
+
|
18
|
+
config.tty = true
|
19
|
+
|
20
|
+
config.expect_with :rspec do |expectations|
|
21
|
+
expectations.syntax = :expect
|
22
|
+
end
|
23
|
+
|
24
|
+
config.mock_with :rspec do |mocks|
|
25
|
+
mocks.syntax = :expect
|
26
|
+
end
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: lion-attr
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Victor Tran
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-02-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: redis
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: mongoid
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '4.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '4.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: connection_pool
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.1'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.1'
|
69
|
+
description: LionAttr uses Redis to store Mongoid object in-mem giving it fast accessibility.
|
70
|
+
It also gives convenience to manipulate numeric attributes which will be called
|
71
|
+
live attrbutes. With live attributes, you can increase, decrease database for high
|
72
|
+
performance.
|
73
|
+
email:
|
74
|
+
- vu.tran54@gmail.com
|
75
|
+
executables: []
|
76
|
+
extensions: []
|
77
|
+
extra_rdoc_files: []
|
78
|
+
files:
|
79
|
+
- MIT-LICENSE
|
80
|
+
- Rakefile
|
81
|
+
- lib/lion_attr.rb
|
82
|
+
- lib/lion_attr/bson.rb
|
83
|
+
- lib/lion_attr/config.rb
|
84
|
+
- lib/lion_attr/internal_redis.rb
|
85
|
+
- lib/lion_attr/redis_pool.rb
|
86
|
+
- lib/lion_attr/version.rb
|
87
|
+
- lib/tasks/lion_attr_tasks.rake
|
88
|
+
- spec/config_spec.rb
|
89
|
+
- spec/lion_attr_spec.rb
|
90
|
+
- spec/lion_attr_with_custom_configuration_spec.rb
|
91
|
+
- spec/mongoid.yml
|
92
|
+
- spec/spec_helper.rb
|
93
|
+
homepage: http://github.com/tranvictor
|
94
|
+
licenses:
|
95
|
+
- MIT
|
96
|
+
metadata: {}
|
97
|
+
post_install_message:
|
98
|
+
rdoc_options: []
|
99
|
+
require_paths:
|
100
|
+
- lib
|
101
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '0'
|
106
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
requirements: []
|
112
|
+
rubyforge_project:
|
113
|
+
rubygems_version: 2.4.3
|
114
|
+
signing_key:
|
115
|
+
specification_version: 4
|
116
|
+
summary: LionAttr gives convenience of caching Mongoid objects and increase counter
|
117
|
+
without touching database.
|
118
|
+
test_files:
|
119
|
+
- spec/config_spec.rb
|
120
|
+
- spec/lion_attr_spec.rb
|
121
|
+
- spec/lion_attr_with_custom_configuration_spec.rb
|
122
|
+
- spec/mongoid.yml
|
123
|
+
- spec/spec_helper.rb
|
124
|
+
has_rdoc:
|