restruct 0.0.3 → 0.1.0
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/.ruby-version +1 -1
- data/lib/restruct.rb +7 -2
- data/lib/restruct/array.rb +9 -9
- data/lib/restruct/connection.rb +71 -0
- data/lib/restruct/errors.rb +54 -0
- data/lib/restruct/hash.rb +8 -8
- data/lib/restruct/locker.rb +43 -0
- data/lib/restruct/marshal_queue.rb +5 -0
- data/lib/restruct/nested_hash.rb +2 -2
- data/lib/restruct/queue.rb +38 -0
- data/lib/restruct/set.rb +5 -5
- data/lib/restruct/structure.rb +10 -6
- data/lib/restruct/version.rb +1 -1
- data/lua/register.lua +21 -0
- data/lua/unregister.lua +10 -0
- data/restruct.gemspec +2 -1
- data/spec/array_spec.rb +37 -1
- data/spec/connection_spec.rb +116 -0
- data/spec/hash_spec.rb +17 -3
- data/spec/locker_spec.rb +161 -0
- data/spec/minitest_helper.rb +4 -3
- data/spec/nested_hash_spec.rb +2 -2
- data/spec/queue_spec.rb +41 -0
- data/spec/set_spec.rb +18 -1
- metadata +55 -31
- data/lib/restruct/batch.rb +0 -39
- data/spec/batch_spec.rb +0 -53
data/lib/restruct/structure.rb
CHANGED
@@ -1,31 +1,35 @@
|
|
1
1
|
module Restruct
|
2
2
|
class Structure
|
3
3
|
|
4
|
-
attr_reader :
|
4
|
+
attr_reader :connection, :id
|
5
5
|
|
6
6
|
def initialize(options={})
|
7
|
-
@
|
7
|
+
@connection = options[:connection] || Restruct.connection
|
8
8
|
@id = Id.new options[:id] || Restruct.generate_id
|
9
9
|
end
|
10
10
|
|
11
11
|
def ==(object)
|
12
12
|
object.class == self.class &&
|
13
13
|
object.id == id &&
|
14
|
-
object.
|
14
|
+
object.connection == connection
|
15
15
|
end
|
16
16
|
alias_method :eql?, :==
|
17
17
|
|
18
18
|
def dump
|
19
|
-
|
19
|
+
connection.call 'DUMP', id
|
20
20
|
end
|
21
21
|
|
22
22
|
def restore(dump)
|
23
23
|
destroy
|
24
|
-
|
24
|
+
connection.lazy 'RESTORE', id, 0, dump
|
25
25
|
end
|
26
26
|
|
27
27
|
def destroy
|
28
|
-
|
28
|
+
connection.lazy 'DEL', id
|
29
|
+
end
|
30
|
+
|
31
|
+
def exists?
|
32
|
+
connection.call('EXISTS', id) == 1
|
29
33
|
end
|
30
34
|
|
31
35
|
end
|
data/lib/restruct/version.rb
CHANGED
data/lua/register.lua
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
local locker_id = ARGV[1]
|
2
|
+
local lock_key = ARGV[2]
|
3
|
+
local exclusive = ARGV[3]
|
4
|
+
|
5
|
+
if redis.call('EXISTS', locker_id) == 1 then
|
6
|
+
local actual_lock_key = redis.call('HGET', locker_id, 'key')
|
7
|
+
local actual_exclusive = redis.call('HGET', locker_id, 'exclusive')
|
8
|
+
|
9
|
+
local error_message = "Lock " .. lock_key .. " (exclusive=" .. exclusive .. ") fail. Alradey locked by " .. actual_lock_key .. " (exclusive=" .. actual_exclusive .. ")"
|
10
|
+
|
11
|
+
if exclusive == "true" or
|
12
|
+
actual_exclusive == 'true' or
|
13
|
+
actual_lock_key ~= lock_key then
|
14
|
+
|
15
|
+
error(error_message)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
redis.call('HSET', locker_id, 'key', lock_key)
|
20
|
+
redis.call('HSET', locker_id, 'exclusive', exclusive)
|
21
|
+
redis.call("HINCRBY", locker_id, 'nested', 1)
|
data/lua/unregister.lua
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
local locker_id = ARGV[1]
|
2
|
+
|
3
|
+
if redis.call('EXISTS', locker_id) == 0 then
|
4
|
+
error("Unregister Error. Dont exist key " .. locker_id)
|
5
|
+
else
|
6
|
+
redis.call("HINCRBY", locker_id, 'nested', -1)
|
7
|
+
if redis.call('HGET', locker_id, 'nested') == '0' then
|
8
|
+
redis.call('DEL', locker_id)
|
9
|
+
end
|
10
|
+
end
|
data/restruct.gemspec
CHANGED
@@ -18,12 +18,13 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
19
|
spec.require_paths = ['lib']
|
20
20
|
|
21
|
-
spec.add_dependency 'redic', '~> 1.
|
21
|
+
spec.add_dependency 'redic', '~> 1.5.0'
|
22
22
|
spec.add_dependency 'class_config', '~> 0.0.1'
|
23
23
|
|
24
24
|
spec.add_development_dependency 'bundler', '~> 1.6'
|
25
25
|
spec.add_development_dependency 'rake'
|
26
26
|
spec.add_development_dependency 'minitest', '~> 4.7'
|
27
|
+
spec.add_development_dependency "minitest-great_expectations"
|
27
28
|
spec.add_development_dependency 'turn', '~> 0.9'
|
28
29
|
spec.add_development_dependency 'simplecov'
|
29
30
|
spec.add_development_dependency 'pry-nav'
|
data/spec/array_spec.rb
CHANGED
@@ -7,7 +7,7 @@ require 'minitest_helper'
|
|
7
7
|
let(:array) { klass.new }
|
8
8
|
|
9
9
|
def fill(elements)
|
10
|
-
|
10
|
+
connection.call 'RPUSH', array.id, *(elements.map { |e| array.send(:serialize, e) })
|
11
11
|
end
|
12
12
|
|
13
13
|
describe 'Getters' do
|
@@ -333,6 +333,42 @@ require 'minitest_helper'
|
|
333
333
|
other.to_primitive.must_equal array.to_primitive
|
334
334
|
end
|
335
335
|
|
336
|
+
it 'Batch' do
|
337
|
+
fill %w(a b c)
|
338
|
+
|
339
|
+
array.connection.batch do
|
340
|
+
array[1] = 'x' #a x c
|
341
|
+
array << 'd' #a x c d
|
342
|
+
array.shift #x c d
|
343
|
+
array.pop(2) #x
|
344
|
+
%w(y h z).each {|e| array << e} #x y h z
|
345
|
+
array.delete 'h' #x y z
|
346
|
+
|
347
|
+
array.to_a.must_equal %w(a b c)
|
348
|
+
end
|
349
|
+
|
350
|
+
array.to_a.must_equal %w(x y z)
|
351
|
+
end
|
352
|
+
|
353
|
+
it 'Restore/Destroy in batch' do
|
354
|
+
fill %w(a b c)
|
355
|
+
|
356
|
+
dump = array.dump
|
357
|
+
other = klass.new
|
358
|
+
|
359
|
+
array.connection.batch do
|
360
|
+
array.destroy
|
361
|
+
other.restore dump
|
362
|
+
array.must_be :exists?
|
363
|
+
other.wont_be :exists?
|
364
|
+
array.to_a.must_equal %w(a b c)
|
365
|
+
end
|
366
|
+
|
367
|
+
array.wont_be :exists?
|
368
|
+
other.must_be :exists?
|
369
|
+
other.to_a.must_equal %w(a b c)
|
370
|
+
end
|
371
|
+
|
336
372
|
end
|
337
373
|
|
338
374
|
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'minitest_helper'
|
2
|
+
|
3
|
+
describe Restruct::Connection do
|
4
|
+
|
5
|
+
describe 'Success' do
|
6
|
+
it 'call' do
|
7
|
+
connection.call('HSET', 'test_id', 'key', 'test_key')
|
8
|
+
connection.call('HGET', 'test_id', 'key').must_equal 'test_key'
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'script' do
|
12
|
+
script = %Q{
|
13
|
+
local id = ARGV[1]
|
14
|
+
local key = ARGV[2]
|
15
|
+
local value = ARGV[3]
|
16
|
+
redis.call('HSET', id, key, value)
|
17
|
+
return redis.call('HGET', id, key)
|
18
|
+
}
|
19
|
+
connection.script(script, 0, 'test_id', 'key', 'test_key').must_equal 'test_key'
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'reload script when remove from the redis cache' do
|
23
|
+
script = %Q{
|
24
|
+
local id = ARGV[1]
|
25
|
+
return id
|
26
|
+
}
|
27
|
+
connection.script(script, 0, 'test').must_equal 'test'
|
28
|
+
connection.call 'SCRIPT', 'FLUSH'
|
29
|
+
connection.script(script, 0, 'test').must_equal 'test'
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
describe 'Errors' do
|
35
|
+
|
36
|
+
it 'ArgumentError' do
|
37
|
+
proc { connection.call }.must_raise ArgumentError
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'NoScriptError' do
|
41
|
+
proc { connection.call 'EVALSHA', 'invalid sha', 0 }.must_raise Restruct::NoScriptError
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'Invalid Script' do
|
45
|
+
proc { connection.script 'xyz', 0 }.must_raise Restruct::ConnectionError
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'RuntimeScriptError' do
|
49
|
+
proc { connection.script 'return x', 0 }.must_raise Restruct::ConnectionError
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
describe 'Batch' do
|
55
|
+
|
56
|
+
let (:id) {Restruct.generate_id}
|
57
|
+
|
58
|
+
it 'Execute' do
|
59
|
+
connection.lazy('HSET', id, 'key_1', 'x')
|
60
|
+
|
61
|
+
connection.batch do
|
62
|
+
connection.lazy('HSET', id, 'key_1', 'y')
|
63
|
+
connection.lazy('HSET', id, 'key_2', 'x')
|
64
|
+
|
65
|
+
connection.call('HKEYS', id).must_equal ['key_1']
|
66
|
+
connection.call('HGET', id, 'key_1').must_equal 'x'
|
67
|
+
connection.call('HGET', id, 'key_2').must_be_nil
|
68
|
+
end
|
69
|
+
|
70
|
+
connection.call('HKEYS', id).must_equal ['key_1', 'key_2']
|
71
|
+
connection.call('HGET', id, 'key_1').must_equal 'y'
|
72
|
+
connection.call('HGET', id, 'key_2').must_equal 'x'
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'Discard' do
|
76
|
+
connection.lazy('HSET', id, 'key_1', 'x')
|
77
|
+
|
78
|
+
proc do
|
79
|
+
connection.batch do
|
80
|
+
connection.lazy('HSET', id, 'key_1', 'y')
|
81
|
+
connection.lazy('HSET', id, 'key_2', 'x')
|
82
|
+
raise 'Test error'
|
83
|
+
end
|
84
|
+
end.must_raise RuntimeError
|
85
|
+
|
86
|
+
connection.call('HKEYS', id).must_equal ['key_1']
|
87
|
+
connection.call('HGET', id, 'key_1').must_equal 'x'
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'Nested' do
|
91
|
+
connection.lazy('HSET', id, 'key_1', 'x')
|
92
|
+
|
93
|
+
connection.batch do
|
94
|
+
connection.lazy('HSET', id, 'key_1', 'y')
|
95
|
+
connection.lazy('HSET', id, 'key_2', 'x')
|
96
|
+
|
97
|
+
connection.batch do
|
98
|
+
connection.lazy('HSET', id, 'key_1', 'z')
|
99
|
+
connection.lazy('HSET', id, 'key_2', 'z')
|
100
|
+
|
101
|
+
connection.call('HGET', id, 'key_1').must_equal 'x'
|
102
|
+
connection.call('HGET', id, 'key_2').must_be_nil
|
103
|
+
end
|
104
|
+
|
105
|
+
connection.call('HGET', id, 'key_1').must_equal 'x'
|
106
|
+
connection.call('HGET', id, 'key_2').must_be_nil
|
107
|
+
end
|
108
|
+
|
109
|
+
connection.call('HKEYS', id).must_equal ['key_1', 'key_2']
|
110
|
+
connection.call('HGET', id, 'key_1').must_equal 'z'
|
111
|
+
connection.call('HGET', id, 'key_2').must_equal 'z'
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
data/spec/hash_spec.rb
CHANGED
@@ -8,7 +8,7 @@ require 'minitest_helper'
|
|
8
8
|
|
9
9
|
def fill(data)
|
10
10
|
data.each { |k,v| data[k] = hash.send(:serialize, v) }
|
11
|
-
|
11
|
+
connection.call 'HMSET', hash.id, *data.flatten
|
12
12
|
end
|
13
13
|
|
14
14
|
describe 'Getters' do
|
@@ -20,7 +20,7 @@ require 'minitest_helper'
|
|
20
20
|
hash[:b].must_equal 'y'
|
21
21
|
hash[:c].must_be_nil
|
22
22
|
end
|
23
|
-
|
23
|
+
|
24
24
|
it 'fetch' do
|
25
25
|
fill a: 'x', b: 'y'
|
26
26
|
|
@@ -32,7 +32,7 @@ require 'minitest_helper'
|
|
32
32
|
error = proc { hash.fetch(:c) }.must_raise KeyError
|
33
33
|
error.message.must_equal 'key not found: c'
|
34
34
|
end
|
35
|
-
|
35
|
+
|
36
36
|
it 'key' do
|
37
37
|
fill a: 'x', b: 'y', c: 'y'
|
38
38
|
|
@@ -237,6 +237,20 @@ require 'minitest_helper'
|
|
237
237
|
other.to_primitive.must_equal hash.to_primitive
|
238
238
|
end
|
239
239
|
|
240
|
+
it 'Batch' do
|
241
|
+
fill a: 'x', b: 'y', c: 'z'
|
242
|
+
|
243
|
+
hash.connection.batch do
|
244
|
+
hash[:d] = 'w'
|
245
|
+
hash.delete :a
|
246
|
+
hash.merge! b: 'x', e: 'v'
|
247
|
+
|
248
|
+
hash.to_h.must_equal 'a' => 'x', 'b' => 'y', 'c' => 'z'
|
249
|
+
end
|
250
|
+
|
251
|
+
hash.to_h.must_equal 'b' => 'x', 'c' => 'z', 'd' => 'w', 'e' => 'v'
|
252
|
+
end
|
253
|
+
|
240
254
|
end
|
241
255
|
|
242
256
|
end
|
data/spec/locker_spec.rb
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'minitest_helper'
|
2
|
+
|
3
|
+
describe Restruct::Locker do
|
4
|
+
|
5
|
+
let(:locker) { Restruct::Locker.new }
|
6
|
+
|
7
|
+
it 'Flexible' do
|
8
|
+
locker.wont_be :locked?
|
9
|
+
|
10
|
+
locker.lock :process_1 do
|
11
|
+
locker.must_be :locked?
|
12
|
+
locker.locked_by.must_equal 'process_1'
|
13
|
+
|
14
|
+
locker.lock(:process_1) { }
|
15
|
+
locker.to_h.must_equal({"key"=>"process_1", "exclusive"=>"false", "nested"=>"1"})
|
16
|
+
|
17
|
+
error = proc { locker.lock :process_2 }.must_raise Restruct::LockerError
|
18
|
+
error.message.must_equal 'Lock process_2 (exclusive=false) fail. Alradey locked by process_1 (exclusive=false)'
|
19
|
+
end
|
20
|
+
|
21
|
+
locker.wont_be :locked?
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'Strict' do
|
25
|
+
locker.wont_be :locked?
|
26
|
+
|
27
|
+
locker.lock! :process_1 do
|
28
|
+
locker.must_be :locked?
|
29
|
+
locker.locked_by.must_equal 'process_1'
|
30
|
+
|
31
|
+
error = proc { locker.lock! :process_1 }.must_raise Restruct::LockerError
|
32
|
+
error.message.must_equal 'Lock process_1 (exclusive=true) fail. Alradey locked by process_1 (exclusive=true)'
|
33
|
+
|
34
|
+
error = proc { locker.lock :process_2 }.must_raise Restruct::LockerError
|
35
|
+
error.message.must_equal 'Lock process_2 (exclusive=false) fail. Alradey locked by process_1 (exclusive=true)'
|
36
|
+
end
|
37
|
+
|
38
|
+
locker.wont_be :locked?
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'Force unlock' do
|
42
|
+
connection.call('HSET', locker.id, 'key', :process_1)
|
43
|
+
|
44
|
+
locker.must_be :locked?
|
45
|
+
locker.locked_by.must_equal 'process_1'
|
46
|
+
|
47
|
+
locker.unlock!
|
48
|
+
|
49
|
+
locker.wont_be :locked?
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'Threads safe' do
|
53
|
+
test_id = Restruct.generate_id[:test]
|
54
|
+
|
55
|
+
threads = 10.times.map do |thread_number|
|
56
|
+
Thread.new do
|
57
|
+
10.times do |iteration|
|
58
|
+
locker.lock :process_1 do
|
59
|
+
connection.call 'RPUSH', test_id, "#{thread_number}-#{iteration}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
threads.each(&:join)
|
66
|
+
|
67
|
+
locker.wont_be :locked?
|
68
|
+
|
69
|
+
expected_list = []
|
70
|
+
10.times do |i|
|
71
|
+
10.times do |j|
|
72
|
+
expected_list << "#{i}-#{j}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
list = connection.call('LRANGE', test_id, 0, -1)
|
77
|
+
list.sort.must_equal expected_list
|
78
|
+
list.wont_equal expected_list
|
79
|
+
end
|
80
|
+
|
81
|
+
if RUBY_ENGINE != 'jruby'
|
82
|
+
it 'Multiples process' do
|
83
|
+
test_id = Restruct.generate_id[:test]
|
84
|
+
locker_id = locker.id
|
85
|
+
|
86
|
+
pids = 10.times.map do |thread_number|
|
87
|
+
Process.fork do
|
88
|
+
connection = Restruct::Connection.new
|
89
|
+
locker = Restruct::Locker.new id: locker_id, connection: connection
|
90
|
+
10.times do |iteration|
|
91
|
+
locker.lock :process_1 do
|
92
|
+
connection.call 'RPUSH', test_id, "#{thread_number}-#{iteration}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
Process.waitall
|
99
|
+
|
100
|
+
locker.wont_be :locked?
|
101
|
+
|
102
|
+
expected_list = []
|
103
|
+
10.times do |i|
|
104
|
+
10.times do |j|
|
105
|
+
expected_list << "#{i}-#{j}"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
list = connection.call('LRANGE', test_id, 0, -1)
|
110
|
+
list.sort.must_equal expected_list
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'Return block result' do
|
115
|
+
result = locker.lock :process_1 do
|
116
|
+
'OK'
|
117
|
+
end
|
118
|
+
|
119
|
+
result.must_equal 'OK'
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'Unlock when raise error in block' do
|
123
|
+
locker.wont_be :locked?
|
124
|
+
|
125
|
+
proc do
|
126
|
+
locker.lock :process_1 do
|
127
|
+
raise RuntimeError, 'ERROR'
|
128
|
+
end
|
129
|
+
end.must_raise RuntimeError, 'ERROR'
|
130
|
+
|
131
|
+
locker.wont_be :locked?
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'Batch' do
|
135
|
+
id = Restruct.generate_id[:test]
|
136
|
+
connection.lazy('HSET', id, 'key_1', 'x')
|
137
|
+
|
138
|
+
connection.batch do
|
139
|
+
connection.lazy('HSET', id, 'key_1', 'y')
|
140
|
+
|
141
|
+
locker.wont_be :locked?
|
142
|
+
|
143
|
+
locker.lock :process_1 do
|
144
|
+
locker.must_be :locked?
|
145
|
+
connection.lazy('HSET', id, 'key_2', 'x')
|
146
|
+
locker.lock :process_1 do
|
147
|
+
connection.lazy('HSET', id, 'key_2', 'z')
|
148
|
+
locker.to_h['nested'].must_equal '2'
|
149
|
+
end
|
150
|
+
locker.must_be :locked?
|
151
|
+
locker.to_h['nested'].must_equal '1'
|
152
|
+
end
|
153
|
+
|
154
|
+
locker.wont_be :locked?
|
155
|
+
end
|
156
|
+
connection.call('HKEYS', id).must_equal ['key_1', 'key_2']
|
157
|
+
connection.call('HGET', id, 'key_1').must_equal 'y'
|
158
|
+
connection.call('HGET', id, 'key_2').must_equal 'z'
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|