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 +4 -4
- data/couchpillow.gemspec +1 -0
- data/lib/couchpillow/document.rb +124 -44
- data/lib/couchpillow/{validation_error.rb → errors.rb} +3 -2
- data/lib/couchpillow/version.rb +1 -1
- data/lib/couchpillow.rb +2 -1
- data/test/helper.rb +11 -5
- data/test/test_attribute.rb +0 -1
- data/test/test_document.rb +67 -2
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6c941b4db98918cd72e334fb39d97ce2bb6ea24b
|
4
|
+
data.tar.gz: 28babdcc130a232e33d10f72c8771291729b45ce
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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'
|
data/lib/couchpillow/document.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
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)
|
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
|
data/lib/couchpillow/version.rb
CHANGED
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/
|
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
|
-
|
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
|
-
|
data/test/test_attribute.rb
CHANGED
data/test/test_document.rb
CHANGED
@@ -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
|
+
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/
|
99
|
+
- lib/couchpillow/errors.rb
|
86
100
|
- lib/couchpillow/version.rb
|
87
101
|
- test/helper.rb
|
88
102
|
- test/test_attribute.rb
|