chimera 0.0.3 → 0.0.4
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/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: []
|