risky 1.0.1 → 1.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/.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
|