risky 1.0.1 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.rspec +1 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +38 -0
- data/README.markdown +8 -4
- data/Rakefile.rb +31 -0
- data/lib/risky.rb +274 -265
- data/lib/risky/gzip.rb +28 -0
- data/lib/risky/indexes.rb +4 -4
- data/lib/risky/inflector.rb +337 -0
- data/lib/risky/list_keys.rb +58 -0
- data/lib/risky/paginated_collection.rb +11 -0
- data/lib/risky/secondary_indexes.rb +196 -0
- data/lib/risky/version.rb +1 -1
- data/risky.gemspec +22 -0
- data/spec/risky/cron_list_spec.rb +52 -0
- data/spec/risky/crud_spec.rb +69 -0
- data/spec/risky/enumerable_spec.rb +45 -0
- data/spec/risky/gzip_spec.rb +73 -0
- data/spec/risky/indexes_spec.rb +34 -0
- data/spec/risky/resolver_spec.rb +55 -0
- data/spec/risky/secondary_indexes_spec.rb +222 -0
- data/spec/risky/threads_spec.rb +57 -0
- data/spec/risky_spec.rb +100 -0
- data/spec/spec_helper.rb +40 -0
- metadata +87 -27
- data/lib/risky/all.rb +0 -4
- data/lib/risky/threadsafe.rb +0 -42
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class Indexed < Risky
|
4
|
+
include Risky::ListKeys
|
5
|
+
include Risky::Indexes
|
6
|
+
|
7
|
+
bucket :risky_indexes
|
8
|
+
value :value
|
9
|
+
value :unique
|
10
|
+
index :value
|
11
|
+
index :unique, :unique => true
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
describe 'indexes' do
|
16
|
+
before :all do
|
17
|
+
Indexed.delete_all
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'can index a string' do
|
21
|
+
o = Indexed.new 'test', 'value' => 'value'
|
22
|
+
o.save.should_not be_false
|
23
|
+
Indexed.by_value('value').should === o
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'can keep values unique (mostly)' do
|
27
|
+
o = Indexed.new '1', 'unique' => 'u'
|
28
|
+
o.save.should_not be_false
|
29
|
+
|
30
|
+
o2 = Indexed.new '2', 'unique' => 'u'
|
31
|
+
o2.save.should be_false
|
32
|
+
o2.errors[:unique].should == 'taken'
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'risky/resolver'
|
3
|
+
|
4
|
+
Thread.abort_on_exception = true
|
5
|
+
|
6
|
+
class Multi < Risky
|
7
|
+
include Risky::ListKeys
|
8
|
+
include Risky::Resolver
|
9
|
+
|
10
|
+
bucket :risky_mult
|
11
|
+
allow_mult
|
12
|
+
value :users, :default => []
|
13
|
+
value :union, :resolve => :union
|
14
|
+
value :intersection, :resolve => Risky::Resolver::Resolvers.method(:intersection)
|
15
|
+
value :max, :resolve => :max
|
16
|
+
value :min, :resolve => :min
|
17
|
+
value :merge, :resolve => :merge
|
18
|
+
value :custom, :resolve => lambda { |xs|
|
19
|
+
:custom
|
20
|
+
}
|
21
|
+
|
22
|
+
def self.merge(v)
|
23
|
+
p = super v
|
24
|
+
|
25
|
+
p.users = v.map(&:users).min
|
26
|
+
|
27
|
+
p
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def test(property, ins, out)
|
32
|
+
it property do
|
33
|
+
conflict(Multi, property, ins)[property].should == out
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def set_test(property, ins, out)
|
38
|
+
it property do
|
39
|
+
conflict(Multi, property, ins)[property].to_set.should == out.to_set
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
describe Risky::Resolver do
|
45
|
+
set_test 'union', [[1], [2]], [1,2]
|
46
|
+
set_test 'union', [[1], nil], [1]
|
47
|
+
set_test 'union', [[1,4,1], [2,3], [4,4]], [1,2,3,4]
|
48
|
+
set_test 'intersection', [[1,2],[]], []
|
49
|
+
set_test 'intersection', [[1,2,3,4], [1,2,3], [2,3,4]], [2,3]
|
50
|
+
test 'min', [0,1,2,3], 0
|
51
|
+
test 'max', [0,2,4,2], 4
|
52
|
+
test 'max', [nil, nil], nil
|
53
|
+
test 'max', [nil, 4], 4
|
54
|
+
test 'custom', ['a', 'b', 'c'], :custom
|
55
|
+
end
|
@@ -0,0 +1,222 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class Album < Risky
|
4
|
+
include Risky::ListKeys
|
5
|
+
include Risky::SecondaryIndexes
|
6
|
+
|
7
|
+
bucket :risky_albums
|
8
|
+
allow_mult
|
9
|
+
index2i :artist_id, :map => true
|
10
|
+
index2i :label_key, :map => '_key', :finder => :find_by_id, :allow_nil => true
|
11
|
+
index2i :genre, :type => :bin, :allow_nil => true
|
12
|
+
index2i :tags, :type => :bin, :multi => true, :allow_nil => true
|
13
|
+
value :name
|
14
|
+
value :year
|
15
|
+
end
|
16
|
+
|
17
|
+
class Artist < Risky
|
18
|
+
include Risky::ListKeys
|
19
|
+
|
20
|
+
bucket :risky_artists
|
21
|
+
value :name
|
22
|
+
end
|
23
|
+
|
24
|
+
class Label < Risky
|
25
|
+
include Risky::ListKeys
|
26
|
+
|
27
|
+
bucket :risky_labels
|
28
|
+
value :name
|
29
|
+
|
30
|
+
def self.find_by_id(id)
|
31
|
+
find(id)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class City < Risky
|
36
|
+
include Risky::SecondaryIndexes
|
37
|
+
|
38
|
+
bucket :risky_cities
|
39
|
+
index2i :country_id, :type => :invalid, :allow_nil => true
|
40
|
+
value :name
|
41
|
+
value :details
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
describe Risky::SecondaryIndexes do
|
46
|
+
let(:artist) { Artist.create(1, :name => 'Motorhead') }
|
47
|
+
let(:label) { Label.create(1, :name => 'Bronze Records') }
|
48
|
+
|
49
|
+
before :each do
|
50
|
+
Album.delete_all
|
51
|
+
Artist.delete_all
|
52
|
+
Label.delete_all
|
53
|
+
end
|
54
|
+
|
55
|
+
it "sets indexes on initialize" do
|
56
|
+
album = Album.new(1, {:name => 'Bomber', :year => 1979}, {:artist_id => 2})
|
57
|
+
album.indexes2i.should == {"artist_id" => 2 }
|
58
|
+
end
|
59
|
+
|
60
|
+
it "defines getter and setter methods" do
|
61
|
+
album = Album.new(1)
|
62
|
+
album.artist_id = 1
|
63
|
+
album.artist_id.should == 1
|
64
|
+
end
|
65
|
+
|
66
|
+
it "defines association getter and setter methods" do
|
67
|
+
album = Album.new(1)
|
68
|
+
album.artist = artist
|
69
|
+
album.artist.should == artist
|
70
|
+
end
|
71
|
+
|
72
|
+
it "defines association getter and setter methods when using suffix" do
|
73
|
+
album = Album.new(1)
|
74
|
+
album.label = label
|
75
|
+
album.label.should == label
|
76
|
+
end
|
77
|
+
|
78
|
+
it "can use a custom finder" do
|
79
|
+
album = Album.create(1, {:name => 'Bomber', :year => 1979},
|
80
|
+
{:artist_id => artist.id, :label_key => label.id})
|
81
|
+
|
82
|
+
Label.should_receive(:find_by_id).with(label.id).and_return(label)
|
83
|
+
|
84
|
+
album.label.should == label
|
85
|
+
end
|
86
|
+
|
87
|
+
it "resets association if associated object is not saved" do
|
88
|
+
artist = Artist.new('new_key')
|
89
|
+
album = Album.new('new_key')
|
90
|
+
album.artist = artist
|
91
|
+
album.artist.should be_nil
|
92
|
+
end
|
93
|
+
|
94
|
+
it "assigns attributs after association assignment" do
|
95
|
+
album = Album.new(1)
|
96
|
+
album.artist = artist
|
97
|
+
album.artist_id.should == artist.id
|
98
|
+
end
|
99
|
+
|
100
|
+
it "assigns association after attribute assignment" do
|
101
|
+
album = Album.new(1)
|
102
|
+
album.artist_id = artist.id
|
103
|
+
album.artist.should == artist
|
104
|
+
end
|
105
|
+
|
106
|
+
it "saves a model with indexes" do
|
107
|
+
album = Album.new(1, {:name => 'Ace of Spades' }, { :artist_id => 1 }).save
|
108
|
+
album.artist_id.should == 1
|
109
|
+
end
|
110
|
+
|
111
|
+
it "creates a model with indexes" do
|
112
|
+
album = Album.create(1, {:name => 'Ace of Spades' }, { :artist_id => 1 })
|
113
|
+
album.artist_id.should == 1
|
114
|
+
end
|
115
|
+
|
116
|
+
it "persists association after save" do
|
117
|
+
album = Album.new('persist_key')
|
118
|
+
album.name = 'Ace of Spades'
|
119
|
+
album.artist_id = artist.id
|
120
|
+
album.save
|
121
|
+
|
122
|
+
album.artist.should == artist
|
123
|
+
album.artist_id.should == artist.id
|
124
|
+
|
125
|
+
album.reload
|
126
|
+
|
127
|
+
album.artist.should == artist
|
128
|
+
album.artist_id.should == artist.id
|
129
|
+
|
130
|
+
album = Album.find(album.key)
|
131
|
+
|
132
|
+
album.artist.should == artist
|
133
|
+
album.artist_id.should == artist.id
|
134
|
+
end
|
135
|
+
|
136
|
+
it "finds first by int secondary index" do
|
137
|
+
album = Album.create(1, {:name => 'Bomber', :year => 1979},
|
138
|
+
{:artist_id => artist.id})
|
139
|
+
|
140
|
+
albums = Album.find_by_index(:artist_id, artist.id)
|
141
|
+
albums.should == album
|
142
|
+
end
|
143
|
+
|
144
|
+
it "finds all by int secondary index" do
|
145
|
+
album1 = Album.create(1, {:name => 'Bomber', :year => 1979},
|
146
|
+
{:artist_id => artist.id, :label_key => label.id})
|
147
|
+
album2 = Album.create(2, {:name => 'Ace Of Spaces', :year => 1980},
|
148
|
+
{:artist_id => artist.id, :label_key => label.id})
|
149
|
+
|
150
|
+
albums = Album.find_all_by_index(:artist_id, artist.id)
|
151
|
+
albums.should include(album1)
|
152
|
+
albums.should include(album2)
|
153
|
+
end
|
154
|
+
|
155
|
+
it "finds all by binary secondary index" do
|
156
|
+
album = Album.create(1, {:name => 'Bomber', :year => 1979},
|
157
|
+
{:artist_id => artist.id, :label_key => label.id, :genre => 'heavy'})
|
158
|
+
|
159
|
+
Album.find_all_by_index(:genre, 'heavy').should == [album]
|
160
|
+
end
|
161
|
+
|
162
|
+
it "finds all by multi binary secondary index" do
|
163
|
+
album = Album.create(1, {:name => 'Bomber', :year => 1979},
|
164
|
+
{:artist_id => artist.id, :label_key => label.id,
|
165
|
+
:tags => ['rock', 'heavy']})
|
166
|
+
|
167
|
+
Album.find_all_by_index(:tags, 'heavy').should == [album]
|
168
|
+
Album.find_all_by_index(:tags, 'rock').should == [album]
|
169
|
+
end
|
170
|
+
|
171
|
+
it "paginates keys" do
|
172
|
+
album1 = Album.create('1', {:name => 'Bomber', :year => 1979},
|
173
|
+
{:artist_id => artist.id, :label_key => label.id})
|
174
|
+
album2 = Album.create('2', {:name => 'Ace Of Spaces', :year => 1980},
|
175
|
+
{:artist_id => artist.id, :label_key => label.id})
|
176
|
+
album3 = Album.create('3', {:name => 'Overkill', :year => 1979},
|
177
|
+
{:artist_id => artist.id, :label_key => label.id})
|
178
|
+
|
179
|
+
page1 = Album.paginate_keys_by_index(:artist_id, artist.id, :max_results => 2)
|
180
|
+
page1.should == ['1', '2']
|
181
|
+
page1.continuation.should_not be_blank
|
182
|
+
|
183
|
+
page2 = Album.paginate_keys_by_index(:artist_id, artist.id, :max_results => 2, :continuation => page1.continuation)
|
184
|
+
page2.should == ['3']
|
185
|
+
page2.continuation.should be_blank
|
186
|
+
end
|
187
|
+
|
188
|
+
it "paginates risky objects" do
|
189
|
+
album1 = Album.create('1', {:name => 'Bomber', :year => 1979},
|
190
|
+
{:artist_id => artist.id, :label_key => label.id})
|
191
|
+
album2 = Album.create('2', {:name => 'Ace Of Spaces', :year => 1980},
|
192
|
+
{:artist_id => artist.id, :label_key => label.id})
|
193
|
+
album3 = Album.create('3', {:name => 'Overkill', :year => 1979},
|
194
|
+
{:artist_id => artist.id, :label_key => label.id})
|
195
|
+
|
196
|
+
page1 = Album.paginate_by_index(:artist_id, artist.id, :max_results => 2)
|
197
|
+
page1.should == [album1, album2]
|
198
|
+
page1.continuation.should_not be_blank
|
199
|
+
|
200
|
+
page2 = Album.paginate_by_index(:artist_id, artist.id, :max_results => 2, :continuation => page1.continuation)
|
201
|
+
page2.should == [album3]
|
202
|
+
page2.continuation.should be_blank
|
203
|
+
end
|
204
|
+
|
205
|
+
it "raises an exception when index is nil" do
|
206
|
+
album = Album.new(1)
|
207
|
+
expect { album.save }.to raise_error(ArgumentError)
|
208
|
+
end
|
209
|
+
|
210
|
+
it "raises an exception when type is invalid" do
|
211
|
+
city = City.new(1)
|
212
|
+
expect { city.save }.to raise_error(TypeError)
|
213
|
+
end
|
214
|
+
|
215
|
+
it "can inspect a model" do
|
216
|
+
album = Album.new(1, { :name => 'Bomber' }, { :artist_id => 2 })
|
217
|
+
|
218
|
+
album.inspect.should match(/Album 1/)
|
219
|
+
album.inspect.should match(/"name"=>"Bomber"/)
|
220
|
+
album.inspect.should match(/"artist_id"=>2/)
|
221
|
+
end
|
222
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class Crud < Risky
|
4
|
+
include Risky::ListKeys
|
5
|
+
|
6
|
+
bucket :risky_crud
|
7
|
+
value :value
|
8
|
+
end
|
9
|
+
|
10
|
+
class Concurrent < Risky
|
11
|
+
include Risky::ListKeys
|
12
|
+
|
13
|
+
bucket :risky_concurrent
|
14
|
+
allow_mult
|
15
|
+
value :v
|
16
|
+
|
17
|
+
# Merge value v together as a list
|
18
|
+
def self.merge(versions)
|
19
|
+
p = super versions
|
20
|
+
p.v = versions.inject([]) do |merged, version|
|
21
|
+
merged + [*version.v]
|
22
|
+
end.uniq
|
23
|
+
p
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
describe 'Threads' do
|
29
|
+
it 'supports concurrent modification' do
|
30
|
+
Concurrent.bucket.props['allow_mult'].should be_true
|
31
|
+
|
32
|
+
# Riak doesn't do well with concurrent *new* writes, so get an existing
|
33
|
+
# value in there first.
|
34
|
+
c = Concurrent.get_or_new('c')
|
35
|
+
c.v = []
|
36
|
+
c.save(:w => :all)
|
37
|
+
|
38
|
+
workers = 10
|
39
|
+
|
40
|
+
# Make a bunch of concurrent writes
|
41
|
+
(0...workers).map do |i|
|
42
|
+
Thread.new do
|
43
|
+
# Give them a little bit of jitter, just to make the vclocks interesting
|
44
|
+
sleep rand/6
|
45
|
+
c = Concurrent.get_or_new('c')
|
46
|
+
c.v << i
|
47
|
+
c.save or raise
|
48
|
+
end
|
49
|
+
end.each do |thread|
|
50
|
+
thread.join
|
51
|
+
end
|
52
|
+
|
53
|
+
# Check to ensure we obsoleted or have an extant write for every thread.
|
54
|
+
final = Concurrent['c', {:r => :all}]
|
55
|
+
final.v.compact.sort.should == (0...workers).to_a
|
56
|
+
end
|
57
|
+
end
|
data/spec/risky_spec.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class User < Risky
|
4
|
+
include Risky::ListKeys
|
5
|
+
|
6
|
+
bucket 'risky_users'
|
7
|
+
allow_mult
|
8
|
+
value :admin, :default => false
|
9
|
+
value :age
|
10
|
+
end
|
11
|
+
|
12
|
+
describe 'Risky' do
|
13
|
+
before :each do
|
14
|
+
User.delete_all
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'has a bucket' do
|
18
|
+
User.bucket.should be_kind_of Riak::Bucket
|
19
|
+
end
|
20
|
+
|
21
|
+
it "can store a value and retrieve it" do
|
22
|
+
user = User.new('test', 'admin' => true)
|
23
|
+
user.save.should_not be_false
|
24
|
+
|
25
|
+
user.key.should == 'test'
|
26
|
+
user.admin.should == true
|
27
|
+
end
|
28
|
+
|
29
|
+
it "can find" do
|
30
|
+
user = User.create('test')
|
31
|
+
User.find('test').should == user
|
32
|
+
end
|
33
|
+
|
34
|
+
it "can find all by key" do
|
35
|
+
user = User.create('test')
|
36
|
+
User.find_all_by_key(['test']).should == [user]
|
37
|
+
end
|
38
|
+
|
39
|
+
it "returns id as integer" do
|
40
|
+
user = User.new
|
41
|
+
user.id = 1
|
42
|
+
user.save
|
43
|
+
user.id.should == 1
|
44
|
+
end
|
45
|
+
|
46
|
+
it "returns id as string" do
|
47
|
+
user = User.new
|
48
|
+
user.id = 'test'
|
49
|
+
user.save
|
50
|
+
user.id.should == 'test'
|
51
|
+
end
|
52
|
+
|
53
|
+
it "can update attribute" do
|
54
|
+
user = User.new('test', 'admin' => true)
|
55
|
+
user.update_attribute(:admin, false)
|
56
|
+
user.admin.should be_false
|
57
|
+
end
|
58
|
+
|
59
|
+
it "can update attributes" do
|
60
|
+
user = User.new('test', 'admin' => true)
|
61
|
+
user.update_attributes({:admin => false})
|
62
|
+
user.admin.should be_false
|
63
|
+
end
|
64
|
+
|
65
|
+
context "conflict resolution" do
|
66
|
+
let(:key) { 'siblings' }
|
67
|
+
|
68
|
+
before :each do
|
69
|
+
User.new(key, 'age' => 20).save
|
70
|
+
|
71
|
+
user1 = User[key]
|
72
|
+
user2 = User[key]
|
73
|
+
|
74
|
+
user1.age = 21
|
75
|
+
user1.save
|
76
|
+
|
77
|
+
# no conflict
|
78
|
+
User.bucket.get(key).siblings.length.should == 1
|
79
|
+
|
80
|
+
user2.age = 22
|
81
|
+
user2.save
|
82
|
+
|
83
|
+
# it creates a new sibling because of conflict
|
84
|
+
User.bucket.get(key).siblings.length.should == 2
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'it resolves the conflict on risky level' do
|
88
|
+
user = User[key]
|
89
|
+
User.bucket.get(key).siblings.length.should == 2
|
90
|
+
user.riak_object.siblings.length.should == 1
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'resolves the conflict on riak data level' do
|
94
|
+
user = User[key]
|
95
|
+
user.save
|
96
|
+
user.riak_object.siblings.length.should == 1
|
97
|
+
User.bucket.get(key).siblings.length.should == 1
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|