chimera 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +6 -2
- data/README.rdoc +14 -4
- data/lib/chimera.rb +1 -1
- data/lib/chimera/base.rb +14 -10
- data/lib/chimera/error.rb +2 -0
- data/lib/chimera/finders.rb +20 -8
- data/lib/chimera/persistence.rb +55 -3
- data/lib/riak_raw.rb +12 -3
- data/test/test_chimera.rb +41 -2
- metadata +6 -3
data/History.txt
CHANGED
@@ -1,4 +1,8 @@
|
|
1
|
+
=== 0.0.4 2010-03-07
|
2
|
+
|
3
|
+
* Added support for siblings in riak so that conflicts can be managed.
|
4
|
+
See tests for example and http://blog.basho.com/2010/01/29/why-vector-clocks-are-easy/.
|
5
|
+
|
1
6
|
=== 0.0.1 2010-03-01
|
2
7
|
|
3
|
-
*
|
4
|
-
* Initial release
|
8
|
+
* Initial release
|
data/README.rdoc
CHANGED
@@ -14,6 +14,9 @@ that are automatically created. There's no built in sharding for Redis, but sinc
|
|
14
14
|
only being used for key storage and basic data elements you should be able to go a long
|
15
15
|
way with one Redis server (especially if you use the new Redis VM).
|
16
16
|
|
17
|
+
!! Chimera is alpha. It's not production tested and needs a better test suite. !!
|
18
|
+
!! It's also only tested in Ruby 1.9. !!
|
19
|
+
|
17
20
|
== FEATURES:
|
18
21
|
|
19
22
|
* Uses Riak (http://riak.basho.com/) for your primary storage.
|
@@ -21,10 +24,13 @@ way with one Redis server (especially if you use the new Redis VM).
|
|
21
24
|
* Supports unique and non-unique indexes, as well as basic search indexes (words are stemmed,
|
22
25
|
uses set intersection so you can get an AND/OR search).
|
23
26
|
* Supports a geospatial index type for simple storage and lookup of coordinates.
|
27
|
+
* Uses Typhoeus for communicating with Riak.
|
28
|
+
* Surfaces siblings from Riak so that conflicts can be managed and resolved.
|
24
29
|
|
25
30
|
== ISSUES/NOTES:
|
26
31
|
|
27
32
|
* Experimental. Not yet tested in production environment, use at your own risk.
|
33
|
+
* Only tested with Ruby 1.9. Why not upgrade?
|
28
34
|
* Test coverage needs to be improved.
|
29
35
|
* Documentation is lacking. At the moment reading the tests and sample test/models.rb
|
30
36
|
file are your best bet.
|
@@ -62,10 +68,10 @@ way with one Redis server (especially if you use the new Redis VM).
|
|
62
68
|
Car.find_with_index(:year, 2010)
|
63
69
|
=> [c]
|
64
70
|
|
65
|
-
Car.find_with_index(:description, "rigid boat", :type => :union)
|
71
|
+
Car.find_with_index(:description, :q => "rigid boat", :type => :union)
|
66
72
|
=> [c]
|
67
73
|
|
68
|
-
Car.find_with_index(:description, "rigid boat", :type => :intersect)
|
74
|
+
Car.find_with_index(:description, :q => "rigid boat", :type => :intersect)
|
69
75
|
=> []
|
70
76
|
|
71
77
|
* See tests for more usage examples
|
@@ -75,6 +81,8 @@ way with one Redis server (especially if you use the new Redis VM).
|
|
75
81
|
* Riak Server
|
76
82
|
* Redis Server
|
77
83
|
|
84
|
+
* Ruby 1.9
|
85
|
+
|
78
86
|
* ActiveSupport+ActiveModel 3.0.0.beta
|
79
87
|
|
80
88
|
* uuidtools 2.1.1
|
@@ -89,13 +97,15 @@ way with one Redis server (especially if you use the new Redis VM).
|
|
89
97
|
|
90
98
|
== USEFUL LINKS:
|
91
99
|
|
92
|
-
* http://groups.google.com/group/chimera-lib
|
100
|
+
* Discussion: http://groups.google.com/group/chimera-lib
|
101
|
+
* Email: ben dot myles at gmail dot com
|
102
|
+
* Twitter: benmyles
|
93
103
|
|
94
104
|
== LICENSE:
|
95
105
|
|
96
106
|
(The MIT License)
|
97
107
|
|
98
|
-
Copyright (c) 2010
|
108
|
+
Copyright (c) 2010 Ben Myles
|
99
109
|
|
100
110
|
Permission is hereby granted, free of charge, to any person obtaining
|
101
111
|
a copy of this software and associated documentation files (the
|
data/lib/chimera.rb
CHANGED
data/lib/chimera/base.rb
CHANGED
@@ -22,7 +22,8 @@ module Chimera
|
|
22
22
|
extend ActiveModel::Callbacks
|
23
23
|
define_model_callbacks :create, :save, :destroy
|
24
24
|
|
25
|
-
attr_accessor :id, :attributes, :orig_attributes, :riak_response, :associations
|
25
|
+
attr_accessor :id, :attributes, :orig_attributes, :riak_response, :associations,
|
26
|
+
:sibling_attributes
|
26
27
|
|
27
28
|
def self.use_config(key)
|
28
29
|
@config = (Chimera.config[key.to_sym] || raise(Chimera::Error::MissingConfig,":#{key}"))
|
@@ -33,15 +34,17 @@ module Chimera
|
|
33
34
|
end
|
34
35
|
|
35
36
|
def self.connection(server)
|
36
|
-
Thread.current["Chimera::#{self.to_s}::#{server}::connection"] ||=
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
37
|
+
Thread.current["Chimera::#{self.to_s}::#{server}::connection"] ||= new_connection(server)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.new_connection(server)
|
41
|
+
case server.to_sym
|
42
|
+
when :redis
|
43
|
+
Redis.new(self.config[:redis])
|
44
|
+
when :riak_raw
|
45
|
+
RiakRaw::Client.new(self.config[:riak_raw][:host], self.config[:riak_raw][:port])
|
46
|
+
else
|
47
|
+
nil
|
45
48
|
end
|
46
49
|
end
|
47
50
|
|
@@ -76,6 +79,7 @@ module Chimera
|
|
76
79
|
@orig_attributes = @attributes.clone
|
77
80
|
@id = id
|
78
81
|
@new = is_new
|
82
|
+
@sibling_attributes = nil
|
79
83
|
end
|
80
84
|
|
81
85
|
def id=(val)
|
data/lib/chimera/error.rb
CHANGED
@@ -7,6 +7,8 @@ module Chimera
|
|
7
7
|
class ValidationErrors < Chimera::Error; end
|
8
8
|
class AttemptToModifyId < Chimera::Error; end
|
9
9
|
class AssociationClassMismatch < Chimera::Error; end
|
10
|
+
class UnhandledRiakResponseCode < Chimera::Error; end
|
11
|
+
class CannotSaveWithConflicts < Chimera::Error; end
|
10
12
|
end
|
11
13
|
end
|
12
14
|
|
data/lib/chimera/finders.rb
CHANGED
@@ -10,19 +10,27 @@ module Chimera
|
|
10
10
|
self.find_with_index(:all) { |obj| yield(obj) }
|
11
11
|
end
|
12
12
|
|
13
|
-
def find(key)
|
14
|
-
find_many(key)[0]
|
13
|
+
def find(key,opts={})
|
14
|
+
find_many([[key,opts]])[0]
|
15
15
|
end
|
16
16
|
|
17
|
-
def find_many(
|
18
|
-
keys = Array(keys)
|
17
|
+
def find_many(key_opts_arr)
|
19
18
|
found = []
|
20
19
|
threads = []
|
21
|
-
|
20
|
+
key_opts_arr = Array(key_opts_arr).collect { |e| Array(e) }
|
21
|
+
key_opts_arr.each do |key,opts|
|
22
|
+
opts ||= {}
|
22
23
|
threads << Thread.new do
|
23
24
|
if key
|
24
|
-
resp = self.connection(:riak_raw).fetch(self.to_s, key)
|
25
|
-
|
25
|
+
resp = self.connection(:riak_raw).fetch(self.to_s, key, opts)
|
26
|
+
case resp.code
|
27
|
+
when 300 then
|
28
|
+
# siblings
|
29
|
+
obj = self.new({},key,false)
|
30
|
+
obj.riak_response = resp
|
31
|
+
obj.load_sibling_attributes
|
32
|
+
found << obj
|
33
|
+
when 200 then
|
26
34
|
if resp.body and yaml_hash = YAML.load(resp.body)
|
27
35
|
hash = {}
|
28
36
|
yaml_hash.each { |k,v| hash[k.to_sym] = v }
|
@@ -34,7 +42,11 @@ module Chimera
|
|
34
42
|
obj.riak_response = resp
|
35
43
|
found << obj
|
36
44
|
end
|
37
|
-
|
45
|
+
when 404 then
|
46
|
+
nil
|
47
|
+
else
|
48
|
+
raise(Chimera::Error::UnhandledRiakResponseCode.new(resp.code.to_s))
|
49
|
+
end # case
|
38
50
|
end
|
39
51
|
end # Thread.new
|
40
52
|
end # keys.each
|
data/lib/chimera/persistence.rb
CHANGED
@@ -6,16 +6,49 @@ module Chimera
|
|
6
6
|
end
|
7
7
|
|
8
8
|
module ClassMethods
|
9
|
+
# allows for multiple conflicting values from riak
|
10
|
+
def allow_multi=(val)
|
11
|
+
props = self.bucket
|
12
|
+
props["props"]["allow_mult"] = val
|
13
|
+
self.connection(:riak_raw).set_bucket_properties(self.to_s,props)
|
14
|
+
end
|
9
15
|
end # ClassMethods
|
10
16
|
|
11
17
|
module InstanceMethods
|
12
18
|
def new?
|
13
19
|
@new == true
|
14
20
|
end
|
21
|
+
|
22
|
+
def in_conflict?
|
23
|
+
!self.sibling_attributes.nil?
|
24
|
+
end
|
25
|
+
|
26
|
+
def load_sibling_attributes
|
27
|
+
return nil unless self.riak_response.body =~ /^Sibling/
|
28
|
+
vtags = self.riak_response.body.split("\n")[1..-1]
|
29
|
+
if vtags.empty?
|
30
|
+
self.sibling_attributes = nil
|
31
|
+
else
|
32
|
+
self.sibling_attributes = {}
|
33
|
+
vtags.each do |vtag|
|
34
|
+
if resp = self.class.connection(:riak_raw).fetch(self.class.to_s, self.id, {"vtag" => vtag})
|
35
|
+
self.sibling_attributes[vtag] = YAML.load(resp.body)
|
36
|
+
else
|
37
|
+
self.sibling_attributes[vtag] = {}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end # load_sibling_attributes
|
42
|
+
|
43
|
+
def resolve_and_save
|
44
|
+
if valid?
|
45
|
+
self.sibling_attributes = nil
|
46
|
+
save
|
47
|
+
end
|
48
|
+
end
|
15
49
|
|
16
50
|
def save
|
17
|
-
|
18
|
-
raise(Chimera::Error::ValidationErrors) unless self.valid?
|
51
|
+
verify_can_save!
|
19
52
|
check_index_constraints
|
20
53
|
|
21
54
|
if new?
|
@@ -45,12 +78,23 @@ module Chimera
|
|
45
78
|
end
|
46
79
|
|
47
80
|
def save_without_callbacks
|
81
|
+
verify_can_save!
|
82
|
+
|
48
83
|
@riak_response = self.class.connection(:riak_raw).store(
|
49
84
|
self.class.bucket_key,
|
50
85
|
self.id,
|
51
86
|
YAML.dump(@attributes),
|
52
87
|
self.vector_clock)
|
53
|
-
|
88
|
+
|
89
|
+
case @riak_response.code
|
90
|
+
when 300 then
|
91
|
+
self.load_sibling_attributes
|
92
|
+
when 200 then
|
93
|
+
# all good
|
94
|
+
else
|
95
|
+
raise(Chimera::Error::UnhandledRiakResponseCode.new(@riak_response.code))
|
96
|
+
end
|
97
|
+
|
54
98
|
@orig_attributes = @attributes.clone
|
55
99
|
@new = false
|
56
100
|
end
|
@@ -65,6 +109,14 @@ module Chimera
|
|
65
109
|
freeze
|
66
110
|
end
|
67
111
|
end
|
112
|
+
|
113
|
+
protected
|
114
|
+
|
115
|
+
def verify_can_save!
|
116
|
+
raise(Chimera::Error::SaveWithoutId) unless self.id
|
117
|
+
raise(Chimera::Error::CannotSaveWithConflicts) if self.in_conflict?
|
118
|
+
raise(Chimera::Error::ValidationErrors) unless self.valid?
|
119
|
+
end
|
68
120
|
end # InstanceMethods
|
69
121
|
end
|
70
122
|
end
|
data/lib/riak_raw.rb
CHANGED
@@ -40,6 +40,12 @@ module RiakRaw
|
|
40
40
|
end; nil
|
41
41
|
end
|
42
42
|
|
43
|
+
def set_bucket_properties(name,props)
|
44
|
+
response = request(:put,
|
45
|
+
build_path(name),
|
46
|
+
Yajl::Encoder.encode(props), { 'Content-Type' => 'application/json' })
|
47
|
+
end
|
48
|
+
|
43
49
|
def store(bucket_name, key, content, vclock=nil, links=[], content_type='application/json', w=2, dw=2, r=2)
|
44
50
|
headers = { 'Content-Type' => content_type,
|
45
51
|
'X-Riak-ClientId' => self.client_id }
|
@@ -55,16 +61,19 @@ module RiakRaw
|
|
55
61
|
# returnbody=true could cause issues. instead we'll do a
|
56
62
|
# separate fetch. see: https://issues.basho.com/show_bug.cgi?id=52
|
57
63
|
if response.code == 204
|
58
|
-
response = fetch(bucket_name, key, r)
|
64
|
+
response = fetch(bucket_name, key, {:r => r})
|
59
65
|
end
|
60
66
|
|
61
67
|
response
|
62
68
|
end
|
63
69
|
|
64
|
-
def fetch(bucket_name, key,
|
70
|
+
def fetch(bucket_name, key, _params={})
|
71
|
+
params = {}
|
72
|
+
_params.each { |k,v| params[k.to_s] = v }
|
73
|
+
params["r"] ||= 2
|
65
74
|
response = request(:get,
|
66
75
|
build_path(bucket_name, key),
|
67
|
-
nil, {},
|
76
|
+
nil, {}, params)
|
68
77
|
end
|
69
78
|
|
70
79
|
# there could be concurrency issues if we don't force a short sleep
|
data/test/test_chimera.rb
CHANGED
@@ -4,6 +4,7 @@ class TestChimera < Test::Unit::TestCase
|
|
4
4
|
def setup
|
5
5
|
Car.each { |c| c.destroy }
|
6
6
|
Car.connection(:redis).flush_all
|
7
|
+
Car.allow_multi = true
|
7
8
|
end
|
8
9
|
|
9
10
|
def test_geo_indexes
|
@@ -56,7 +57,7 @@ class TestChimera < Test::Unit::TestCase
|
|
56
57
|
c2.comments = "cat dog chicken"
|
57
58
|
c2.id = Car.new_uuid
|
58
59
|
assert c2.save
|
59
|
-
|
60
|
+
|
60
61
|
c3 = Car.new
|
61
62
|
c3.make = "Porsche"
|
62
63
|
c3.model = "911"
|
@@ -69,7 +70,7 @@ class TestChimera < Test::Unit::TestCase
|
|
69
70
|
assert_equal [c,c2,c3].sort, Car.find_with_index(:comments, "dog").sort
|
70
71
|
assert_equal [c,c2].sort, Car.find_with_index(:comments, "cat").sort
|
71
72
|
assert_equal [c,c2].sort, Car.find_with_index(:comments, "cat").sort
|
72
|
-
|
73
|
+
|
73
74
|
assert_equal [c,c2,c3].sort, Car.find_with_index(:comments, {:q => "dog dolphin", :type => :union}).sort
|
74
75
|
assert_equal [c,c3].sort, Car.find_with_index(:comments, {:q => "dog dolphin", :type => :intersect}).sort
|
75
76
|
end
|
@@ -235,4 +236,42 @@ class TestChimera < Test::Unit::TestCase
|
|
235
236
|
u = User.find(u.id)
|
236
237
|
assert u.updated_at.is_a?(Time)
|
237
238
|
end
|
239
|
+
|
240
|
+
# see http://blog.basho.com/2010/01/29/why-vector-clocks-are-easy/
|
241
|
+
def test_conflicts
|
242
|
+
Car.connection(:riak_raw).client_id = "Client1"
|
243
|
+
c = Car.new
|
244
|
+
c.id = Car.new_uuid
|
245
|
+
c.make = "Nissan"
|
246
|
+
c.model = "Versa"
|
247
|
+
c.year = 2009
|
248
|
+
assert c.save
|
249
|
+
|
250
|
+
c2 = c.clone
|
251
|
+
Car.connection(:riak_raw).client_id = "Client2"
|
252
|
+
c2.year = 2008
|
253
|
+
assert c2.save
|
254
|
+
|
255
|
+
assert !c2.in_conflict?
|
256
|
+
|
257
|
+
Car.connection(:riak_raw).client_id = "Client3"
|
258
|
+
c.year = 2007
|
259
|
+
assert c.save
|
260
|
+
|
261
|
+
assert c.in_conflict?
|
262
|
+
assert_raise(Chimera::Error::CannotSaveWithConflicts) { c.save }
|
263
|
+
|
264
|
+
c2 = Car.find(c.id)
|
265
|
+
assert_raise(Chimera::Error::CannotSaveWithConflicts) { c2.save }
|
266
|
+
|
267
|
+
c.attributes = c.sibling_attributes.first[1].dup
|
268
|
+
c.year = 2006
|
269
|
+
assert c.resolve_and_save
|
270
|
+
|
271
|
+
assert !c.in_conflict?
|
272
|
+
|
273
|
+
c = Car.find(c.id)
|
274
|
+
assert !c.in_conflict?
|
275
|
+
assert_equal 2006, c.year
|
276
|
+
end
|
238
277
|
end
|
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 0
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
version: 0.0.
|
8
|
+
- 4
|
9
|
+
version: 0.0.4
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Ben Myles
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-03-
|
17
|
+
date: 2010-03-07 00:00:00 -08:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
@@ -141,6 +141,9 @@ description: |-
|
|
141
141
|
that are automatically created. There's no built in sharding for Redis, but since it's
|
142
142
|
only being used for key storage and basic data elements you should be able to go a long
|
143
143
|
way with one Redis server (especially if you use the new Redis VM).
|
144
|
+
|
145
|
+
!! Chimera is alpha. It's not production tested and needs a better test suite. !!
|
146
|
+
!! It's also only tested in Ruby 1.9. !!
|
144
147
|
email:
|
145
148
|
- ben.myles@gmail.com
|
146
149
|
executables: []
|