couchpillow 0.4.4 → 0.4.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ccd762815a2494ec8a42b99e4d8feebf29d77fac
4
- data.tar.gz: 9b0f6be497aa1a4cc2f0647ff0179953938a4be9
3
+ metadata.gz: 6c941b4db98918cd72e334fb39d97ce2bb6ea24b
4
+ data.tar.gz: 28babdcc130a232e33d10f72c8771291729b45ce
5
5
  SHA512:
6
- metadata.gz: 8771a637242a813b851b683c96fd12b8064c923ec0aafa86a491e40bf3e4d383760e4f428e756ad7518840a2990f07f5b3ff55dc4b605d7475c3a1aa3ac2e9a8
7
- data.tar.gz: b4dd50c6540f695ba3f04ecf73299ce0f517563ea9843b237dd4132c49ee8944c60c81ee305afac6d3a9c2351a65b4c40ebbabaf56b3e7179281902625c59996
6
+ metadata.gz: 5350184abf2713d2385ebb6c810047696fe38dc37a9ec2984777d8099ec342afc363ed0257e75933930dc6269b3a2f049a51a5a1420ce7dbaf6ee453758db808
7
+ data.tar.gz: d0428814d6e6811a16c43db27f096a55472f2c82b7fcb1e957f4f654e477033907af5a35bf9f72a30938183ff1e43999de52b1400c96be1a40c16b1091231f03
data/couchpillow.gemspec CHANGED
@@ -17,6 +17,7 @@ Gem::Specification.new do |s|
17
17
  s.required_ruby_version = '~> 2.0'
18
18
 
19
19
  s.add_runtime_dependency 'celluloid', '~> 0.16'
20
+ s.add_runtime_dependency 'couchbase', '~> 1.3'
20
21
 
21
22
  s.add_development_dependency 'minitest', '~> 5.3'
22
23
  s.add_development_dependency 'mocha', '~> 1.1'
@@ -10,6 +10,10 @@ module CouchPillow
10
10
 
11
11
  DEFAULT_TYPE = "couchpillow".freeze
12
12
 
13
+ EVENTS = [ :cas_conflict ].freeze
14
+
15
+ CAS_CONFLICT_RETRY_COUNT = 5
16
+
13
17
 
14
18
  attribute :_created_at do
15
19
  required
@@ -26,8 +30,18 @@ module CouchPillow
26
30
  end
27
31
 
28
32
 
29
- def initialize hash = {}, id = "#{self.class.doc_type}::#{SecureRandom.hex}"
33
+ # Constructor.
34
+ # @param hash The document
35
+ # @param id The id of the document
36
+ # @param cas CAS value of the document, from the CB client. Optional.
37
+ # @param flags Flags of the document, from the CB client. Optional.
38
+ #
39
+ def initialize hash = {},
40
+ id = "#{self.class.doc_type}::#{SecureRandom.hex}",
41
+ cas = nil
42
+
30
43
  @data = self.class.symbolize(hash)
44
+ @original = Marshal.load(Marshal.dump(@data))
31
45
 
32
46
  @id = id
33
47
  time = Time.now.utc
@@ -36,6 +50,8 @@ module CouchPillow
36
50
 
37
51
  @futures = []
38
52
 
53
+ @cas = cas
54
+
39
55
  rename!
40
56
  whitelist!
41
57
  assign_defaults!
@@ -56,25 +72,24 @@ module CouchPillow
56
72
  # Save this document to the server
57
73
  #
58
74
  def save! opts = {}
59
- whitelist!
60
- sort!
61
- timestamp!
62
- validate!
63
- to_save = @data.merge({
64
- :_type => self.class.doc_type
65
- })
66
-
67
- # write to all connections
68
- result = self.class.default_db.set(@id, to_save, opts)
69
-
70
- unless self.class.secondary_dbs.empty?
71
- @futures << Celluloid::Future.new do
72
- self.class.secondary_dbs.each do |db|
73
- db.set(@id, to_save, opts)
74
- end
75
- end
75
+ result = nil
76
+
77
+ # write to the primary db first
78
+ result = cas_handler do
79
+ whitelist!
80
+ sort!
81
+ timestamp!
82
+ validate!
83
+ opts[:cas] = @cas
84
+ self.class.default_db.set(@id, to_save, opts)
76
85
  end
77
86
 
87
+ # write to the secondary only if the primary succeeds
88
+ # and ignore CAS for secondary DBs.
89
+ write_to_secondary_dbs do |db|
90
+ db.set @id, to_save
91
+ end if result
92
+
78
93
  result
79
94
  end
80
95
 
@@ -84,13 +99,10 @@ module CouchPillow
84
99
  def delete!
85
100
  result = self.class.default_db.delete @id
86
101
 
87
- unless self.class.secondary_dbs.empty?
88
- @futures << Celluloid::Future.new do
89
- self.class.secondary_dbs.each do |db|
90
- db.delete @id
91
- end
92
- end
93
- end
102
+ # write to the secondary only if the primary succeeds
103
+ write_to_secondary_dbs do |db|
104
+ db.delete @id
105
+ end if result
94
106
 
95
107
  result
96
108
  end
@@ -99,25 +111,25 @@ module CouchPillow
99
111
  # Attempt to update this Document. Fails if this Document does not yet
100
112
  # exist in the database.
101
113
  #
102
- def update!
103
- whitelist!
104
- sort!
105
- timestamp!
106
- validate!
107
- to_save = @data.merge({
108
- :_type => self.class.doc_type
109
- })
110
-
111
- result = self.class.default_db.replace @id, to_save
112
-
113
- unless self.class.secondary_dbs.empty?
114
- @futures << Celluloid::Future.new do
115
- self.class.secondary_dbs.each do |db|
116
- db.replace @id, to_save
117
- end
118
- end
114
+ def update! opts = {}
115
+
116
+ # write to the primary db first
117
+ result = cas_handler do
118
+ whitelist!
119
+ sort!
120
+ timestamp!
121
+ validate!
122
+ opts[:cas] = @cas
123
+ result = self.class.default_db.replace(@id, to_save, opts)
119
124
  end
120
125
 
126
+ # write to the secondary only if the primary succeeds
127
+ # and ignore CAS for secondary DBs.
128
+ opts.delete :cas
129
+ write_to_secondary_dbs do |db|
130
+ db.replace @id, to_save
131
+ end if result
132
+
121
133
  result
122
134
  end
123
135
 
@@ -244,10 +256,12 @@ module CouchPillow
244
256
  # @return nil if not found or Document is of a different type.
245
257
  #
246
258
  def self.get id
247
- result = default_db.get(id) and
259
+ result, _, cas = default_db.get(id, extended: true)
260
+
261
+ result and
248
262
  type = result[:_type] || result["_type"] and
249
263
  type == doc_type and
250
- new(result, id) or
264
+ new(result, id, cas) or
251
265
  nil
252
266
  end
253
267
 
@@ -290,6 +304,14 @@ module CouchPillow
290
304
  end
291
305
 
292
306
 
307
+ # Registers a listener on a specific event.
308
+ # See {EVENTS} constant for a list of accepted events.
309
+ #
310
+ def self.on event, &block
311
+ event_listeners[event] = block if EVENTS.include?(event)
312
+ end
313
+
314
+
293
315
  private
294
316
 
295
317
 
@@ -300,6 +322,59 @@ module CouchPillow
300
322
  end
301
323
 
302
324
 
325
+ def write_to_secondary_dbs &block
326
+ unless self.class.secondary_dbs.empty?
327
+ @futures << Celluloid::Future.new do
328
+ self.class.secondary_dbs.each do |db|
329
+ block.call(db)
330
+ end
331
+ end
332
+ end
333
+ end
334
+
335
+
336
+ def cas_handler &block
337
+ # write to the primary db first
338
+ rtcount = CAS_CONFLICT_RETRY_COUNT
339
+ begin
340
+ block.call
341
+
342
+ rescue ValidationError
343
+ raise
344
+
345
+ rescue Couchbase::Error::KeyExists
346
+ other_doc, _, newcas = self.class.default_db.get(@id, extended: true)
347
+ raise CASError, "There is a CAS conflict, but DB does not yield a document" unless other_doc
348
+ raise CASError, "There is a CAS conflict, but no :cas_conflict handler has been defined. See 'on' directive." unless self.class.event_listeners[:cas_conflict]
349
+
350
+ # resolve conflict
351
+ other_doc = self.class.symbolize(other_doc)
352
+ @data = self.class.event_listeners[:cas_conflict].call(@original, other_doc, to_save)
353
+ @cas = newcas
354
+
355
+ rtcount -= 1
356
+ raise CASError, "Exhausted retries" if rtcount == 0
357
+ retry
358
+
359
+ end
360
+ end
361
+
362
+
363
+ # Get the final hash that will be saved to the database.
364
+ # Check if data has been modified.
365
+ #
366
+ def to_save
367
+ hash = @data.hash
368
+ return @tos if @tos && @toshash && @toshash == hash
369
+
370
+ @tos = @data.merge({
371
+ :_type => self.class.doc_type
372
+ })
373
+ @toshash = @data.hash
374
+ @tos
375
+ end
376
+
377
+
303
378
  def self.doc_type
304
379
  @type ||= DEFAULT_TYPE
305
380
  end
@@ -320,6 +395,11 @@ module CouchPillow
320
395
  end
321
396
 
322
397
 
398
+ def self.event_listeners
399
+ @event_listeners ||= {}
400
+ end
401
+
402
+
323
403
  def self.symbolize hash
324
404
  hash.inject({}) do |memo,(k,v)|
325
405
  memo[k.to_sym] = v
@@ -1,8 +1,9 @@
1
1
  module CouchPillow
2
2
 
3
- # Validation Error.
4
- #
5
3
  class ValidationError < StandardError
6
4
  end
7
5
 
6
+ class CASError < StandardError
7
+ end
8
+
8
9
  end
@@ -1,5 +1,5 @@
1
1
  module CouchPillow
2
2
  GEM_NAME = "couchpillow"
3
3
  NAME = "CouchPillow"
4
- VERSION = "0.4.4"
4
+ VERSION = "0.4.5"
5
5
  end
data/lib/couchpillow.rb CHANGED
@@ -2,6 +2,7 @@ require 'securerandom'
2
2
  require 'json'
3
3
  require 'time'
4
4
  require 'celluloid/autostart'
5
+ require 'couchbase'
5
6
 
6
7
  module CouchPillow
7
8
 
@@ -19,7 +20,7 @@ end
19
20
 
20
21
  require 'couchpillow/attribute'
21
22
  require 'couchpillow/attributive'
22
- require 'couchpillow/validation_error'
23
+ require 'couchpillow/errors'
23
24
  require 'couchpillow/boolean'
24
25
  require 'couchpillow/document'
25
26
  require 'couchpillow/version'
data/test/helper.rb CHANGED
@@ -9,28 +9,34 @@ class FakeCouchbaseServer
9
9
 
10
10
  def initialize
11
11
  @storage = {}
12
+ @cas = {}
12
13
  end
13
14
 
14
15
 
15
16
  def set id, data, opts = {}
17
+ raise Couchbase::Error::KeyExists if @storage.has_key?(id) && opts[:cas] && @cas[id] != opts[:cas]
16
18
  @storage[id] = data
19
+ @cas[id] = SecureRandom.hex(8)
17
20
  end
18
21
 
19
22
 
20
23
  def delete id
21
24
  @storage.delete(id)
25
+ @cas.delete(id)
22
26
  end
23
27
 
24
28
 
25
- def replace id, data
26
- @storage.has_key?(id) or raise "Document does not exist"
29
+ def replace id, data, opts = {}
30
+ raise "Document does not exist" unless @storage.has_key?(id)
31
+ raise Couchbase::Error::KeyExists if @storage.has_key?(id) && opts[:cas] && @cas[id] != opts[:cas]
32
+
27
33
  @storage[id] = data
34
+ @cas[id] = SecureRandom.hex(8)
28
35
  end
29
36
 
30
37
 
31
- def get id
38
+ def get id, opts = {}
39
+ return [@storage[id], nil, @cas[id]] if opts[:extended] == true
32
40
  @storage[id]
33
41
  end
34
42
  end
35
-
36
-
@@ -118,5 +118,4 @@ class TestAttribute < Minitest::Test
118
118
  end
119
119
  end
120
120
 
121
-
122
121
  end
@@ -209,7 +209,7 @@ class TestDocument < Minitest::Test
209
209
 
210
210
 
211
211
  def test_get_returns_nil
212
- CouchPillow.db.expects(:get).with('123').returns(nil)
212
+ CouchPillow.db.expects(:get).with('123', { extended: true }).returns(nil)
213
213
  Document.expects(:default_db).returns(CouchPillow.db)
214
214
  d = Document.get('123')
215
215
  assert_equal nil, d
@@ -223,7 +223,7 @@ class TestDocument < Minitest::Test
223
223
 
224
224
  CouchPillow.db
225
225
  .expects(:get)
226
- .with('123')
226
+ .with('123', { extended: true })
227
227
  .returns( { '_type' => 'something else', 'stuff' => 'data' } )
228
228
 
229
229
  d = klass.get('123')
@@ -486,4 +486,69 @@ class TestDocument < Minitest::Test
486
486
  assert_equal "hello", data[:foo]
487
487
  end
488
488
 
489
+
490
+ def test_cas_no_on_directive
491
+
492
+ klass = Class.new(Document) do
493
+ type 'test'
494
+
495
+ attribute :foo do
496
+ type String
497
+ auto_convert
498
+ required
499
+ default { "tester" }
500
+ end
501
+ end
502
+
503
+ d = klass.new( foo: "Hello" )
504
+ d.save!
505
+ doc_id = d.id
506
+
507
+ d1 = klass.get(doc_id)
508
+ d2 = klass.get(doc_id)
509
+ d2.foo = "Someone else"
510
+ d2.save!
511
+ assert_raises CouchPillow::CASError do
512
+ d1.save!
513
+ end
514
+ end
515
+
516
+
517
+ def test_cas_with_on_directive
518
+ # must declare it here so Ruby can see the assert_equal methods
519
+ cas_conflict_resolver = Proc.new do |original, theirs, mine|
520
+ assert_equal "Hello", original[:foo]
521
+ assert_equal "Someone else", theirs[:foo]
522
+ assert_equal "This is mine", mine[:foo]
523
+ mine
524
+ end
525
+
526
+ klass = Class.new(Document) do
527
+ type 'test'
528
+
529
+ attribute :foo do
530
+ type String
531
+ auto_convert
532
+ required
533
+ default { "tester" }
534
+ end
535
+
536
+ on :cas_conflict, &cas_conflict_resolver
537
+ end
538
+
539
+ d = klass.new( foo: "Hello" )
540
+ d.save!
541
+ doc_id = d.id
542
+
543
+ d1 = klass.get(doc_id)
544
+ d1.foo = "This is mine"
545
+ d2 = klass.get(doc_id)
546
+ d2.foo = "Someone else"
547
+ d2.save!
548
+ d1.save!
549
+
550
+ assert_equal "This is mine", klass.get(doc_id).foo
551
+ end
552
+
553
+
489
554
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: couchpillow
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.4
4
+ version: 0.4.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Albert Tedja
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: couchbase
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.3'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: minitest
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -82,7 +96,7 @@ files:
82
96
  - lib/couchpillow/attributive.rb
83
97
  - lib/couchpillow/boolean.rb
84
98
  - lib/couchpillow/document.rb
85
- - lib/couchpillow/validation_error.rb
99
+ - lib/couchpillow/errors.rb
86
100
  - lib/couchpillow/version.rb
87
101
  - test/helper.rb
88
102
  - test/test_attribute.rb