lion-attr 0.1.0
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.
- 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:
|