ohm 0.0.31 → 0.0.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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
|
|