couchpillow 0.4.4 → 0.4.5

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 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