chimera 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- * 1 major enhancement:
4
- * Initial release
8
+ * Initial release
@@ -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 FIXME full name
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
@@ -29,5 +29,5 @@ require "chimera/persistence"
29
29
  require "chimera/base"
30
30
 
31
31
  module Chimera
32
- VERSION = '0.0.3'
32
+ VERSION = '0.0.4'
33
33
  end
@@ -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"] ||= begin
37
- case server.to_sym
38
- when :redis
39
- Redis.new(self.config[:redis])
40
- when :riak_raw
41
- RiakRaw::Client.new(self.config[:riak_raw][:host], self.config[:riak_raw][:port])
42
- else
43
- nil
44
- end
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)
@@ -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
 
@@ -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(keys)
18
- keys = Array(keys)
17
+ def find_many(key_opts_arr)
19
18
  found = []
20
19
  threads = []
21
- keys.each do |key|
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
- if resp.code == 200
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
- end
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
@@ -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
- raise(Chimera::Error::SaveWithoutId) unless self.id
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
@@ -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, r=2)
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, {}, {"r" => r})
76
+ nil, {}, params)
68
77
  end
69
78
 
70
79
  # there could be concurrency issues if we don't force a short sleep
@@ -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
- - 3
9
- version: 0.0.3
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-05 00:00:00 -08:00
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: []