ohm 0.0.31 → 0.0.32
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +34 -5
- data/lib/ohm.rb +26 -27
- data/lib/ohm/redis.rb +15 -14
- data/test/indices_test.rb +33 -0
- data/test/model_test.rb +19 -12
- data/test/mutex_test.rb +53 -0
- data/test/redis_test.rb +17 -0
- data/test/test_helper.rb +1 -1
- metadata +2 -2
data/README.markdown
CHANGED
@@ -106,13 +106,13 @@ when you retrieve the value.
|
|
106
106
|
|
107
107
|
A `set` in Redis is an unordered list, with an external behavior similar
|
108
108
|
to that of Ruby arrays, but optimized for faster membership lookups.
|
109
|
-
It's used
|
109
|
+
It's used internally by Ohm to keep track of the instances of each model
|
110
110
|
and for generating and maintaining indexes.
|
111
111
|
|
112
112
|
### list
|
113
113
|
|
114
|
-
A `list` is like an array in Ruby. It's perfectly suited for queues
|
115
|
-
for keeping elements in order.
|
114
|
+
A `list` is like an array in Ruby. It's perfectly suited for queues
|
115
|
+
and for keeping elements in order.
|
116
116
|
|
117
117
|
### counter
|
118
118
|
|
@@ -122,16 +122,45 @@ the value, but you can not assign it. In the example above, we used a
|
|
122
122
|
counter attribute for tracking votes. As the incr and decr operations
|
123
123
|
are atomic, you can rest assured a vote won't be counted twice.
|
124
124
|
|
125
|
+
Persistence strategy
|
126
|
+
--------------------
|
127
|
+
|
128
|
+
The attributes declared with `attribute` are only persisted after
|
129
|
+
calling `save`. If the object is in an invalid state, no value is sent
|
130
|
+
to Redis (see the section on **Validations** below).
|
131
|
+
|
132
|
+
Operations on attributes of type `list`, `set` and `counter` are
|
133
|
+
possible only after the object is created (when it has an assigned
|
134
|
+
`id`). Any operation on these kinds of attributes is performed
|
135
|
+
immediately, without running the object validations. This design yields
|
136
|
+
better performance than running the validations on each operation or
|
137
|
+
buffering the operations and waiting for a call to `save`.
|
138
|
+
|
139
|
+
For most use cases, this pattern doesn't represent a problem.
|
140
|
+
If you need to check for validity before operating on lists, sets or
|
141
|
+
counters, you can use this pattern:
|
142
|
+
|
143
|
+
if event.valid?
|
144
|
+
event.comments << "Great event!"
|
145
|
+
end
|
146
|
+
|
147
|
+
If you are saving the object, this will suffice:
|
148
|
+
|
149
|
+
if event.save
|
150
|
+
event.comments << "Wonderful event!"
|
151
|
+
end
|
152
|
+
|
153
|
+
|
125
154
|
Associations
|
126
155
|
------------
|
127
156
|
|
128
157
|
Ohm lets you use collections (lists and sets) to represent associations.
|
129
|
-
For this, you only need to provide a second
|
158
|
+
For this, you only need to provide a second parameter when declaring a
|
130
159
|
list or a set:
|
131
160
|
|
132
161
|
set :attendees, Person
|
133
162
|
|
134
|
-
After this,
|
163
|
+
After this, every time you refer to `event.attendees` you will be talking
|
135
164
|
about instances of the model `Person`. If you want to get the raw values
|
136
165
|
of the set, you can use `event.attendees.raw`.
|
137
166
|
|
data/lib/ohm.rb
CHANGED
@@ -183,6 +183,8 @@ module Ohm
|
|
183
183
|
db.rpush(key, value)
|
184
184
|
end
|
185
185
|
|
186
|
+
alias push <<
|
187
|
+
|
186
188
|
# @return [String] Return and remove the last element of the list.
|
187
189
|
def pop
|
188
190
|
db.rpop(key)
|
@@ -353,16 +355,6 @@ module Ohm
|
|
353
355
|
end
|
354
356
|
end
|
355
357
|
|
356
|
-
class RedefinitionError < Error
|
357
|
-
def initialize(att)
|
358
|
-
@att = att
|
359
|
-
end
|
360
|
-
|
361
|
-
def message
|
362
|
-
"Cannot redefine #{@att.inspect}"
|
363
|
-
end
|
364
|
-
end
|
365
|
-
|
366
358
|
@@attributes = Hash.new { |hash, key| hash[key] = [] }
|
367
359
|
@@collections = Hash.new { |hash, key| hash[key] = [] }
|
368
360
|
@@counters = Hash.new { |hash, key| hash[key] = [] }
|
@@ -379,8 +371,6 @@ module Ohm
|
|
379
371
|
#
|
380
372
|
# @param name [Symbol] Name of the attribute.
|
381
373
|
def self.attribute(name)
|
382
|
-
raise RedefinitionError, name if attributes.include?(name)
|
383
|
-
|
384
374
|
define_method(name) do
|
385
375
|
read_local(name)
|
386
376
|
end
|
@@ -389,7 +379,7 @@ module Ohm
|
|
389
379
|
write_local(name, value)
|
390
380
|
end
|
391
381
|
|
392
|
-
attributes << name
|
382
|
+
attributes << name unless attributes.include?(name)
|
393
383
|
end
|
394
384
|
|
395
385
|
# Defines a counter attribute for the model. This attribute can't be assigned, only incremented
|
@@ -397,13 +387,11 @@ module Ohm
|
|
397
387
|
#
|
398
388
|
# @param name [Symbol] Name of the counter.
|
399
389
|
def self.counter(name)
|
400
|
-
raise RedefinitionError, name if counters.include?(name)
|
401
|
-
|
402
390
|
define_method(name) do
|
403
391
|
read_local(name).to_i
|
404
392
|
end
|
405
393
|
|
406
|
-
counters << name
|
394
|
+
counters << name unless counters.include?(name)
|
407
395
|
end
|
408
396
|
|
409
397
|
# Defines a list attribute for the model. It can be accessed only after the model instance
|
@@ -411,10 +399,8 @@ module Ohm
|
|
411
399
|
#
|
412
400
|
# @param name [Symbol] Name of the list.
|
413
401
|
def self.list(name, model = nil)
|
414
|
-
raise RedefinitionError, name if collections.include?(name)
|
415
|
-
|
416
402
|
attr_list_reader(name, model)
|
417
|
-
collections << name
|
403
|
+
collections << name unless collections.include?(name)
|
418
404
|
end
|
419
405
|
|
420
406
|
# Defines a set attribute for the model. It can be accessed only after the model instance
|
@@ -423,10 +409,8 @@ module Ohm
|
|
423
409
|
#
|
424
410
|
# @param name [Symbol] Name of the set.
|
425
411
|
def self.set(name, model = nil)
|
426
|
-
raise RedefinitionError, name if collections.include?(name)
|
427
|
-
|
428
412
|
attr_set_reader(name, model)
|
429
|
-
collections << name
|
413
|
+
collections << name unless collections.include?(name)
|
430
414
|
end
|
431
415
|
|
432
416
|
# Creates an index (a set) that will be used for finding instances.
|
@@ -445,9 +429,7 @@ module Ohm
|
|
445
429
|
#
|
446
430
|
# @param name [Symbol] Name of the attribute to be indexed.
|
447
431
|
def self.index(att)
|
448
|
-
|
449
|
-
|
450
|
-
indices << att
|
432
|
+
indices << att unless indices.include?(att)
|
451
433
|
end
|
452
434
|
|
453
435
|
def self.attr_list_reader(name, model = nil)
|
@@ -602,7 +584,7 @@ module Ohm
|
|
602
584
|
false
|
603
585
|
end
|
604
586
|
|
605
|
-
# Lock the object before
|
587
|
+
# Lock the object before executing the block, and release it once the block is done.
|
606
588
|
def mutex
|
607
589
|
lock!
|
608
590
|
yield
|
@@ -761,9 +743,18 @@ module Ohm
|
|
761
743
|
end
|
762
744
|
|
763
745
|
# Lock the object so no other instances can modify it.
|
746
|
+
# This method implements the design pattern for locks
|
747
|
+
# described at: http://code.google.com/p/redis/wiki/SetnxCommand
|
748
|
+
#
|
764
749
|
# @see Model#mutex
|
765
750
|
def lock!
|
766
|
-
|
751
|
+
until db.setnx(key(:_lock), lock_timeout)
|
752
|
+
next unless lock = db.get(key(:_lock))
|
753
|
+
sleep(0.5) and next unless lock_expired?(lock)
|
754
|
+
|
755
|
+
break unless lock = db.getset(key(:_lock), lock_timeout)
|
756
|
+
break if lock_expired?(lock)
|
757
|
+
end
|
767
758
|
end
|
768
759
|
|
769
760
|
# Release the lock.
|
@@ -771,5 +762,13 @@ module Ohm
|
|
771
762
|
def unlock!
|
772
763
|
db.del(key(:_lock))
|
773
764
|
end
|
765
|
+
|
766
|
+
def lock_timeout
|
767
|
+
Time.now.to_f + 1
|
768
|
+
end
|
769
|
+
|
770
|
+
def lock_expired? lock
|
771
|
+
lock.to_f < Time.now.to_f
|
772
|
+
end
|
774
773
|
end
|
775
774
|
end
|
data/lib/ohm/redis.rb
CHANGED
@@ -7,19 +7,19 @@
|
|
7
7
|
# http://github.com/ezmobius/redis-rb/
|
8
8
|
require 'socket'
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
10
|
+
module Ohm
|
11
|
+
begin
|
12
|
+
if (RUBY_VERSION >= '1.9')
|
13
|
+
require 'timeout'
|
14
|
+
RedisTimer = Timeout
|
15
|
+
else
|
16
|
+
require 'system_timer'
|
17
|
+
RedisTimer = SystemTimer
|
18
|
+
end
|
19
|
+
rescue LoadError
|
20
|
+
RedisTimer = nil
|
17
21
|
end
|
18
|
-
rescue LoadError
|
19
|
-
RedisTimer = nil
|
20
|
-
end
|
21
22
|
|
22
|
-
module Ohm
|
23
23
|
class Redis
|
24
24
|
class ProtocolError < RuntimeError
|
25
25
|
def initialize(reply_type)
|
@@ -42,6 +42,7 @@ module Ohm
|
|
42
42
|
:smove => true,
|
43
43
|
:srem => true,
|
44
44
|
:zadd => true,
|
45
|
+
:zincrby => true,
|
45
46
|
:zrem => true,
|
46
47
|
:zscore => true
|
47
48
|
}
|
@@ -136,13 +137,13 @@ module Ohm
|
|
136
137
|
# socket instead will be supported anyway.
|
137
138
|
if @timeout != 0 and RedisTimer
|
138
139
|
begin
|
139
|
-
@sock = TCPSocket.new(host, port
|
140
|
+
@sock = TCPSocket.new(host, port)
|
140
141
|
rescue Timeout::Error
|
141
142
|
@sock = nil
|
142
143
|
raise Timeout::Error, "Timeout connecting to the server"
|
143
144
|
end
|
144
145
|
else
|
145
|
-
@sock = TCPSocket.new(host, port
|
146
|
+
@sock = TCPSocket.new(host, port)
|
146
147
|
end
|
147
148
|
|
148
149
|
@sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
|
@@ -184,7 +185,7 @@ module Ohm
|
|
184
185
|
def call_command(argv)
|
185
186
|
connect unless connected?
|
186
187
|
raw_call_command(argv.dup)
|
187
|
-
rescue Errno::ECONNRESET, Errno::EPIPE
|
188
|
+
rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED
|
188
189
|
if reconnect
|
189
190
|
raw_call_command(argv.dup)
|
190
191
|
else
|
data/test/indices_test.rb
CHANGED
@@ -165,4 +165,37 @@ class IndicesTest < Test::Unit::TestCase
|
|
165
165
|
assert_equal [user], User.find(:update => "CORRECTED - UPDATE 2-Suspected US missile strike kills 5 in Pakistan")
|
166
166
|
end
|
167
167
|
end
|
168
|
+
|
169
|
+
context "New indices" do
|
170
|
+
should "populate a new index when the model is saved" do
|
171
|
+
class Event < Ohm::Model
|
172
|
+
attribute :name
|
173
|
+
end
|
174
|
+
|
175
|
+
foo = Event.create(:name => "Foo")
|
176
|
+
|
177
|
+
assert_raise(Ohm::Model::IndexNotFound) { Event.find(:name => "Foo") }
|
178
|
+
|
179
|
+
class Event < Ohm::Model
|
180
|
+
index :name
|
181
|
+
end
|
182
|
+
|
183
|
+
# Find works correctly once the index is added.
|
184
|
+
assert_nothing_raised { Event.find(:name => "Foo") }
|
185
|
+
|
186
|
+
# The index was added after foo was created.
|
187
|
+
assert Event.find(:name => "Foo").empty?
|
188
|
+
|
189
|
+
bar = Event.create(:name => "Bar")
|
190
|
+
|
191
|
+
# Bar was indexed properly.
|
192
|
+
assert_equal bar, Event.find(:name => "Bar").first
|
193
|
+
|
194
|
+
# Saving all the objects populates the indices.
|
195
|
+
Event.all.each { |e| e.save }
|
196
|
+
|
197
|
+
# Now foo is indexed.
|
198
|
+
assert_equal foo, Event.find(:name => "Foo").first
|
199
|
+
end
|
200
|
+
end
|
168
201
|
end
|
data/test/model_test.rb
CHANGED
@@ -99,8 +99,8 @@ class TestRedis < Test::Unit::TestCase
|
|
99
99
|
end
|
100
100
|
|
101
101
|
context "Model definition" do
|
102
|
-
should "raise if an attribute is redefined" do
|
103
|
-
|
102
|
+
should "not raise if an attribute is redefined" do
|
103
|
+
assert_nothing_raised do
|
104
104
|
class RedefinedModel < Ohm::Model
|
105
105
|
attribute :name
|
106
106
|
attribute :name
|
@@ -108,8 +108,8 @@ class TestRedis < Test::Unit::TestCase
|
|
108
108
|
end
|
109
109
|
end
|
110
110
|
|
111
|
-
should "raise if a counter is redefined" do
|
112
|
-
|
111
|
+
should "not raise if a counter is redefined" do
|
112
|
+
assert_nothing_raised do
|
113
113
|
class RedefinedModel < Ohm::Model
|
114
114
|
counter :age
|
115
115
|
counter :age
|
@@ -117,8 +117,8 @@ class TestRedis < Test::Unit::TestCase
|
|
117
117
|
end
|
118
118
|
end
|
119
119
|
|
120
|
-
should "raise if a list is redefined" do
|
121
|
-
|
120
|
+
should "not raise if a list is redefined" do
|
121
|
+
assert_nothing_raised do
|
122
122
|
class RedefinedModel < Ohm::Model
|
123
123
|
list :todo
|
124
124
|
list :todo
|
@@ -126,8 +126,8 @@ class TestRedis < Test::Unit::TestCase
|
|
126
126
|
end
|
127
127
|
end
|
128
128
|
|
129
|
-
should "raise if a set is redefined" do
|
130
|
-
|
129
|
+
should "not raise if a set is redefined" do
|
130
|
+
assert_nothing_raised do
|
131
131
|
class RedefinedModel < Ohm::Model
|
132
132
|
set :friends
|
133
133
|
set :friends
|
@@ -135,8 +135,8 @@ class TestRedis < Test::Unit::TestCase
|
|
135
135
|
end
|
136
136
|
end
|
137
137
|
|
138
|
-
should "raise if a collection is redefined" do
|
139
|
-
|
138
|
+
should "not raise if a collection is redefined" do
|
139
|
+
assert_nothing_raised do
|
140
140
|
class RedefinedModel < Ohm::Model
|
141
141
|
list :toys
|
142
142
|
set :toys
|
@@ -144,8 +144,8 @@ class TestRedis < Test::Unit::TestCase
|
|
144
144
|
end
|
145
145
|
end
|
146
146
|
|
147
|
-
should "raise if a index is redefined" do
|
148
|
-
|
147
|
+
should "not raise if a index is redefined" do
|
148
|
+
assert_nothing_raised do
|
149
149
|
class RedefinedModel < Ohm::Model
|
150
150
|
attribute :color
|
151
151
|
index :color
|
@@ -453,6 +453,13 @@ class TestRedis < Test::Unit::TestCase
|
|
453
453
|
assert @post.comments.all.kind_of?(Array)
|
454
454
|
end
|
455
455
|
|
456
|
+
should "append elements with push" do
|
457
|
+
@post.comments.push "1"
|
458
|
+
@post.comments << "2"
|
459
|
+
|
460
|
+
assert_equal ["1", "2"], @post.comments.all
|
461
|
+
end
|
462
|
+
|
456
463
|
should "keep the inserting order" do
|
457
464
|
@post.comments << "1"
|
458
465
|
@post.comments << "2"
|
data/test/mutex_test.rb
CHANGED
@@ -32,5 +32,58 @@ class TestMutex < Test::Unit::TestCase
|
|
32
32
|
p2.join
|
33
33
|
assert t2 > t1
|
34
34
|
end
|
35
|
+
|
36
|
+
should "allow an instance to lock a record if the previous lock is expired" do
|
37
|
+
@p1.send(:lock!)
|
38
|
+
@p2.mutex do
|
39
|
+
assert true
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
should "work if two clients are fighting for the lock" do
|
44
|
+
@p1.send(:lock!)
|
45
|
+
@p3 = Person[1]
|
46
|
+
@p4 = Person[1]
|
47
|
+
|
48
|
+
assert_nothing_raised do
|
49
|
+
p1 = Thread.new { @p1.mutex {} }
|
50
|
+
p2 = Thread.new { @p2.mutex {} }
|
51
|
+
p3 = Thread.new { @p3.mutex {} }
|
52
|
+
p4 = Thread.new { @p4.mutex {} }
|
53
|
+
p1.join
|
54
|
+
p2.join
|
55
|
+
p3.join
|
56
|
+
p4.join
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
should "yield the right result after a lock fight" do
|
61
|
+
class Candidate < Ohm::Model
|
62
|
+
attribute :name
|
63
|
+
counter :votes
|
64
|
+
end
|
65
|
+
|
66
|
+
@candidate = Candidate.create :name => "Foo"
|
67
|
+
@candidate.send(:lock!)
|
68
|
+
|
69
|
+
threads = []
|
70
|
+
|
71
|
+
n = 10
|
72
|
+
m = 9
|
73
|
+
|
74
|
+
n.times do |i|
|
75
|
+
threads << Thread.new do
|
76
|
+
m.times do |i|
|
77
|
+
@candidate.mutex do
|
78
|
+
sleep 0.1
|
79
|
+
@candidate.incr(:votes)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
threads.each { |t| t.join }
|
86
|
+
assert_equal n * m, @candidate.votes
|
87
|
+
end
|
35
88
|
end
|
36
89
|
end
|
data/test/redis_test.rb
CHANGED
@@ -386,6 +386,23 @@ class RedisTest < Test::Unit::TestCase
|
|
386
386
|
end
|
387
387
|
end
|
388
388
|
|
389
|
+
should "increment by a certain amount the score of a zset ZINCRBY" do
|
390
|
+
assert_equal 0, @r.zcard("league")
|
391
|
+
|
392
|
+
@r.zincrby "league", 1, "foo"
|
393
|
+
assert_equal "1", @r.zscore("league", "foo")
|
394
|
+
|
395
|
+
assert_equal 1, @r.zcard("league")
|
396
|
+
|
397
|
+
@r.zincrby "league", 10, "foo"
|
398
|
+
assert_equal "11", @r.zscore("league", "foo")
|
399
|
+
|
400
|
+
@r.set "bar", "2"
|
401
|
+
assert_raises do
|
402
|
+
@r.zincrby "bar", 2, "baz"
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
389
406
|
should "provide info" do
|
390
407
|
%w(last_save_time redis_version total_connections_received connected_clients total_commands_processed connected_slaves uptime_in_seconds used_memory uptime_in_days changes_since_last_save).each do |x|
|
391
408
|
assert @r.info.keys.include?(x)
|
data/test/test_helper.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ohm
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.32
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michel Martens
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date:
|
13
|
+
date: 2010-01-13 00:00:00 -03:00
|
14
14
|
default_executable:
|
15
15
|
dependencies: []
|
16
16
|
|